Merge pull request 'develop' (#3) from develop into master

Reviewed-on: #3
This commit is contained in:
2026-03-23 04:51:22 +00:00
340 changed files with 17463 additions and 895 deletions

View File

@@ -1,31 +0,0 @@
### Note
Copy the example and rename it to your desired name. Plugins define a ui target slot with the 'ui_target' requests data but don't have to if not directly interacted with.
Plugins must have a run method defined; though, you do not need to necessarily do anything within it. The run method implies that the passed in event system or other data is ready for the plugin to use.
### Manifest Example (All are required even if empty.)
```
class Manifest:
name: str = "Example Plugin"
author: str = "John Doe"
version: str = "0.0.1"
support: str = ""
requests: {} = {
'pass_ui_objects': ["plugin_control_list"],
'pass_events': "true",
'bind_keys': []
}
pre_launch: bool = False
```
### Requests
```
requests: {} = {
'pass_events': "true", # If empty or not present will be ignored.
"pass_ui_objects": [""], # Request reference to a UI component. Will be passed back as array to plugin.
'bind_keys': [f"{name}||send_message:<Control>f"],
f"{name}||do_save:<Control>s"] # Bind keys with method and key pare using list. Must pass "name" like shown with delimiter to its right.
}
```

View File

@@ -0,0 +1,3 @@
"""
Plugin Module
"""

View File

@@ -0,0 +1,3 @@
"""
Plugin Package
"""

View File

@@ -0,0 +1,97 @@
# Python imports
# Lib imports
# Application imports
class Autopairs:
def __init__(self):
...
def handle_word_wrap(self, buffer, char_str: str):
wrap_block = self.get_wrap_block(char_str)
if not wrap_block: return
selection = buffer.get_selection_bounds()
if not selection:
self.insert_pair(buffer, char_str, wrap_block)
return True
self.wrap_selection(buffer, char_str, wrap_block, selection)
return True
def insert_pair(
self, buffer, char_str: str, wrap_block: tuple
):
buffer.begin_user_action()
left_block, right_block = wrap_block
insert_mark = buffer.get_insert()
insert_itr = buffer.get_iter_at_mark(insert_mark)
buffer.insert(insert_itr, f"{left_block}{right_block}")
insert_itr = buffer.get_iter_at_mark( insert_mark )
insert_itr.backward_char()
buffer.place_cursor(insert_itr)
buffer.end_user_action()
def wrap_selection(
self, buffer, char_str: str, wrap_block: tuple, selection
):
left_block, \
right_block = wrap_block
start_itr, \
end_itr = selection
data = buffer.get_text(
start_itr, end_itr, include_hidden_chars = False
)
start_mark = buffer.create_mark("startclose", start_itr, False)
end_mark = buffer.create_mark("endclose", end_itr, True)
buffer.begin_user_action()
buffer.insert(start_itr, left_block)
end_itr = buffer.get_iter_at_mark(end_mark)
buffer.insert(end_itr, right_block)
start = buffer.get_iter_at_mark(start_mark)
end = buffer.get_iter_at_mark(end_mark)
buffer.select_range(start, end)
buffer.delete_mark_by_name("startclose")
buffer.delete_mark_by_name("endclose")
buffer.end_user_action()
def get_wrap_block(self, char_str) -> tuple:
left_block = ""
right_block = ""
match char_str:
case "(" | ")":
left_block = "("
right_block = ")"
case "[" | "]":
left_block = "["
right_block = "]"
case "{" | "}":
left_block = "{"
right_block = "}"
case '"':
left_block = '"'
right_block = '"'
case "'":
left_block = "'"
right_block = "'"
case "`":
left_block = "`"
right_block = "`"
case _:
return ()
return left_block, right_block

View File

@@ -0,0 +1,7 @@
{
"name": "Autopairs",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"requests": {}
}

View File

@@ -0,0 +1,73 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
from .autopairs import Autopairs
autopairs = Autopairs()
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
...
def load(self):
event = Event_Factory.create_event("register_command",
command_name = "autopairs",
command = Handler,
binding_mode = "held",
binding = [
"'", "`", "[", "]",
'<Shift>"',
'<Shift>(',
'<Shift>)',
'<Shift>{',
'<Shift>}'
]
)
self.emit_to("source_views", event)
def unload(self):
event = Event_Factory.create_event("unregister_command",
command_name = "autopairs",
command = Handler,
binding_mode = "held",
binding = [
"'", "`", "[", "]",
'<Shift>"',
'<Shift>(',
'<Shift>)',
'<Shift>{',
'<Shift>}'
]
)
self.emit_to("source_views", event)
autopairs = None
def run(self):
...
class Handler:
@staticmethod
def execute(
view: any,
char_str: str,
*args,
**kwargs
):
logger.debug("Command: Autopairs")
autopairs.handle_word_wrap(view.get_buffer(), char_str)

View File

@@ -0,0 +1,3 @@
"""
Plugin Module
"""

View File

@@ -0,0 +1,3 @@
"""
Plugin Package
"""

View File

@@ -0,0 +1,97 @@
# Python imports
# Lib imports
# Application imports
class Autopairs:
def __init__(self):
...
def handle_word_wrap(self, buffer, char_str: str):
wrap_block = self.get_wrap_block(char_str)
if not wrap_block: return
selection = buffer.get_selection_bounds()
if not selection:
self.insert_pair(buffer, char_str, wrap_block)
return True
self.wrap_selection(buffer, char_str, wrap_block, selection)
return True
def insert_pair(
self, buffer, char_str: str, wrap_block: tuple
):
buffer.begin_user_action()
left_block, right_block = wrap_block
insert_mark = buffer.get_insert()
insert_itr = buffer.get_iter_at_mark(insert_mark)
buffer.insert(insert_itr, f"{left_block}{right_block}")
insert_itr = buffer.get_iter_at_mark( insert_mark )
insert_itr.backward_char()
buffer.place_cursor(insert_itr)
buffer.end_user_action()
def wrap_selection(
self, buffer, char_str: str, wrap_block: tuple, selection
):
left_block, \
right_block = wrap_block
start_itr, \
end_itr = selection
data = buffer.get_text(
start_itr, end_itr, include_hidden_chars = False
)
start_mark = buffer.create_mark("startclose", start_itr, False)
end_mark = buffer.create_mark("endclose", end_itr, True)
buffer.begin_user_action()
buffer.insert(start_itr, left_block)
end_itr = buffer.get_iter_at_mark(end_mark)
buffer.insert(end_itr, right_block)
start = buffer.get_iter_at_mark(start_mark)
end = buffer.get_iter_at_mark(end_mark)
buffer.select_range(start, end)
buffer.delete_mark_by_name("startclose")
buffer.delete_mark_by_name("endclose")
buffer.end_user_action()
def get_wrap_block(self, char_str) -> tuple:
left_block = ""
right_block = ""
match char_str:
case "(" | ")":
left_block = "("
right_block = ")"
case "[" | "]":
left_block = "["
right_block = "]"
case "{" | "}":
left_block = "{"
right_block = "}"
case '"':
left_block = '"'
right_block = '"'
case "'":
left_block = "'"
right_block = "'"
case "`":
left_block = "`"
right_block = "`"
case _:
return ()
return left_block, right_block

View File

@@ -0,0 +1,7 @@
{
"name": "File History",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"requests": {}
}

View File

@@ -0,0 +1,65 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
history: list = []
history_size: int = 30
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
if isinstance(event, Code_Event_Types.RemovedFileEvent):
if event.file.ftype == "buffer": return
if len(history) == history_size:
history.pop(0)
history.append(event.file)
def load(self):
self._manage_signals("register_command")
def unload(self):
self._manage_signals("unregister_command")
def _manage_signals(self, action: str):
event = Event_Factory.create_event(action,
command_name = "file_history_pop",
command = Handler,
binding_mode = "released",
binding = "<Shift><Control>t"
)
self.emit_to("source_views", event)
def run(self):
...
class Handler:
@staticmethod
def execute(
view: any,
char_str: str,
*args,
**kwargs
):
logger.debug("Command: File History")
if len(history) == 0: return
view._on_uri_data_received(
[
history.pop().replace("file://", "")
]
)

View File

@@ -0,0 +1,44 @@
# Python imports
# Lib imports
import gi
gi.require_version('GtkSource', '4')
from gi.repository import GtkSource
# Application imports
from .helpers import clear_temp_cut_buffer_delayed, set_temp_cut_buffer_delayed
class Handler:
@staticmethod
def execute(
view: GtkSource.View,
*args,
**kwargs
):
logger.debug("Command: Cut to Temp Buffer")
clear_temp_cut_buffer_delayed(view)
buffer = view.get_buffer()
itr = buffer.get_iter_at_mark(buffer.get_insert())
start_itr = itr.copy()
start_itr.set_line_offset(0)
end_itr = start_itr.copy()
if not end_itr.forward_line():
end_itr = buffer.get_end_iter()
if not hasattr(view, "_cut_buffer"):
view._cut_buffer = ""
line_str = buffer.get_text(start_itr, end_itr, True)
view._cut_buffer += line_str
buffer.delete(start_itr, end_itr)
set_temp_cut_buffer_delayed(view)

View File

@@ -0,0 +1,24 @@
# Python imports
# Lib imports
import gi
from gi.repository import GLib
# Application imports
def clear_temp_cut_buffer_delayed(view: any):
if not hasattr(view, "_cut_temp_timeout_id"): return
if not view._cut_temp_timeout_id: return
GLib.source_remove(view._cut_temp_timeout_id)
def set_temp_cut_buffer_delayed(view: any):
def clear_temp_buffer(view: any):
view._cut_buffer = ""
view._cut_temp_timeout_id = None
return False
view._cut_temp_timeout_id = GLib.timeout_add(15000, clear_temp_buffer, view)

View File

@@ -0,0 +1,7 @@
{
"name": "Nanoesq Temp Buffer",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"requests": {}
}

View File

@@ -0,0 +1,35 @@
# Python imports
# Lib imports
import gi
gi.require_version('GtkSource', '4')
from gi.repository import GLib
from gi.repository import GtkSource
# Application imports
from .helpers import clear_temp_cut_buffer_delayed, set_temp_cut_buffer_delayed
class Handler2:
@staticmethod
def execute(
view: GtkSource.View,
*args,
**kwargs
):
logger.debug("Command: Paste Temp Buffer")
if not hasattr(view, "_cut_temp_timeout_id"): return
if not hasattr(view, "_cut_buffer"): return
if not view._cut_buffer: return
clear_temp_cut_buffer_delayed(view)
buffer = view.get_buffer()
itr = buffer.get_iter_at_mark( buffer.get_insert() )
insert_itr = itr.copy()
buffer.insert(insert_itr, view._cut_buffer, -1)
set_temp_cut_buffer_delayed(view)

View File

@@ -0,0 +1,49 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
from .cut_to_temp_buffer import Handler
from .paste_temp_buffer import Handler2
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
...
def load(self):
self._manage_signals("register_command")
def load(self):
self._manage_signals("unregister_command")
def _manage_signals(self, action: str):
event = Event_Factory.create_event(action,
command_name = "cut_to_temp_buffer",
command = Handler,
binding_mode = "held",
binding = "<Control>k"
)
self.emit_to("source_views", event)
event = Event_Factory.create_event(action,
command_name = "paste_temp_buffer",
command = Handler2,
binding_mode = "held",
binding = "<Control>u"
)
self.emit_to("source_views", event)
def run(self):
...

View File

@@ -0,0 +1,3 @@
"""
Plugin Module
"""

View File

@@ -0,0 +1,3 @@
"""
Plugin Package
"""

View File

@@ -0,0 +1,7 @@
{
"name": "Toggle Source View",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"requests": {}
}

View File

@@ -0,0 +1,64 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
...
def load(self):
event = Event_Factory.create_event("register_command",
command_name = "toggle_source_view",
command = Handler,
binding_mode = "released",
binding = "<Shift><Control>h"
)
self.emit_to("source_views", event)
def unload(self):
event = Event_Factory.create_event("unregister_command",
command_name = "toggle_source_view",
command = Handler,
binding_mode = "released",
binding = "<Shift><Control>h"
)
self.emit_to("source_views", event)
def run(self):
...
class Handler:
@staticmethod
def execute(
view: any,
char_str: str,
*args,
**kwargs
):
logger.debug("Command: Toggle Source View")
target = view.get_parent()
target.hide() if target.is_visible() else target.show()
if view.sibling_left:
target = view.sibling_left.get_parent()
target.show()
view.sibling_left.grab_focus()
if view.sibling_right:
target = view.sibling_right.get_parent()
target.show()
view.sibling_right.grab_focus()

View File

@@ -0,0 +1,3 @@
"""
Pligin Module
"""

View File

@@ -0,0 +1,3 @@
"""
Pligin Package
"""

View File

@@ -0,0 +1,7 @@
{
"name": "Example Completer",
"author": "John Doe",
"version": "0.0.1",
"support": "",
"requests": {}
}

View File

@@ -0,0 +1,49 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
from .provider import Provider
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
self.provider: Provider = None
def _controller_message(self, event: Code_Event_Types.CodeEvent):
...
def load(self):
self.provider = Provider()
event = Event_Factory.create_event(
"register_provider",
provider_name = "Example Completer",
provider = self.provider,
language_ids = []
)
self.emit_to("completion", event)
def unload(self):
event = Event_Factory.create_event(
"unregister_provider",
provider_name = "Example Completer"
)
self.emit_to("completion", event)
self.provider = None
del self.provider
def run(self):
...

View File

@@ -0,0 +1,76 @@
# Python imports
# Lib imports
import gi
gi.require_version('GtkSource', '4')
from gi.repository import GObject
from gi.repository import GtkSource
# Application imports
from .provider_response_cache import ProviderResponseCache
class Provider(GObject.GObject, GtkSource.CompletionProvider):
"""
This is a custom Completion Example Provider.
# NOTE: used information from here --> https://warroom.rsmus.com/do-that-auto-complete/
"""
__gtype_name__ = 'ExampleCompletionProvider'
def __init__(self):
super(Provider, self).__init__()
self.response_cache: ProviderResponseCache = ProviderResponseCache()
def do_get_name(self):
""" Returns: a new string containing the name of the provider. """
return 'Example Code Completion'
def do_match(self, context):
# Note: If provider is in interactive activation then need to check
# view focus as otherwise non focus views start trying to grab it.
completion = context.get_property("completion")
if not completion.get_view().has_focus(): return
word = self.response_cache.get_word(context)
if not word or len(word) < 2: return False
return True
def do_get_priority(self):
""" Determin position in result list along other providor results. """
return 5
def do_activate_proposal(self, proposal, iter_):
""" Manually handle actual completion insert or set flags and handle normally. """
buffer = iter_.get_buffer()
# Note: Flag mostly intended for SourceViewsMultiInsertState
# to insure marker processes inserted text correctly.
buffer.is_processing_completion = True
return False
def do_get_activation(self):
""" The context for when a provider will show results """
# return GtkSource.CompletionActivation.NONE
# return GtkSource.CompletionActivation.USER_REQUESTED
# return GtkSource.CompletionActivation.USER_REQUESTED | GtkSource.CompletionActivation.INTERACTIVE
return GtkSource.CompletionActivation.INTERACTIVE
def do_populate(self, context):
results = self.response_cache.filter_with_context(context)
proposals = []
for entry in results:
proposals.append(
self.response_cache.create_completion_item(
entry["label"],
entry["text"],
entry["info"]
)
)
context.add_proposals(self, proposals, True)

View File

@@ -0,0 +1,113 @@
# Python imports
from concurrent.futures import ThreadPoolExecutor
import re
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('GtkSource', '4')
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import GtkSource
# Application imports
from libs.event_factory import Code_Event_Types
from core.widgets.code.completion_providers.provider_response_cache_base import ProviderResponseCacheBase
class ProviderResponseCache(ProviderResponseCacheBase):
def __init__(self):
super(ProviderResponseCache, self).__init__()
# Note: Using asyncio.run causes a keyboard trap that prevents app
# closure from terminal. ThreadPoolExecutor seems to not have such issues...
self.executor = ThreadPoolExecutor(max_workers = 1)
self.matchers: dict = {
"hello": {
"label": "Hello, World!",
"text": "Hello, World!",
"info": GLib.markup_escape_text( "<b>Says the first ever program developers write...</b>" )
},
"foo": {
"label": "foo",
"text": "foo }}"
},
"bar": {
"label": "bar",
"text": "bar }}"
}
}
def process_file_load(self, event: Code_Event_Types.AddedNewFileEvent):
...
def process_file_close(self, event: Code_Event_Types.RemovedFileEvent):
...
def process_file_save(self, event: Code_Event_Types.SavedFileEvent):
...
def process_file_change(self, event: Code_Event_Types.TextChangedEvent):
...
def filter(self, word: str) -> list[dict]:
...
def filter_with_context(self, context) -> list[dict]:
"""
In this instance, it will do 2 things:
1) always provide Hello World! (Not ideal but an option so its in the example)
2) Utilizes the Gtk.TextIter from the TextBuffer to determine if there is a jinja
example of '{{ custom.' if so it will provide you with the options of foo and bar.
If selected it will insert foo }} or bar }}, completing your syntax...
PLEASE NOTE the GtkTextIter Logic and regex are really rough and should be adjusted and tuned
"""
proposals: list[dict] = [
{
"label": self.matchers[ "hello" ]["label"],
"text": self.matchers[ "hello" ]["text"],
"info": self.matchers[ "hello" ]["info"]
}
]
# Gtk Versions differ on get_iter responses...
end_iter = context.get_iter()
if not isinstance(end_iter, Gtk.TextIter):
_, end_iter = context.get_iter()
if not end_iter: return
buf = end_iter.get_buffer()
mov_iter = end_iter.copy()
if mov_iter.backward_search('{{', Gtk.TextSearchFlags.VISIBLE_ONLY):
mov_iter, _ = mov_iter.backward_search('{{', Gtk.TextSearchFlags.VISIBLE_ONLY)
left_text = buf.get_text(mov_iter, end_iter, True)
else:
left_text = ''
if re.match(r'.*\{\{\s*custom\.$', left_text):
# optionally proposed based on left search via regex
proposals.append(
{
"label": self.matchers[ "foo" ]["label"],
"text": self.matchers[ "foo" ]["text"],
"info": ""
}
)
# optionally proposed based on left search via regex
proposals.append(
{
"label": self.matchers[ "bar" ]["label"],
"text": self.matchers[ "bar" ]["text"],
"info": ""
}
)
return proposals

View File

@@ -0,0 +1,3 @@
"""
Pligin Module
"""

View File

@@ -0,0 +1,3 @@
"""
Pligin Package
"""

View File

@@ -0,0 +1,3 @@
from .parser import load, loads
from .writer import dump, dumps
from .speg import ParseError

View File

@@ -0,0 +1,295 @@
from .speg import peg
import re, sys
if sys.version_info[0] == 2:
_chr = unichr
else:
_chr = chr
def load(fin):
return loads(fin.read())
def loads(s):
if isinstance(s, bytes):
s = s.decode('utf-8')
if s.startswith(u'\ufeff'):
s = s[1:]
return peg(s.replace('\r\n', '\n'), _p_root)
def _p_ws(p):
p('[ \t]*')
def _p_nl(p):
p(r'([ \t]*(?:#[^\n]*)?\r?\n)+')
def _p_ews(p):
with p:
p(_p_nl)
p(_p_ws)
def _p_id(p):
return p(r'[$a-zA-Z_][$0-9a-zA-Z_]*')
_escape_table = {
'r': '\r',
'n': '\n',
't': '\t',
'f': '\f',
'b': '\b',
}
def _p_unescape(p):
esc = p('\\\\(?:u[0-9a-fA-F]{4}|[^\n])')
if esc[1] == 'u':
return _chr(int(esc[2:], 16))
return _escape_table.get(esc[1:], esc[1:])
_re_indent = re.compile(r'[ \t]*')
def _p_block_str(p, c):
p(r'{c}{c}{c}'.format(c=c))
lines = [['']]
with p:
while True:
s = p(r'(?:{c}(?!{c}{c})|[^{c}\\])*'.format(c=c))
l = s.split('\n')
lines[-1].append(l[0])
lines.extend([x] for x in l[1:])
if p(r'(?:\\\n[ \t]*)*'):
continue
p.commit()
lines[-1].append(p(_p_unescape))
p(r'{c}{c}{c}'.format(c=c))
lines = [''.join(l) for l in lines]
strip_ws = len(lines) > 1
if strip_ws and all(c in ' \t' for c in lines[-1]):
lines.pop()
indent = None
for line in lines[1:]:
if not line:
continue
if indent is None:
indent = _re_indent.match(line).group(0)
continue
for i, (c1, c2) in enumerate(zip(indent, line)):
if c1 != c2:
indent = indent[:i]
break
ind_len = len(indent or '')
if strip_ws and all(c in ' \t' for c in lines[0]):
lines = [line[ind_len:] for line in lines[1:]]
else:
lines[1:] = [line[ind_len:] for line in lines[1:]]
return '\n'.join(lines)
_re_mstr_nl = re.compile(r'(?:[ \t]*\n)+[ \t]*')
_re_mstr_trailing_nl = re.compile(_re_mstr_nl.pattern + r'\Z')
def _p_multiline_str(p, c):
p('{c}(?!{c}{c})(?:[ \t]*\n[ \t]*)?'.format(c=c))
string_parts = []
with p:
while True:
string_parts.append(p(r'[^{c}\\]*'.format(c=c)))
if p(r'(?:\\\n[ \t]*)*'):
string_parts.append('')
continue
p.commit()
string_parts.append(p(_p_unescape))
p(c)
string_parts[-1] = _re_mstr_trailing_nl.sub('', string_parts[-1])
string_parts[::2] = [_re_mstr_nl.sub(' ', part) for part in string_parts[::2]]
return ''.join(string_parts)
def _p_string(p):
with p:
return p(_p_block_str, '"')
with p:
return p(_p_block_str, "'")
with p:
return p(_p_multiline_str, '"')
return p(_p_multiline_str, "'")
def _p_array_value(p):
with p:
p(_p_nl)
return p(_p_object)
with p:
p(_p_ws)
return p(_p_line_object)
p(_p_ews)
return p(_p_simple_value)
def _p_key(p):
with p:
return p(_p_id)
return p(_p_string)
def _p_flow_kv(p):
k = p(_p_key)
p(_p_ews)
p(':')
with p:
p(_p_nl)
return k, p(_p_object)
with p:
p(_p_ws)
return k, p(_p_line_object)
p(_p_ews)
return k, p(_p_simple_value)
def _p_flow_obj_sep(p):
with p:
p(_p_ews)
p(',')
p(_p_ews)
return
p(_p_nl)
p(_p_ws)
def _p_simple_value(p):
with p:
p('null')
return None
with p:
p('false')
return False
with p:
p('true')
return True
with p:
return int(p('0b[01]+')[2:], 2)
with p:
return int(p('0o[0-7]+')[2:], 8)
with p:
return int(p('0x[0-9a-fA-F]+')[2:], 16)
with p:
return float(p(r'-?(?:[1-9][0-9]*|0)?\.[0-9]+(?:[Ee][\+-]?[0-9]+)?|(?:[1-9][0-9]*|0)(?:\.[0-9]+)?[Ee][\+-]?[0-9]+'))
with p:
return int(p('-?[1-9][0-9]*|0'), 10)
with p:
return p(_p_string)
with p:
p(r'\[')
r = []
with p:
p.set('I', '')
r.append(p(_p_array_value))
with p:
while True:
with p:
p(_p_ews)
p(',')
rr = p(_p_array_value)
if not p:
p(_p_nl)
with p:
rr = p(_p_object)
if not p:
p(_p_ews)
rr = p(_p_simple_value)
r.append(rr)
p.commit()
with p:
p(_p_ews)
p(',')
p(_p_ews)
p(r'\]')
return r
p(r'\{')
r = {}
p(_p_ews)
with p:
p.set('I', '')
k, v = p(_p_flow_kv)
r[k] = v
with p:
while True:
p(_p_flow_obj_sep)
k, v = p(_p_flow_kv)
r[k] = v
p.commit()
p(_p_ews)
with p:
p(',')
p(_p_ews)
p(r'\}')
return r
def _p_line_kv(p):
k = p(_p_key)
p(_p_ws)
p(':')
p(_p_ws)
with p:
p(_p_nl)
p(p.get('I'))
return k, p(_p_indented_object)
with p:
return k, p(_p_line_object)
with p:
return k, p(_p_simple_value)
p(_p_nl)
p(p.get('I'))
p('[ \t]')
p(_p_ws)
return k, p(_p_simple_value)
def _p_line_object(p):
k, v = p(_p_line_kv)
r = { k: v }
with p:
while True:
p(_p_ws)
p(',')
p(_p_ws)
k, v = p(_p_line_kv)
r[k] = v # uniqueness
p.commit()
return r
def _p_object(p):
p.set('I', p.get('I') + p('[ \t]*'))
r = p(_p_line_object)
with p:
while True:
p(_p_ws)
with p:
p(',')
p(_p_nl)
p(p.get('I'))
rr = p(_p_line_object)
r.update(rr) # unqueness
p.commit()
return r
def _p_indented_object(p):
p.set('I', p.get('I') + p('[ \t]'))
return p(_p_object)
def _p_root(p):
with p:
p(_p_nl)
with p:
p.set('I', '')
r = p(_p_object)
p(_p_ws)
with p:
p(',')
if not p:
p(_p_ws)
r = p(_p_simple_value)
p(_p_ews)
p(p.eof)
return r

View File

@@ -0,0 +1 @@
from .peg import peg, ParseError

View File

@@ -0,0 +1,147 @@
import sys, re
class ParseError(Exception):
def __init__(self, msg, text, offset, line, col):
self.msg = msg
self.text = text
self.offset = offset
self.line = line
self.col = col
super(ParseError, self).__init__(msg, offset, line, col)
if sys.version_info[0] == 2:
_basestr = basestring
else:
_basestr = str
def peg(s, r):
p = _Peg(s)
try:
return p(r)
except _UnexpectedError as e:
offset = max(p._errors)
err = p._errors[offset]
raise ParseError(err.msg, s, offset, err.line, err.col)
class _UnexpectedError(RuntimeError):
def __init__(self, state, expr):
self.state = state
self.expr = expr
class _PegState:
def __init__(self, pos, line, col):
self.pos = pos
self.line = line
self.col = col
self.vars = {}
self.commited = False
class _PegError:
def __init__(self, msg, line, col):
self.msg = msg
self.line = line
self.col = col
class _Peg:
def __init__(self, s):
self._s = s
self._states = [_PegState(0, 1, 1)]
self._errors = {}
self._re_cache = {}
def __call__(self, r, *args, **kw):
if isinstance(r, _basestr):
compiled = self._re_cache.get(r)
if not compiled:
compiled = re.compile(r)
self._re_cache[r] = compiled
st = self._states[-1]
m = compiled.match(self._s[st.pos:])
if not m:
self.error(expr=r, err=kw.get('err'))
ms = m.group(0)
st.pos += len(ms)
nl_pos = ms.rfind('\n')
if nl_pos < 0:
st.col += len(ms)
else:
st.col = len(ms) - nl_pos
st.line += ms[:nl_pos].count('\n') + 1
return ms
else:
kw.pop('err', None)
return r(self, *args, **kw)
def __repr__(self):
pos = self._states[-1].pos
vars = {}
for st in self._states:
vars.update(st.vars)
return '_Peg(%r, %r)' % (self._s[:pos] + '*' + self._s[pos:], vars)
@staticmethod
def eof(p):
if p._states[-1].pos != len(p._s):
p.error()
def error(self, err=None, expr=None):
st = self._states[-1]
if err is None:
err = 'expected {!r}, found {!r}'.format(expr, self._s[st.pos:st.pos+4])
self._errors[st.pos] = _PegError(err, st.line, st.col)
raise _UnexpectedError(st, expr)
def get(self, key, default=None):
for state in self._states[::-1]:
if key in state.vars:
return state.vars[key][0]
return default
def set(self, key, value):
self._states[-1].vars[key] = value, False
def set_global(self, key, value):
self._states[-1].vars[key] = value, True
def opt(self, *args, **kw):
with self:
return self(*args, **kw)
def not_(self, s, *args, **kw):
with self:
self(s)
self.error()
def __enter__(self):
self._states[-1].committed = False
self._states.append(_PegState(self._states[-1].pos, self._states[-1].line, self._states[-1].col))
def __exit__(self, type, value, traceback):
if type is None:
self.commit()
self._states.pop()
return type == _UnexpectedError
def commit(self):
cur = self._states[-1]
prev = self._states[-2]
for key in cur.vars:
val, g = cur.vars[key]
if not g:
continue
if key in prev.vars:
prev.vars[key] = val, prev.vars[key][1]
else:
prev.vars[key] = val, True
prev.pos = cur.pos
prev.line = cur.line
prev.col = cur.col
prev.committed = True
def __nonzero__(self):
return self._states[-1].committed
__bool__ = __nonzero__

View File

@@ -0,0 +1,191 @@
import re, json, sys
if sys.version_info[0] == 2:
def _is_num(o):
return isinstance(o, int) or isinstance(o, long) or isinstance(o, float)
def _stringify(o):
if isinstance(o, str):
return unicode(o)
if isinstance(o, unicode):
return o
return None
else:
def _is_num(o):
return isinstance(o, int) or isinstance(o, float)
def _stringify(o):
if isinstance(o, bytes):
return o.decode()
if isinstance(o, str):
return o
return None
_id_re = re.compile(r'[$a-zA-Z_][$0-9a-zA-Z_]*\Z')
class CSONEncoder:
def __init__(self, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False,
indent=None, default=None):
self._skipkeys = skipkeys
self._ensure_ascii = ensure_ascii
self._allow_nan = allow_nan
self._sort_keys = sort_keys
self._indent = ' ' * (indent or 4)
self._default = default
if check_circular:
self._obj_stack = set()
else:
self._obj_stack = None
def _format_simple_val(self, o):
if o is None:
return 'null'
if isinstance(o, bool):
return 'true' if o else 'false'
if _is_num(o):
return str(o)
s = _stringify(o)
if s is not None:
return self._escape_string(s)
return None
def _escape_string(self, s):
r = json.dumps(s, ensure_ascii=self._ensure_ascii)
return u"'{}'".format(r[1:-1].replace("'", r"\'"))
def _escape_key(self, s):
if s is None or isinstance(s, bool) or _is_num(s):
s = str(s)
s = _stringify(s)
if s is None:
if self._skipkeys:
return None
raise TypeError('keys must be a string')
if not _id_re.match(s):
return self._escape_string(s)
return s
def _push_obj(self, o):
if self._obj_stack is not None:
if id(o) in self._obj_stack:
raise ValueError('Circular reference detected')
self._obj_stack.add(id(o))
def _pop_obj(self, o):
if self._obj_stack is not None:
self._obj_stack.remove(id(o))
def _encode(self, o, obj_val=False, indent='', force_flow=False):
if isinstance(o, list):
if not o:
if obj_val:
yield ' []\n'
else:
yield indent
yield '[]\n'
else:
if obj_val:
yield ' [\n'
else:
yield indent
yield '[\n'
indent = indent + self._indent
self._push_obj(o)
for v in o:
for chunk in self._encode(v, obj_val=False, indent=indent, force_flow=True):
yield chunk
self._pop_obj(o)
yield indent[:-len(self._indent)]
yield ']\n'
elif isinstance(o, dict):
items = [(self._escape_key(k), v) for k, v in o.items()]
if self._skipkeys:
items = [(k, v) for k, v in items if k is not None]
if self._sort_keys:
items.sort()
if force_flow or not items:
if not items:
if obj_val:
yield ' {}\n'
else:
yield indent
yield '{}\n'
else:
if obj_val:
yield ' {\n'
else:
yield indent
yield '{\n'
indent = indent + self._indent
self._push_obj(o)
for k, v in items:
yield indent
yield k
yield ':'
for chunk in self._encode(v, obj_val=True, indent=indent + self._indent, force_flow=False):
yield chunk
self._pop_obj(o)
yield indent[:-len(self._indent)]
yield '}\n'
else:
if obj_val:
yield '\n'
self._push_obj(o)
for k, v in items:
yield indent
yield k
yield ':'
for chunk in self._encode(v, obj_val=True, indent=indent + self._indent, force_flow=False):
yield chunk
self._pop_obj(o)
else:
v = self._format_simple_val(o)
if v is None:
self._push_obj(o)
v = self.default(o)
for chunk in self._encode(v, obj_val=obj_val, indent=indent, force_flow=force_flow):
yield chunk
self._pop_obj(o)
else:
if obj_val:
yield ' '
else:
yield indent
yield v
yield '\n'
def iterencode(self, o):
return self._encode(o)
def encode(self, o):
return ''.join(self.iterencode(o))
def default(self, o):
if self._default is None:
raise TypeError('Cannot serialize an object of type {}'.format(type(o).__name__))
return self._default(o)
def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None,
indent=None, default=None, sort_keys=False, **kw):
if indent is None and cls is None:
return json.dump(obj, fp, skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular,
allow_nan=allow_nan, default=default, sort_keys=sort_keys, separators=(',', ':'))
if cls is None:
cls = CSONEncoder
encoder = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular,
allow_nan=allow_nan, sort_keys=sort_keys, indent=indent, default=default, **kw)
for chunk in encoder.iterencode(obj):
fp.write(chunk)
def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None,
default=None, sort_keys=False, **kw):
if indent is None and cls is None:
return json.dumps(obj, skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular,
allow_nan=allow_nan, default=default, sort_keys=sort_keys, separators=(',', ':'))
if cls is None:
cls = CSONEncoder
encoder = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular,
allow_nan=allow_nan, sort_keys=sort_keys, indent=indent, default=default, **kw)
return encoder.encode(obj)

View File

@@ -0,0 +1,7 @@
{
"name": "Snippets Completer",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"requests": {}
}

View File

@@ -0,0 +1,49 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
from .provider import Provider
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
self.provider: Provider = None
def _controller_message(self, event: Code_Event_Types.CodeEvent):
...
def load(self):
self.provider = Provider()
event = Event_Factory.create_event(
"register_provider",
provider_name = "Snippets Completer",
provider = self.provider,
language_ids = []
)
self.emit_to("completion", event)
def unload(self):
event = Event_Factory.create_event(
"unregister_provider",
provider_name = "Snippets Completer"
)
self.emit_to("completion", event)
self.provider = None
del self.provider
def run(self):
...

View File

@@ -0,0 +1,68 @@
# Python imports
import re
# Lib imports
import gi
gi.require_version('GtkSource', '4')
from gi.repository import GObject
from gi.repository import GtkSource
# Application imports
from .provider_response_cache import ProviderResponseCache
class Provider(GObject.GObject, GtkSource.CompletionProvider):
"""
This is a Snippits Completion Provider.
# NOTE: used information from here --> https://warroom.rsmus.com/do-that-auto-complete/
"""
__gtype_name__ = 'SnippetsCompletionProvider'
def __init__(self):
super(Provider, self).__init__()
self.response_cache: ProviderResponseCache = ProviderResponseCache()
def do_get_name(self):
return 'Snippits Completion'
def do_match(self, context):
word = self.response_cache.get_word(context)
if not word or len(word) < 2: return False
return True
def do_get_priority(self):
return 2
def do_activate_proposal(self, proposal, iter_):
buffer = iter_.get_buffer()
# Note: Flag mostly intended for SourceViewsMultiInsertState
# to insure marker processes inserted text correctly.
buffer.is_processing_completion = True
return False
def do_get_activation(self):
""" The context for when a provider will show results """
# return GtkSource.CompletionActivation.NONE
return GtkSource.CompletionActivation.USER_REQUESTED
# return GtkSource.CompletionActivation.INTERACTIVE
def do_populate(self, context):
word = self.response_cache.get_word(context)
results = self.response_cache.filter(word)
proposals = []
for entry in results:
proposals.append(
self.response_cache.create_completion_item(
entry["label"],
entry["text"],
entry["info"]
)
)
context.add_proposals(self, proposals, True)

View File

@@ -0,0 +1,68 @@
# Python imports
from os import path
# Lib imports
import gi
gi.require_version('GtkSource', '4')
from gi.repository import GLib
from gi.repository import GtkSource
# Application imports
from libs.event_factory import Code_Event_Types
from core.widgets.code.completion_providers.provider_response_cache_base import ProviderResponseCacheBase
from . import cson
class ProviderResponseCache(ProviderResponseCacheBase):
def __init__(self):
super(ProviderResponseCache, self).__init__()
self.matchers: dict = {}
self.load_snippets()
def load_snippets(self):
fpath = path.join(path.dirname(path.realpath(__file__)), "snippets.cson")
with open(fpath, 'rb') as f:
self.snippets = cson.load(f)
for group in self.snippets:
self.snippets[group]
for entry in self.snippets[group]:
data = self.snippets[group][entry]
self.matchers[ data["prefix"] ] = {
"label": entry,
"text": data["body"],
"info": GLib.markup_escape_text( data["body"] )
}
def process_file_load(self, event: Code_Event_Types.AddedNewFileEvent):
...
def process_file_close(self, event: Code_Event_Types.RemovedFileEvent):
...
def process_file_save(self, event: Code_Event_Types.SavedFileEvent):
...
def process_file_change(self, event: Code_Event_Types.TextChangedEvent):
...
def filter(self, word: str) -> list[dict]:
response: list[dict] = []
for entry in self.matchers:
if not word in entry: continue
data = self.matchers[entry]
response.append(data)
return response
def filter_with_context(self, context: GtkSource.CompletionContext) -> list[dict]:
response: list[dict] = []
return response

View File

@@ -0,0 +1,614 @@
# Your snippets
#
# Atom snippets allow you to enter a simple prefix in the editor and hit tab to
# expand the prefix into a larger code block with templated values.
#
# You can create a new snippet in this file by typing "snip" and then hitting
# tab.
#
# An example CoffeeScript snippet to expand log to console.log:
#
# '.source.coffee':
# 'Console log':
# 'prefix': 'log'
# 'body': 'console.log $1'
#
# Each scope (e.g. '.source.coffee' above) can only be declared once.
#
# This file uses CoffeeScript Object Notation (CSON).
# If you are unfamiliar with CSON, you can read more about it in the
# Atom Flight Manual:
# http://flight-manual.atom.io/using-atom/sections/basic-customization/#_cson
### HTML SNIPPETS ###
'.text.html.basic':
'HTML Template':
'prefix': 'html'
'body': """<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title></title>
<link rel="shortcut icon" href="fave_icon.png">
<link rel="stylesheet" href="resources/css/base.css">
<link rel="stylesheet" href="resources/css/main.css">
</head>
<body>
<script src="resources/js/.js" charset="utf-8"></script>
<script src="resources/js/.js" charset="utf-8"></script>
</body>
</html>
"""
'Canvas Tag':
'prefix': 'canvas'
'body': """<canvas id="canvas" width="800" height="600" style="border:1px solid #c3c3c3;"></canvas>"""
'Img Tag':
'prefix': 'img'
'body': """<img class="" src="" alt="" />"""
'Br Tag':
'prefix': 'br'
'body': """<br/>"""
'Hr Tag':
'prefix': 'hr'
'body': """<hr/>"""
'Server Side Events':
'prefix': 'sse'
'body': """// SSE events if supported
if(typeof(EventSource) !== "undefined") {
let source = new EventSource("resources/php/sse.php");
source.onmessage = (event) => {
if (event.data === "<yourDataStringToLookFor>") {
// code here
}
};
} else {
console.log("SSE Not Supported In Browser...");
}
"""
'AJAX Template Function':
'prefix': 'ajax template'
'body': """const doAjax = async (actionPath, data) => {
let xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState === 4 && this.status === 200) {
if (this.responseText != null) { // this.responseXML if getting XML fata
handleReturnData(JSON.parse(this.responseText));
} else {
console.log("No content returned. Check the file path.");
}
}
};
xhttp.open("POST", actionPath, true);
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
// Force return to be JSON NOTE: Use application/xml to force XML
xhttp.overrideMimeType('application/json');
xhttp.send(data);
}
"""
'CSS Message Colors':
'prefix': 'css colors'
'body': """.error { color: rgb(255, 0, 0); }
.warning { color: rgb(255, 168, 0); }
.success { color: rgb(136, 204, 39); }
"""
### JS SNIPPETS ###
'.source.js':
'Server Side Events':
'prefix': 'sse'
'body': """// SSE events if supported
if(typeof(EventSource) !== "undefined") {
let source = new EventSource("resources/php/sse.php");
source.onmessage = (event) => {
if (event.data === "<yourDataStringToLookFor>") {
// code here
}
};
} else {
console.log("SSE Not Supported In Browser...");
}
"""
'AJAX Template Function':
'prefix': 'ajax template'
'body': """const doAjax = async (actionPath, data) => {
let xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState === 4 && this.status === 200) {
if (this.responseText != null) { // this.responseXML if getting XML fata
handleReturnData(JSON.parse(this.responseText));
} else {
console.log("No content returned. Check the file path.");
}
}
};
xhttp.open("POST", actionPath, true);
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
// Force return to be JSON NOTE: Use application/xml to force XML
xhttp.overrideMimeType('application/json');
xhttp.send(data);
}
"""
'SE6 Function':
'prefix': 'function se6'
'body': """const funcName = (arg = "") => {
}
"""
### CSS SNIPPETS ###
'.source.css':
'CSS Message Colors':
'prefix': 'css colors'
'body': """.error { color: rgb(255, 0, 0); }
.warning { color: rgb(255, 168, 0); }
.success { color: rgb(136, 204, 39); }
"""
### PHP SNIPPETS ###
'.text.html.php':
'SSE PHP':
'prefix': 'sse php'
'body': """<?php
// Start the session
session_start();
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
echo "data:dataToReturn\\\\n\\\\n";
flush();
?>
"""
'PHP Template':
'prefix': 'php'
'body': """<?php
// Start the session
session_start();
// Determin action
chdir("../../"); // Note: If in resources/php/
if (isset($_POST['yourPostID'])) {
// code here
} else {
$message = "Server: [Error] --> Illegal Access Method!";
serverMessage("error", $message);
}
?>
"""
'HTML Template':
'prefix': 'html'
'body': """<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title></title>
<link rel="shortcut icon" href="fave_icon.png">
<link rel="stylesheet" href="resources/css/base.css">
<link rel="stylesheet" href="resources/css/main.css">
</head>
<body>
<script src="resources/js/.js" charset="utf-8"></script>
<script src="resources/js/.js" charset="utf-8"></script>
</body>
</html>
"""
### BASH SNIPPETS ###
'.source.shell':
'Bash or Shell Template':
'prefix': 'bash template'
'body': """#!/bin/bash
# . CONFIG.sh
# set -o xtrace ## To debug scripts
# set -o errexit ## To exit on error
# set -o errunset ## To exit if a variable is referenced but not set
function main() {
cd "$(dirname "$0")"
echo "Working Dir: " $(pwd)
file="$1"
if [ -z "${file}" ]; then
echo "ERROR: No file argument supplied..."
exit
fi
if [[ -f "${file}" ]]; then
echo "SUCCESS: The path and file exists!"
else
echo "ERROR: The path or file '${file}' does NOT exist..."
fi
}
main "$@";
"""
'Bash or Shell Config':
'prefix': 'bash config'
'body': """#!/bin/bash
shopt -s expand_aliases
alias echo="echo -e"
"""
### PYTHON SNIPPETS ###
'.source.python':
'Glade __main__ Class Template':
'prefix': 'glade __main__ class'
'body': """#!/usr/bin/python3
# Python imports
import argparse
import faulthandler
import traceback
import signal
from setproctitle import setproctitle
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import GLib
# Application imports
from app import Application
def run():
try:
setproctitle('<replace this>')
faulthandler.enable() # For better debug info
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, Gtk.main_quit)
parser = argparse.ArgumentParser()
# Add long and short arguments
parser.add_argument("--debug", "-d", default="false", help="Do extra console messaging.")
parser.add_argument("--trace-debug", "-td", default="false", help="Disable saves, ignore IPC lock, do extra console messaging.")
parser.add_argument("--no-plugins", "-np", default="false", help="Do not load plugins.")
parser.add_argument("--new-tab", "-t", default="", help="Open a file into new tab.")
parser.add_argument("--new-window", "-w", default="", help="Open a file into a new window.")
# Read arguments (If any...)
args, unknownargs = parser.parse_known_args()
main = Application(args, unknownargs)
Gtk.main()
except Exception as e:
traceback.print_exc()
quit()
if __name__ == "__main__":
''' Set process title, get arguments, and create GTK main thread. '''
run()
"""
'Glade __main__ Testing Template':
'prefix': 'glade testing class'
'body': """#!/usr/bin/python3
# Python imports
import traceback
import faulthandler
import signal
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import GLib
# Application imports
app_name = "Gtk Quick Test"
class Application(Gtk.ApplicationWindow):
def __init__(self):
super(Application, self).__init__()
self._setup_styling()
self._setup_signals()
self._load_widgets()
self.add(Gtk.Box())
self.show_all()
def _setup_styling(self):
self.set_default_size(1670, 830)
self.set_title(f"{app_name}")
# self.set_icon_from_file( settings.get_window_icon() )
self.set_gravity(5) # 5 = CENTER
self.set_position(1) # 1 = CENTER, 4 = CENTER_ALWAYS
def _setup_signals(self):
self.connect("delete-event", Gtk.main_quit)
def _load_widgets(self):
...
def run():
try:
faulthandler.enable() # For better debug info
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, Gtk.main_quit)
main = Application()
Gtk.main()
except Exception as e:
traceback.print_exc()
quit()
if __name__ == "__main__":
''' Set process title, get arguments, and create GTK main thread. '''
run()
"""
'Glade _init_ Class Template':
'prefix': 'glade __init__ class'
'body': """# Python imports
import inspect
# Lib imports
# Application imports
from utils import Settings
from signal_classes import CrossClassSignals
class Main:
def __init__(self, args):
settings = Settings()
builder = settings.returnBuilder()
# Gets the methods from the classes and sets to handler.
# Then, builder connects to any signals it needs.
classes = [CrossClassSignals(settings)]
handlers = {}
for c in classes:
methods = inspect.getmembers(c, predicate=inspect.ismethod)
handlers.update(methods)
builder.connect_signals(handlers)
window = settings.createWindow()
window.show()
"""
'Class Method':
'prefix': 'def1'
'body': """
def fname(self):
...
"""
'Gtk Class Method':
'prefix': 'def2'
'body': """
def fname(self, widget, eve):
...
"""
'Python Glade Settings Template':
'prefix': 'glade settings class'
'body': """# Python imports
import os
# Lib imports
import gi, cairo
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
from gi.repository import Gtk
from gi.repository import Gdk
# Application imports
class Settings:
def __init__(self):
self.SCRIPT_PTH = os.path.dirname(os.path.realpath(__file__)) + "/"
self.builder = Gtk.Builder()
self.builder.add_from_file(self.SCRIPT_PTH + "../resources/Main_Window.glade")
# 'Filters'
self.office = ('.doc', '.docx', '.xls', '.xlsx', '.xlt', '.xltx', '.xlm',
'.ppt', 'pptx', '.pps', '.ppsx', '.odt', '.rtf')
self.vids = ('.mkv', '.avi', '.flv', '.mov', '.m4v', '.mpg', '.wmv',
'.mpeg', '.mp4', '.webm')
self.txt = ('.txt', '.text', '.sh', '.cfg', '.conf')
self.music = ('.psf', '.mp3', '.ogg' , '.flac')
self.images = ('.png', '.jpg', '.jpeg', '.gif')
self.pdf = ('.pdf')
def createWindow(self):
# Get window and connect signals
window = self.builder.get_object("Main_Window")
window.connect("delete-event", gtk.main_quit)
self.setWindowData(window, False)
return window
def setWindowData(self, window, paintable):
screen = window.get_screen()
visual = screen.get_rgba_visual()
if visual != None and screen.is_composited():
window.set_visual(visual)
# bind css file
cssProvider = gtk.CssProvider()
cssProvider.load_from_path(self.SCRIPT_PTH + '../resources/stylesheet.css')
screen = Gdk.Screen.get_default()
styleContext = Gtk.StyleContext()
styleContext.add_provider_for_screen(screen, cssProvider, gtk.STYLE_PROVIDER_PRIORITY_USER)
window.set_app_paintable(paintable)
if paintable:
window.connect("draw", self.area_draw)
def getMonitorData(self):
screen = self.builder.get_object("Main_Window").get_screen()
monitors = []
for m in range(screen.get_n_monitors()):
monitors.append(screen.get_monitor_geometry(m))
for monitor in monitors:
print(str(monitor.width) + "x" + str(monitor.height) + "+" + str(monitor.x) + "+" + str(monitor.y))
return monitors
def area_draw(self, widget, cr):
cr.set_source_rgba(0, 0, 0, 0.54)
cr.set_operator(cairo.OPERATOR_SOURCE)
cr.paint()
cr.set_operator(cairo.OPERATOR_OVER)
def returnBuilder(self): return self.builder
# Filter returns
def returnOfficeFilter(self): return self.office
def returnVidsFilter(self): return self.vids
def returnTextFilter(self): return self.txt
def returnMusicFilter(self): return self.music
def returnImagesFilter(self): return self.images
def returnPdfFilter(self): return self.pdf
"""
'Python Glade CrossClassSignals Template':
'prefix': 'glade crossClassSignals class'
'body': """# Python imports
import threading
import subprocess
import os
# Lib imports
# Application imports
def threaded(fn):
def wrapper(*args, **kwargs):
threading.Thread(target=fn, args=args, kwargs=kwargs).start()
return wrapper
class CrossClassSignals:
def __init__(self, settings):
self.settings = settings
self.builder = self.settings.returnBuilder()
def getClipboardData(self):
proc = subprocess.Popen(['xclip','-selection', 'clipboard', '-o'], stdout=subprocess.PIPE)
retcode = proc.wait()
data = proc.stdout.read()
return data.decode("utf-8").strip()
def setClipboardData(self, data):
proc = subprocess.Popen(['xclip','-selection','clipboard'], stdin=subprocess.PIPE)
proc.stdin.write(data)
proc.stdin.close()
retcode = proc.wait()
"""
'Python Glade Generic Template':
'prefix': 'glade generic class'
'body': """# Python imports
# Lib imports
# Application imports
class GenericClass:
def __init__(self):
super(GenericClass, self).__init__()
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
def _setup_styling(self):
...
def _setup_signals(self):
...
def _subscribe_to_events(self):
event_system.subscribe("handle_file_from_ipc", self.handle_file_from_ipc)
def _load_widgets(self):
...
"""

View File

@@ -0,0 +1,3 @@
"""
Pligin Module
"""

View File

@@ -0,0 +1,3 @@
"""
Pligin Package
"""

View File

@@ -0,0 +1,68 @@
# Python imports
import re
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('GtkSource', '4')
from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import GtkSource
# Application imports
from .provider_response_cache import ProviderResponseCache
class Provider(GtkSource.CompletionWords):
"""
This is a Words Completion Provider.
# NOTE: used information from here --> https://warroom.rsmus.com/do-that-auto-complete/
"""
__gtype_name__ = 'WordsCompletionProvider'
def __init__(self):
super(Provider, self).__init__()
self.response_cache: ProviderResponseCache = ProviderResponseCache()
def do_get_name(self):
return 'Words Completion'
def do_match(self, context):
# Note: If provider is in interactive activation then need to check
# view focus as otherwise non focus views start trying to grab it.
completion = context.get_property("completion")
if not completion.get_view().has_focus(): return
word = self.response_cache.get_word(context)
if not word or len(word) < 2: return False
iter = self.response_cache.get_iter_correctly(context)
iter.backward_char()
ch = iter.get_char()
# NOTE: Look to re-add or apply supprting logic to use spaces
# As is it slows down the editor in certain contexts...
# if not (ch in ('_', '.', ' ') or ch.isalnum()):
if not (ch in ('_', '.') or ch.isalnum()):
return False
return True
def do_get_priority(self):
return 0
def do_activate_proposal(self, proposal, iter_):
buffer = iter_.get_buffer()
# Note: Flag mostly intended for SourceViewsMultiInsertState
# to insure marker processes inserted text correctly.
buffer.is_processing_completion = True
return False
def do_get_activation(self):
""" The context for when a provider will show results """
# return GtkSource.CompletionActivation.NONE
# return GtkSource.CompletionActivation.USER_REQUESTED
return GtkSource.CompletionActivation.INTERACTIVE

View File

@@ -0,0 +1,29 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Code_Event_Types
from core.widgets.code.completion_providers.provider_response_cache_base import ProviderResponseCacheBase
class ProviderResponseCache(ProviderResponseCacheBase):
def __init__(self):
super(ProviderResponseCache, self).__init__()
self.matchers: dict = {}
def process_file_load(self, event: Code_Event_Types.AddedNewFileEvent):
...
def process_file_close(self, event: Code_Event_Types.RemovedFileEvent):
...
def process_file_save(self, event: Code_Event_Types.SavedFileEvent):
...
def process_file_change(self, event: Code_Event_Types.TextChangedEvent):
...

View File

@@ -0,0 +1,7 @@
{
"name": "Words Completer",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"requests": {}
}

View File

@@ -0,0 +1,49 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
from .provider import Provider
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
self.provider: Provider = None
def _controller_message(self, event: Code_Event_Types.CodeEvent):
...
def load(self):
self.provider = Provider()
event = Event_Factory.create_event(
"register_provider",
provider_name = "Words Completer",
provider = self.provider,
language_ids = []
)
self.emit_to("completion", event)
def unload(self):
event = Event_Factory.create_event(
"unregister_provider",
provider_name = "Words Completer"
)
self.emit_to("completion", event)
self.provider = None
del self.provider
def run(self):
...

View File

@@ -0,0 +1,86 @@
# Python imports
import re
# Lib imports
import gi
gi.require_version('GtkSource', '4')
from gi.repository import GObject
from gi.repository import GtkSource
# Application imports
from .provider_response_cache import ProviderResponseCache
class Provider(GObject.GObject, GtkSource.CompletionProvider):
"""
This is a Words Completion Provider.
# NOTE: used information from here --> https://warroom.rsmus.com/do-that-auto-complete/
"""
__gtype_name__ = 'WordsCompletionProvider'
def __init__(self):
super(Provider, self).__init__()
self.response_cache: ProviderResponseCache = ProviderResponseCache()
def do_get_name(self):
return 'Words Completion'
def do_match(self, context):
# Note: If provider is in interactive activation then need to check
# view focus as otherwise non focus views start trying to grab it.
completion = context.get_property("completion")
if not completion.get_view().has_focus(): return
word = self.response_cache.get_word(context)
if not word or len(word) < 2: return False
iter = self.response_cache.get_iter_correctly(context)
iter.backward_char()
ch = iter.get_char()
# NOTE: Look to re-add or apply supprting logic to use spaces
# As is it slows down the editor in certain contexts...
# if not (ch in ('_', '.', ' ') or ch.isalnum()):
if not (ch in ('_', '.') or ch.isalnum()):
return False
return True
def do_get_priority(self):
return 0
def do_activate_proposal(self, proposal, iter_):
buffer = iter_.get_buffer()
# Note: Flag mostly intended for SourceViewsMultiInsertState
# to insure marker processes inserted text correctly.
buffer.is_processing_completion = True
return False
def do_get_activation(self):
""" The context for when a provider will show results """
# return GtkSource.CompletionActivation.NONE
# return GtkSource.CompletionActivation.USER_REQUESTED
return GtkSource.CompletionActivation.INTERACTIVE
def do_populate(self, context):
word = self.response_cache.get_word(context)
results = self.response_cache.filter_with_context(context)
# results = self.response_cache.filter(word)
# if not results:
# results = self.response_cache.filter_with_context(context)
proposals = []
for entry in results:
proposals.append(
self.response_cache.create_completion_item(
entry["label"],
entry["text"],
entry["info"]
)
)
context.add_proposals(self, proposals, True)

View File

@@ -0,0 +1,136 @@
# Python imports
from concurrent.futures import ThreadPoolExecutor
# Lib imports
import gi
gi.require_version('GtkSource', '4')
from gi.repository import GLib
from gi.repository import GtkSource
# Application imports
from libs.event_factory import Code_Event_Types
from core.widgets.code.completion_providers.provider_response_cache_base import ProviderResponseCacheBase
class ProviderResponseCache(ProviderResponseCacheBase):
def __init__(self):
super(ProviderResponseCache, self).__init__()
self.matchers: dict = {}
self._temp_timeout_id: int = None
def process_file_load(self, event: Code_Event_Types.AddedNewFileEvent):
buffer = event.file.buffer
with ThreadPoolExecutor(max_workers = 1) as executor:
executor.submit(self._handle_change, buffer)
def process_file_close(self, event: Code_Event_Types.RemovedFileEvent):
self.matchers[event.file.buffer] = set()
del self.matchers[event.file.buffer]
def process_file_save(self, event: Code_Event_Types.SavedFileEvent):
...
def process_file_change(self, event: Code_Event_Types.TextChangedEvent):
buffer = event.file.buffer
self._clear_temp_delay()
self._set_temp_delay(buffer)
def _clear_temp_delay(self):
if self._temp_timeout_id:
GLib.source_remove(self._temp_timeout_id)
def _set_temp_delay(self, buffer):
def run_refresh_update(buffer):
with ThreadPoolExecutor(max_workers = 1) as executor:
executor.submit(self._handle_change, buffer)
return False
self._temp_timeout_id = GLib.timeout_add(1500, run_refresh_update, buffer)
def _handle_change(self, buffer):
start_itr = buffer.get_start_iter()
end_itr = buffer.get_end_iter()
data = buffer.get_text(start_itr, end_itr, False)
if not data:
GLib.idle_add(self.load_empty_set, buffer)
return
if not buffer in self.matchers:
GLib.idle_add(self.load_as_new_set, buffer, data)
return
new_words = self.get_all_words(data)
GLib.idle_add(self.load_into_set, buffer, new_words)
def filter(self, word: str) -> list[dict]:
response: list[dict] = []
for entry in self.matchers:
if not word in entry: continue
data = self.matchers[entry]
response.append(data)
return response
def filter_with_context(self, context: GtkSource.CompletionContext) -> list[dict]:
buffer = self.get_iter_correctly(context).get_buffer()
word = self.get_word(context).rstrip()
if not buffer in self.matchers:
self.matchers[buffer] = set()
response: list[dict] = []
for entry in self.matchers[buffer]:
if not entry.rstrip().startswith(word): continue
data = {
"label": entry,
"text": entry,
"info": ""
}
response.append(data)
return response
def load_empty_set(self, buffer):
self.matchers[buffer] = set()
def load_into_set(self, buffer, new_words):
# self.matchers[buffer].update(new_words)
self.matchers[buffer] = new_words
def load_as_new_set(self, buffer, data):
self.matchers[buffer] = self.get_all_words(data)
def get_all_words(self, data: str):
words = set()
def is_word_char(c):
return c.isalnum() or c == '_'
size = len(data)
i = 0
while i < size:
# Skip non-word characters
while i < size and not is_word_char(data[i]):
i += 1
start = i
# Consume word characters
while i < size and is_word_char(data[i]):
i += 1
word = data[start:i]
if not word: continue
words.add(word)
return words

View File

@@ -0,0 +1,3 @@
"""
Plugin Module
"""

View File

@@ -0,0 +1,3 @@
"""
Plugin Package
"""

View File

@@ -0,0 +1,7 @@
{
"name": "Extend Source View Menu",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"requests": {}
}

View File

@@ -0,0 +1,29 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
from .source_view_menu import extend_source_view_menu
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
if isinstance(event, Code_Event_Types.PopulateSourceViewPopupEvent):
extend_source_view_menu(event.buffer, event.menu)
def load(self):
...
def unload(self):
...
def run(self):
...

View File

@@ -0,0 +1,68 @@
# Python imports
import json
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
def on_case_handle(menuitem, buffer, action):
start_itr, \
end_itr = buffer.get_selection_bounds()
data = buffer.get_text(start_itr, end_itr, False)
text = data
if action == "on_all_upper":
text = data.upper()
elif action == "on_all_lower":
text = data.lower()
elif action == "on_invert":
text = data.swapcase()
elif action == "on_title":
text = data.title()
elif action == "on_title_strip":
text = data.title().replace("-", "").replace("_", "").replace(" ", "")
buffer.begin_user_action()
buffer.delete(start_itr, end_itr)
buffer.insert(start_itr, text)
buffer.end_user_action()
def extend_source_view_menu(buffer, menu):
if not buffer.get_selection_bounds(): return
for child in menu.get_children():
if not child.get_label() == "C_hange Case": continue
menu.remove(child)
change_case_item = Gtk.MenuItem(label = "Change Case")
case_menu = Gtk.Menu()
au_case_item = Gtk.MenuItem(label = "All Upper Case")
al_case_item = Gtk.MenuItem(label = "All Lower Case")
inver_case_item = Gtk.MenuItem(label = "Invert Case")
title_case_item = Gtk.MenuItem(label = "Title Case")
title_strip_case_item = Gtk.MenuItem(label = "Title Strip Case")
au_case_item.connect("activate", on_case_handle, buffer, "on_all_upper")
al_case_item.connect("activate", on_case_handle, buffer, "on_all_lower")
inver_case_item.connect("activate", on_case_handle, buffer, "on_invert")
title_case_item.connect("activate", on_case_handle, buffer, "on_title")
title_strip_case_item.connect("activate", on_case_handle, buffer, "on_title_strip")
case_menu.append(au_case_item)
case_menu.append(al_case_item)
case_menu.append(inver_case_item)
case_menu.append(title_case_item)
case_menu.append(title_strip_case_item)
change_case_item.set_submenu(case_menu)
menu.append(change_case_item)

View File

@@ -0,0 +1,3 @@
"""
Pligin Module
"""

View File

@@ -0,0 +1,3 @@
"""
Pligin Package
"""

View File

@@ -0,0 +1,7 @@
{
"name": "File State Watcher",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"requests": {}
}

View File

@@ -0,0 +1,43 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
from .watcher_checks import *
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
if not isinstance(event, Code_Event_Types.FocusedViewEvent): return
event = Event_Factory.create_event(
"get_file", buffer = event.view.get_buffer()
)
self.emit_to("files", event)
file = event.response
if file.ftype == "buffer": return
file.check_file_on_disk()
if file.is_deleted():
file_is_deleted(file, self.emit)
elif file.is_externally_modified():
file_is_externally_modified(file, self.emit)
def load(self):
...
def unload(self):
...
def run(self):
...

View File

@@ -0,0 +1,54 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
def ask_yes_no(message):
dialog = Gtk.MessageDialog(
parent = None,
flags = 0,
message_type = Gtk.MessageType.QUESTION,
buttons = Gtk.ButtonsType.YES_NO,
text = message,
)
dialog.set_title("Confirm")
response = dialog.run()
dialog.destroy()
return response == Gtk.ResponseType.YES
def file_is_deleted(file, emit):
file.was_deleted = True
event = Event_Factory.create_event(
"file_externally_deleted",
file = file,
buffer = file.buffer
)
emit(event)
def file_is_externally_modified(file, emit):
event = Event_Factory.create_event(
"file_externally_modified",
file = file,
buffer = file.buffer
)
emit(event)
if not file.buffer.get_modified():
file.reload()
return
result = ask_yes_no("File has been externally modified. Reload?")
if not result: return
file.reload()

View File

@@ -0,0 +1,3 @@
"""
Pligin Module
"""

View File

@@ -0,0 +1,3 @@
"""
Pligin Package
"""

View File

@@ -0,0 +1,7 @@
{
"name": "Prettify JSON",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"requests": {}
}

View File

@@ -0,0 +1,39 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
from .prettify_json import add_prettify_json
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
if isinstance(event, Code_Event_Types.PopulateSourceViewPopupEvent):
language = event.buffer.get_language()
if not language: return
if "json" == language.get_id():
add_prettify_json(event.buffer, event.menu)
def load(self):
...
def unload(self):
...
def run(self):
...

View File

@@ -0,0 +1,29 @@
# Python imports
import json
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
def add_prettify_json(buffer, menu):
def on_prettify_json(menuitem, buffer):
start_itr, \
end_itr = buffer.get_start_iter(), buffer.get_end_iter()
data = buffer.get_text(start_itr, end_itr, False)
text = json.dumps(json.loads(data), separators = (',', ':'), indent = 4)
buffer.begin_user_action()
buffer.delete(start_itr, end_itr)
buffer.insert(start_itr, text)
buffer.end_user_action()
item = Gtk.MenuItem(label = "Prettify JSON")
item.connect("activate", on_prettify_json, buffer)
menu.append(item)

View File

@@ -0,0 +1,3 @@
"""
Pligin Module
"""

View File

@@ -0,0 +1,3 @@
"""
Pligin Package
"""

View File

@@ -0,0 +1,214 @@
{
"info": "https://download.eclipse.org/jdtls/",
"info-init-options": "https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line",
"info-import-build": "https://www.javahotchocolate.com/tutorials/build-path.html",
"info-external-class-paths": "https://github.com/eclipse-jdtls/eclipse.jdt.ls/issues/3291",
"link": "https://download.eclipse.org/jdtls/milestones/?d",
"command": "lsp-ws-proxy --listen 4114 -- jdtls",
"alt-command": "lsp-ws-proxy -- jdtls",
"alt-command2": "java-language-server",
"socket": "ws://127.0.0.1:9999/java",
"socket-two": "ws://127.0.0.1:9999/?name=jdtls",
"alt-socket": "ws://127.0.0.1:9999/?name=java-language-server",
"initialization-options": {
"bundles": [
"intellicode-core.jar"
],
"workspaceFolders": [
"file://{workspace.folder}"
],
"extendedClientCapabilities": {
"classFileContentsSupport": true,
"executeClientCommandSupport": false
},
"settings": {
"java": {
"autobuild": {
"enabled": true
},
"jdt": {
"ls": {
"javac": {
"enabled": true
},
"java": {
"home": "{user.home}/Portable_Apps/sdks/javasdk/jdk-22.0.2"
},
"lombokSupport": {
"enabled": true
},
"protobufSupport":{
"enabled": true
},
"androidSupport": {
"enabled": true
}
}
},
"configuration": {
"updateBuildConfiguration": "automatic",
"maven": {
"userSettings": "{user.home}/.config/jdtls/settings.xml",
"globalSettings": "{user.home}/.config/jdtls/settings.xml"
},
"runtimes": [
{
"name": "JavaSE-17",
"path": "/usr/lib/jvm/java-17-openjdk",
"javadoc": "https://docs.oracle.com/en/java/javase/17/docs/api/",
"default": false
},
{
"name": "JavaSE-22",
"path": "{user.home}/Portable_Apps/sdks/javasdk/jdk-22.0.2",
"javadoc": "https://docs.oracle.com/en/java/javase/22/docs/api/",
"default": true
}
]
},
"classPath": [
"{user.home}/.config/jdtls/m2/repository/**/*-sources.jar",
"lib/**/*-sources.jar"
],
"docPath": [
"{user.home}/.config/jdtls/m2/repository/**/*-javadoc.jar",
"lib/**/*-javadoc.jar"
],
"project": {
"encoding": "ignore",
"outputPath": "bin",
"referencedLibraries": [
"{user.home}/.config/jdtls/m2/repository/**/*.jar",
"lib/**/*.jar"
],
"importOnFirstTimeStartup": "automatic",
"importHint": true,
"resourceFilters": [
"node_modules",
"\\.git"
],
"sourcePaths": [
"src",
"{user.home}/.config/jdtls/m2/repository/**/*.jar"
]
},
"sources": {
"organizeImports": {
"starThreshold": 99,
"staticStarThreshold": 99
}
},
"imports": {
"gradle": {
"wrapper": {
"checksums": []
}
}
},
"import": {
"maven": {
"enabled": true,
"offline": {
"enabled": false
},
"disableTestClasspathFlag": false
},
"gradle": {
"enabled": false,
"wrapper": {
"enabled": true
},
"version": "",
"home": "abs(static/gradle-7.3.3)",
"java": {
"home": "abs(static/launch_jres/17.0.6-linux-x86_64)"
},
"offline": {
"enabled": false
},
"arguments": [],
"jvmArguments": [],
"user": {
"home": ""
},
"annotationProcessing": {
"enabled": true
}
},
"exclusions": [
"**/node_modules/**",
"**/.metadata/**",
"**/archetype-resources/**",
"**/META-INF/maven/**"
],
"generatesMetadataFilesAtProjectRoot": false
},
"maven": {
"downloadSources": true,
"updateSnapshots": true
},
"silentNotification": true,
"contentProvider": {
"preferred": "fernflower"
},
"signatureHelp": {
"enabled": true,
"description": {
"enabled": true
}
},
"completion": {
"enabled": true,
"engine": "ecj",
"matchCase": "firstletter",
"maxResults": 25,
"guessMethodArguments": true,
"lazyResolveTextEdit": {
"enabled": true
},
"postfix": {
"enabled": true
},
"favoriteStaticMembers": [
"org.junit.Assert.*",
"org.junit.Assume.*",
"org.junit.jupiter.api.Assertions.*",
"org.junit.jupiter.api.Assumptions.*",
"org.junit.jupiter.api.DynamicContainer.*",
"org.junit.jupiter.api.DynamicTest.*"
],
"importOrder": [
"#",
"java",
"javax",
"org",
"com"
]
},
"references": {
"includeAccessors": true,
"includeDecompiledSources": true
},
"codeGeneration": {
"toString": {
"template": "${object.className}{${member.name()}=${member.value}, ${otherMembers}}"
},
"insertionLocation": "afterCursor",
"useBlocks": true
},
"implementationsCodeLens": {
"enabled": true
},
"referencesCodeLens": {
"enabled": true
},
"progressReports": {
"enabled": false
},
"saveActions": {
"organizeImports": true
}
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"name": "Java LSP Client",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"autoload": false,
"requests": {}
}

View File

@@ -0,0 +1,43 @@
# Python imports
from os import path
# Lib imports
import gi
from gi.repository import GLib
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
from .response_handler import JavaHandler
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
...
def load(self):
dirPth = path.dirname( path.realpath(__file__) )
with open(f"{dirPth}/config/lsp-server-config.json", "r") as f:
config = f.read()
event = Event_Factory.create_event("register_lsp_client",
lang_id = "java",
lang_config = config,
handler = JavaHandler
)
self.emit_to("lsp_manager", event)
def unload(self):
event = Event_Factory.create_event("unregister_lsp_client",
lang_id = "java"
)
self.emit_to("lsp_manager", event)
def run(self):
...

View File

@@ -0,0 +1 @@
from .java import JavaHandler

View File

@@ -0,0 +1,51 @@
# Python imports
# Lib imports
import gi
gi.require_version('GtkSource', '4')
from gi.repository import GtkSource
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from lsp_manager.response_handlers.default import DefaultHandler
class JavaHandler(DefaultHandler):
"""Java-specific: overrides definition, handles classFileContents."""
def handle(self, method: str, response, controller):
match method:
case "textDocument/definition":
self._handle_definition(response, controller)
case "java/classFileContents":
self._handle_class_file_contents(response)
case _:
super().handle(method, response, controller)
def _handle_definition(self, response, controller):
if not response: return
uri = response[0]["uri"]
if "jdt://" in uri:
controller._lsp_java_class_file_contents(uri)
return
self._prompt_goto_request(uri, response[0]["range"])
def _handle_class_file_contents(self, text: str):
event = Event_Factory.create_event("get_active_view")
self.emit_to("source_views", event)
view = event.response
file = view.command.exec("new_file")
buffer = view.get_buffer()
itr = buffer.get_iter_at_mark(buffer.get_insert())
lm = GtkSource.LanguageManager.get_default()
language = lm.get_language("java")
file.ftype = "java"
buffer.set_language(language)
buffer.insert(itr, text, -1)

View File

@@ -0,0 +1,3 @@
"""
Pligin Module
"""

View File

@@ -0,0 +1,3 @@
"""
Pligin Package
"""

View File

@@ -0,0 +1,3 @@
"""
LSP Clients Module
"""

View File

@@ -0,0 +1,68 @@
# Python imports
import threading
from os import path
import json
# Lib imports
import gi
from gi.repository import GLib
# Application imports
from ..dto.code.lsp.lsp_messages import get_message_str
from ..dto.code.lsp.lsp_message_structs import \
LSPResponseTypes, ClientRequest, ClientNotification
from .lsp_client_websocket import LSPClientWebsocket
class LSPClient(LSPClientWebsocket):
def __init__(self):
super(LSPClient, self).__init__()
# https://github.com/microsoft/multilspy/tree/main/src/multilspy/language_servers
# initialize-params-slim.json was created off of jedi_language_server one
# self._init_params = settings_manager.get_lsp_init_data()
self._language: str = ""
self._init_params: dict = {}
self._event_history: dict[int, str] = {}
try:
_USER_HOME = path.expanduser('~')
_SCRIPT_PTH = path.dirname( path.realpath(__file__) )
_LSP_INIT_CONFIG = f"{_SCRIPT_PTH}/../configs/initialize-params-slim.json"
with open(_LSP_INIT_CONFIG) as file:
data = file.read().replace("{user.home}", _USER_HOME)
self._init_params = json.loads(data)
except Exception as e:
logger.error( f"LSP Controller: {_LSP_INIT_CONFIG}\n\t\t{repr(e)}" )
self._message_id: int = -1
self._socket = None
self.read_lock = threading.Lock()
self.write_lock = threading.Lock()
def set_language(self, language: str):
self._language = language
def set_socket(self, socket: str):
self._socket = socket
def unset_socket(self):
self._socket = None
def send_notification(self, method: str, params: dict = {}):
self._send_message( ClientNotification(method, params) )
def send_request(self, method: str, params: dict = {}):
self._message_id += 1
self._event_history[self._message_id] = method
self._send_message( ClientRequest(self._message_id, method, params) )
def get_event_by_id(self, message_id: int) -> str:
if not message_id in self._event_history: return
return self._event_history[message_id]
def handle_lsp_response(self, lsp_response: LSPResponseTypes):
raise NotImplementedError

View File

@@ -0,0 +1,20 @@
# Python imports
# Lib imports
# Application imports
from ..dto.code.lsp.lsp_message_structs import ClientRequest, ClientNotification
from .lsp_client_events import LSPClientEvents
class LSPClientBase(LSPClientEvents):
def _send_message(self, data: ClientRequest or ClientNotification):
raise NotImplementedError
def start_client(self):
raise NotImplementedError
def stop_client(self):
raise NotImplementedError

View File

@@ -0,0 +1,128 @@
# Python imports
import os
# Lib imports
# Application imports
from ..dto.code.lsp.lsp_messages import get_message_obj
from ..dto.code.lsp.lsp_messages import didopen_notification
from ..dto.code.lsp.lsp_messages import didsave_notification
from ..dto.code.lsp.lsp_messages import didclose_notification
from ..dto.code.lsp.lsp_messages import didchange_notification
from ..dto.code.lsp.lsp_messages import completion_request
from ..dto.code.lsp.lsp_messages import definition_request
from ..dto.code.lsp.lsp_messages import references_request
from ..dto.code.lsp.lsp_messages import symbols_request
class LSPClientEvents:
def send_initialize_message(self, init_ops: dict, workspace_file: str, workspace_uri: str):
folder_name = os.path.basename(workspace_file)
self._init_params["processId"] = None
self._init_params["rootPath"] = workspace_file
self._init_params["rootUri"] = workspace_uri
self._init_params["workspaceFolders"] = [
{
"name": folder_name,
"uri": workspace_uri
}
]
self._init_params["initializationOptions"] = init_ops
self.send_request("initialize", self._init_params)
def send_initialized_message(self):
self.send_notification("initialized")
def _lsp_did_open(self, data: dict):
method = "textDocument/didOpen"
params = didopen_notification["params"]
params["textDocument"]["uri"] = data["uri"]
params["textDocument"]["languageId"] = data["language_id"]
params["textDocument"]["text"] = data["text"]
self.send_notification( method, params )
def _lsp_did_save(self, data: dict):
method = "textDocument/didSave"
params = didsave_notification["params"]
params["textDocument"]["uri"] = data["uri"]
params["text"] = data["text"]
self.send_notification( method, params )
def _lsp_did_close(self, data: dict):
method = "textDocument/didClose"
params = didclose_notification["params"]
params["textDocument"]["uri"] = data["uri"]
self.send_notification( method, params )
def _lsp_did_change(self, data: dict):
method = "textDocument/didChange"
params = didchange_notification["params"]
params["textDocument"]["uri"] = data["uri"]
params["textDocument"]["languageId"] = data["language_id"]
params["textDocument"]["version"] = data["version"]
contentChanges = params["contentChanges"][0]
contentChanges["text"] = data["text"]
self.send_notification( method, params )
# def _lsp_did_change(self, data: dict):
# method = "textDocument/didChange"
# params = didchange_notification_range["params"]
# params["textDocument"]["uri"] = data["uri"]
# params["textDocument"]["languageId"] = data["language_id"]
# params["textDocument"]["version"] = data["version"]
# contentChanges = params["contentChanges"][0]
# start = contentChanges["range"]["start"]
# end = contentChanges["range"]["end"]
# contentChanges["text"] = data["text"]
# start["line"] = data["line"]
# start["character"] = 0
# end["line"] = data["line"]
# end["character"] = data["column"]
# self.send_notification( method, params )
def _lsp_definition(self, data: dict):
method = "textDocument/definition"
params = definition_request["params"]
params["textDocument"]["uri"] = data["uri"]
params["textDocument"]["languageId"] = data["language_id"]
params["textDocument"]["version"] = data["version"]
params["position"]["line"] = data["line"]
params["position"]["character"] = data["column"]
self.send_request( method, params )
def _lsp_completion(self, data: dict):
method = "textDocument/completion"
params = completion_request["params"]
params["textDocument"]["uri"] = data["uri"]
params["textDocument"]["languageId"] = data["language_id"]
params["textDocument"]["version"] = data["version"]
params["position"]["line"] = data["line"]
params["position"]["character"] = data["column"]
self.send_request( method, params )
def _lsp_java_class_file_contents(self, uri: str):
method = "java/classFileContents"
params = {
"uri": uri
}
self.send_request( method, params )

View File

@@ -0,0 +1,56 @@
# Python imports
# Lib imports
from gi.repository import GLib
# Application imports
# from libs import websockets
from ..dto.code.lsp.lsp_messages import get_message_str, get_message_obj
from ..dto.code.lsp.lsp_message_structs import \
LSPResponseTypes, ClientRequest, ClientNotification, \
LSPResponseRequest, LSPResponseNotification, LSPIDResponseNotification
from .lsp_client_base import LSPClientBase
from .websocket_client import WebsocketClient
class LSPClientWebsocket(LSPClientBase):
def _send_message(self, data: ClientRequest | ClientNotification):
if not data: return
message_str = get_message_str(data)
message_size = len(message_str)
message = f"Content-Length: {message_size}\r\n\r\n{message_str}"
logger.debug(f"Client: {message_str}")
self.ws_client.send(message_str)
def start_client(self):
self.ws_client = WebsocketClient()
self.ws_client.set_socket(self._socket)
self.ws_client.set_callback(self._monitor_lsp_response)
self.ws_client.start_client()
return self.ws_client
def stop_client(self):
if not hasattr(self, "ws_client"): return
self.ws_client.close_client()
def _monitor_lsp_response(self, data: dict | None):
if not data: return
message = get_message_obj(data)
keys = message.keys()
lsp_response = None
if "result" in keys:
lsp_response = LSPResponseRequest(**get_message_obj(data))
if "method" in keys:
lsp_response = LSPResponseNotification(**get_message_obj(data)) if not "id" in keys else LSPIDResponseNotification( **get_message_obj(data) )
if not lsp_response: return
GLib.idle_add(self.handle_lsp_response, lsp_response)

View File

@@ -0,0 +1,62 @@
# Python imports
import json
import threading
# Lib imports
# Application imports
from ..libs import websocket
class WebsocketClient:
def __init__(self):
self.ws = None
self._socket = None
self._connected = threading.Event()
def set_socket(self, socket: str):
self._socket = socket
def unset_socket(self):
self._socket = None
def send(self, message: str):
self.ws.send(message)
def on_message(self, ws, message: dict):
self.respond(message)
def on_error(self, ws, error: str):
logger.debug(f"WS Error: {error}")
def on_close(self, ws, close_status_code: int, close_msg: str):
logger.debug("WS Closed...")
def on_open(self, ws):
self._connected.set()
logger.debug("WS opened connection...")
def wait_for_connection(self, timeout: float = 5.0) -> bool:
return self._connected.wait(timeout)
def set_callback(self, callback: object):
self.respond = callback
def close_client(self):
self.ws.close()
@daemon_threaded
def start_client(self):
if not self._socket:
raise Exception("Socket address isn't set so cannot start WebsocketClient listener...")
# websocket.enableTrace(True)
self.ws = websocket.WebSocketApp(self._socket,
on_open = self.on_open,
on_message = self.on_message,
on_error = self.on_error,
on_close = self.on_close)
self.ws.run_forever(reconnect = 0.5)

View File

@@ -0,0 +1,151 @@
{
"_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize",
"processId": "os.getpid()",
"clientInfo": {
"name": "LSP Manager",
"version": "0.0.1"
},
"locale": "en",
"rootPath": "repository_absolute_path",
"rootUri": "pathlib.Path(repository_absolute_path).as_uri()",
"capabilities": {
"textDocument": {
"completion": {
"dynamicRegistration": true,
"contextSupport": true,
"completionItem": {
"snippetSupport": false,
"commitCharactersSupport": true,
"documentationFormat": [
"markdown",
"plaintext"
],
"deprecatedSupport": true,
"preselectSupport": true,
"tagSupport": {
"valueSet": [
1
]
},
"insertReplaceSupport": false,
"resolveSupport": {
"properties": [
"documentation",
"detail",
"additionalTextEdits"
]
},
"insertTextModeSupport": {
"valueSet": [
1,
2
]
},
"labelDetailsSupport": true
},
"insertTextMode": 2,
"completionItemKind": {
"valueSet": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25
]
},
"completionList": {
"itemDefaults": [
"commitCharacters",
"editRange",
"insertTextFormat",
"insertTextMode"
]
}
},
"hover": {
"dynamicRegistration": true,
"contentFormat": [
"markdown",
"plaintext"
]
},
"signatureHelp": {
"dynamicRegistration": true,
"signatureInformation": {
"documentationFormat": [
"markdown",
"plaintext"
],
"parameterInformation": {
"labelOffsetSupport": true
},
"activeParameterSupport": true
},
"contextSupport": true
},
"definition": {
"dynamicRegistration": true,
"linkSupport": true
},
"references": {
"dynamicRegistration": true
},
"typeDefinition": {
"dynamicRegistration": true,
"linkSupport": true
},
"implementation": {
"dynamicRegistration": true,
"linkSupport": true
},
"colorProvider": {
"dynamicRegistration": true
},
"declaration": {
"dynamicRegistration": true,
"linkSupport": true
},
"callHierarchy": {
"dynamicRegistration": true
},
"inlayHint": {
"dynamicRegistration": true,
"resolveSupport": {
"properties": [
"tooltip",
"textEdits",
"label.tooltip",
"label.location",
"label.command"
]
}
},
"diagnostic": {
"dynamicRegistration": true,
"relatedDocumentSupport": false
}
}
},
"trace": "verbose",
"workspaceFolders": "[\n {\n \"uri\": pathlib.Path(repository_absolute_path).as_uri(),\n \"name\": os.path.basename(repository_absolute_path),\n }\n ]"
}

View File

@@ -0,0 +1,365 @@
{
"java": {
"info": "https://download.eclipse.org/jdtls/",
"info-init-options": "https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line",
"info-import-build": "https://www.javahotchocolate.com/tutorials/build-path.html",
"info-external-class-paths": "https://github.com/eclipse-jdtls/eclipse.jdt.ls/issues/3291",
"link": "https://download.eclipse.org/jdtls/milestones/?d",
"command": "lsp-ws-proxy --listen 4114 -- jdtls",
"alt-command": "lsp-ws-proxy -- jdtls",
"alt-command2": "java-language-server",
"socket": "ws://127.0.0.1:9999/java",
"socket-two": "ws://127.0.0.1:9999/?name=jdtls",
"alt-socket": "ws://127.0.0.1:9999/?name=java-language-server",
"initialization-options": {
"bundles": [
"intellicode-core.jar"
],
"workspaceFolders": [
"file://{workspace.folder}"
],
"extendedClientCapabilities": {
"classFileContentsSupport": true,
"executeClientCommandSupport": false
},
"settings": {
"java": {
"autobuild": {
"enabled": true
},
"jdt": {
"ls": {
"javac": {
"enabled": true
},
"java": {
"home": "{user.home}/Portable_Apps/sdks/javasdk/jdk-22.0.2"
},
"lombokSupport": {
"enabled": true
},
"protobufSupport":{
"enabled": true
},
"androidSupport": {
"enabled": true
}
}
},
"configuration": {
"updateBuildConfiguration": "automatic",
"maven": {
"userSettings": "{user.home}/.config/jdtls/settings.xml",
"globalSettings": "{user.home}/.config/jdtls/settings.xml"
},
"runtimes": [
{
"name": "JavaSE-17",
"path": "/usr/lib/jvm/java-17-openjdk",
"javadoc": "https://docs.oracle.com/en/java/javase/17/docs/api/",
"default": false
},
{
"name": "JavaSE-22",
"path": "{user.home}/Portable_Apps/sdks/javasdk/jdk-22.0.2",
"javadoc": "https://docs.oracle.com/en/java/javase/22/docs/api/",
"default": true
}
]
},
"classPath": [
"{user.home}/.config/jdtls/m2/repository/**/*-sources.jar",
"lib/**/*-sources.jar"
],
"docPath": [
"{user.home}/.config/jdtls/m2/repository/**/*-javadoc.jar",
"lib/**/*-javadoc.jar"
],
"project": {
"encoding": "ignore",
"outputPath": "bin",
"referencedLibraries": [
"{user.home}/.config/jdtls/m2/repository/**/*.jar",
"lib/**/*.jar"
],
"importOnFirstTimeStartup": "automatic",
"importHint": true,
"resourceFilters": [
"node_modules",
"\\.git"
],
"sourcePaths": [
"src",
"{user.home}/.config/jdtls/m2/repository/**/*.jar"
]
},
"sources": {
"organizeImports": {
"starThreshold": 99,
"staticStarThreshold": 99
}
},
"imports": {
"gradle": {
"wrapper": {
"checksums": []
}
}
},
"import": {
"maven": {
"enabled": true,
"offline": {
"enabled": false
},
"disableTestClasspathFlag": false
},
"gradle": {
"enabled": false,
"wrapper": {
"enabled": true
},
"version": "",
"home": "abs(static/gradle-7.3.3)",
"java": {
"home": "abs(static/launch_jres/17.0.6-linux-x86_64)"
},
"offline": {
"enabled": false
},
"arguments": [],
"jvmArguments": [],
"user": {
"home": ""
},
"annotationProcessing": {
"enabled": true
}
},
"exclusions": [
"**/node_modules/**",
"**/.metadata/**",
"**/archetype-resources/**",
"**/META-INF/maven/**"
],
"generatesMetadataFilesAtProjectRoot": false
},
"maven": {
"downloadSources": true,
"updateSnapshots": true
},
"silentNotification": true,
"contentProvider": {
"preferred": "fernflower"
},
"signatureHelp": {
"enabled": true,
"description": {
"enabled": true
}
},
"completion": {
"enabled": true,
"engine": "ecj",
"matchCase": "firstletter",
"maxResults": 25,
"guessMethodArguments": true,
"lazyResolveTextEdit": {
"enabled": true
},
"postfix": {
"enabled": true
},
"favoriteStaticMembers": [
"org.junit.Assert.*",
"org.junit.Assume.*",
"org.junit.jupiter.api.Assertions.*",
"org.junit.jupiter.api.Assumptions.*",
"org.junit.jupiter.api.DynamicContainer.*",
"org.junit.jupiter.api.DynamicTest.*"
],
"importOrder": [
"#",
"java",
"javax",
"org",
"com"
]
},
"references": {
"includeAccessors": true,
"includeDecompiledSources": true
},
"codeGeneration": {
"toString": {
"template": "${object.className}{${member.name()}=${member.value}, ${otherMembers}}"
},
"insertionLocation": "afterCursor",
"useBlocks": true
},
"implementationsCodeLens": {
"enabled": true
},
"referencesCodeLens": {
"enabled": true
},
"progressReports": {
"enabled": false
},
"saveActions": {
"organizeImports": true
}
}
}
}
},
"python": {
"info": "https://github.com/python-lsp/python-lsp-server",
"command": "lsp-ws-proxy -- pylsp",
"alt-command": "pylsp",
"alt-command2": "lsp-ws-proxy --listen 4114 -- pylsp",
"alt-command3": "pylsp --ws --port 4114",
"socket": "ws://127.0.0.1:9999/python",
"socket-two": "ws://127.0.0.1:9999/?name=pylsp",
"initialization-options": {
"pylsp": {
"rope": {
"ropeFolder": "{user.home}/.config/newton/lsps/ropeproject"
},
"plugins": {
"ruff": {
"enabled": true,
"extendSelect": ["I"],
"lineLength": 80
},
"pycodestyle": {
"enabled": false
},
"pyflakes": {
"enabled": false
},
"pylint": {
"enabled": true
},
"mccabe": {
"enabled": false
},
"pylsp_rope": {
"rename": false
},
"rope_rename": {
"enabled": false
},
"rope_autoimport": {
"enabled": true
},
"rope_completion": {
"enabled": false,
"eager": false
},
"jedi_rename": {
"enabled": true
},
"jedi_completion": {
"enabled": true,
"include_class_objects": true,
"include_function_objects": true,
"fuzzy": false
},
"jedi": {
"root_dir": "file://{workspace.folder}",
"extra_paths": [
"{user.home}/Portable_Apps/py-venvs/pylsp-venv/venv/lib/python3.10/site-packages"
]
}
}
}
}
},
"python - jedi-language-server": {
"hidden": true,
"info": "https://pypi.org/project/jedi-language-server/",
"command": "jedi-language-server",
"alt-command": "lsp-ws-proxy --listen 3030 -- jedi-language-server",
"socket": "ws://127.0.0.1:9999/python",
"socket-two": "ws://127.0.0.1:9999/?name=jedi-language-server",
"initialization-options": {
"jediSettings": {
"autoImportModules": [],
"caseInsensitiveCompletion": true,
"debug": false
},
"completion": {
"disableSnippets": false,
"resolveEagerly": false,
"ignorePatterns": []
},
"markupKindPreferred": "markdown",
"workspace": {
"extraPaths": [
"{user.home}/Portable_Apps/py-venvs/pylsp-venv/venv/lib/python3.10/site-packages"
],
"environmentPath": "{user.home}/Portable_Apps/py-venvs/gtk-apps-venv/venv/bin/python",
"symbols": {
"ignoreFolders": [
".nox",
".tox",
".venv",
"__pycache__",
"venv"
],
"maxSymbols": 20
}
}
}
},
"cpp": {
"info": "https://clangd.llvm.org/",
"command": "lsp-ws-proxy -- clangd",
"alt-command": "clangd",
"socket": "ws://127.0.0.1:9999/cpp",
"socket-two": "ws://127.0.0.1:9999/?name=clangd",
"initialization-options": {}
},
"c": {
"hidden": true,
"info": "https://clangd.llvm.org/",
"command": "lsp-ws-proxy -- clangd",
"alt-command": "clangd",
"socket": "ws://127.0.0.1:9999/c",
"socket-two": "ws://127.0.0.1:9999/?name=clangd",
"initialization-options": {}
},
"go": {
"info": "https://pkg.go.dev/golang.org/x/tools/gopls#section-readme",
"command": "lsp-ws-proxy -- gopls",
"alt-command": "gopls",
"socket": "ws://127.0.0.1:9999/go",
"socket-two": "ws://127.0.0.1:9999/?name=gopls",
"initialization-options": {}
},
"typescript": {
"info": "https://github.com/typescript-language-server/typescript-language-server",
"command": "lsp-ws-proxy -- typescript-language-server",
"alt-command": "typescript-language-server --stdio",
"socket": "ws://127.0.0.1:9999/typescript",
"socket-two": "ws://127.0.0.1:9999/?name=ts",
"initialization-options": {}
},
"sh": {
"info": "",
"command": "",
"alt-command": "",
"socket": "ws://127.0.0.1:9999/bash",
"socket-two": "ws://127.0.0.1:9999/?name=shell",
"initialization-options": {}
},
"lua": {
"info": "https://github.com/LuaLS/lua-language-server",
"command": "lsp-ws-proxy -- lua-language-server",
"alt-command": "lua-language-server",
"socket": "ws://127.0.0.1:9999/lua",
"socket-two": "ws://127.0.0.1:9999/?name=lua",
"initialization-options": {}
}
}

View File

@@ -0,0 +1,8 @@
"""
Libs Code DTO(s) Events Package
"""
from .lsp_event import LspEvent
from .register_lsp_client_event import RegisterLspClientEvent
from .unregister_lsp_client_event import UnregisterLspClientEvent

View File

@@ -0,0 +1,13 @@
# Python imports
from dataclasses import dataclass, field
# Lib imports
# Application imports
from libs.dto.code.events import CodeEvent
@dataclass
class LspEvent(CodeEvent):
...

View File

@@ -0,0 +1,17 @@
# Python imports
from dataclasses import dataclass, field
# Lib imports
# Application imports
from ....response_handlers.base_handler import BaseHandler
from .lsp_event import LspEvent
@dataclass
class RegisterLspClientEvent(LspEvent):
lang_id: str = ""
lang_config: str = "{}"
handler: BaseHandler = None

View File

@@ -0,0 +1,13 @@
# Python imports
from dataclasses import dataclass, field
# Lib imports
# Application imports
from .lsp_event import LspEvent
@dataclass
class UnregisterLspClientEvent(LspEvent):
lang_id: str = ""

View File

@@ -0,0 +1,95 @@
# Python imports
import json
import enum
from dataclasses import dataclass
# Lib imports
# Application imports
from .lsp_structs import TextDocumentItem
class MessageEncoder(json.JSONEncoder):
"""
Encodes an object in JSON
"""
def default(self, o): # pylint: disable=E0202
return o.__dict__
@dataclass
class ClientRequest(object):
def __init__(self, id: int, method: str, params: dict):
"""
Constructs a new Client Request instance.
:param int id: Message id to track instance.
:param str method: The type of lsp request being made.
:param dict params: The arguments of the given method.
"""
self.jsonrpc = "2.0"
self.id = id
self.method = method
self.params = params
@dataclass
class ClientNotification(object):
def __init__(self, method: str, params: dict):
"""
Constructs a new Client Notification instance.
:param str method: The type of lsp notification being made.
:param dict params: The arguments of the given method.
"""
self.jsonrpc = "2.0"
self.method = method
self.params = params
@dataclass
class LSPResponseRequest(object):
"""
Constructs a new LSP Response Request instance.
:param id result: The id of the given message.
:param dict result: The arguments of the given method.
"""
jsonrpc: str
id: int
result: dict
@dataclass
class LSPResponseNotification(object):
"""
Constructs a new LSP Response Notification instance.
:param str method: The type of lsp notification being made.
:params dict result: The arguments of the given method.
"""
jsonrpc: str
method: str
params: dict
@dataclass
class LSPIDResponseNotification(object):
"""
Constructs a new LSP Response Notification instance.
:param str method: The type of lsp notification being made.
:params dict result: The arguments of the given method.
"""
jsonrpc: str
id: int
method: str
params: dict
class MessageTypes(ClientRequest, ClientNotification, LSPResponseRequest, LSPResponseNotification, LSPIDResponseNotification):
...
class ClientMessageTypes(ClientRequest, ClientNotification):
...
class LSPResponseTypes(LSPResponseRequest, LSPResponseNotification):
...

View File

@@ -0,0 +1,193 @@
# Python imports
import json
# Lib imports
# Application imports
from .lsp_message_structs import MessageEncoder
LEN_HEADER = "Content-Length: "
TYPE_HEADER = "Content-Type: "
def get_message_str(data: dict) -> str:
return json.dumps(data, separators = (',', ':'), indent = 4, cls = MessageEncoder)
def get_message_obj(data: str):
return json.loads(data)
# Request type formatting
# https://github.com/microsoft/multilspy/blob/main/src/multilspy/language_server.py#L417
content_part = {
"method": "textDocument/definition",
"params": {
"textDocument": {
"uri": "file://",
"languageId": "python",
"version": 1,
"text": ""
},
"position": {
"line": 5,
"character": 12,
"offset": 0
}
}
}
didopen_notification = {
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "file://",
"languageId": "python",
"version": 1,
"text": ""
}
}
}
didsave_notification = {
"method": "textDocument/didSave",
"params": {
"textDocument": {
"uri": "file://"
},
"text": ""
}
}
didclose_notification = {
"method": "textDocument/didClose",
"params": {
"textDocument": {
"uri": "file://"
}
}
}
didchange_notification = {
"method": "textDocument/didChange",
"params": {
"textDocument": {
"uri": "file://",
"languageId": "python",
"version": 1,
},
"contentChanges": [
{
"text": ""
}
]
}
}
didchange_notification_range = {
"method": "textDocument/didChange",
"params": {
"textDocument": {
"uri": "file://",
"languageId": "python",
"version": 1,
"text": ""
},
"contentChanges": [
{
"range": {
"start": {
"line": 1,
"character": 1,
},
"end": {
"line": 1,
"character": 1,
},
"rangeLength": 0
}
}
]
}
}
# CompletionTriggerKind = 1 | 2 | 3;
# export const Invoked: 1 = 1;
# export const TriggerCharacter: 2 = 2;
# export const TriggerForIncompleteCompletions: 3 = 3;
completion_request = {
"method": "textDocument/completion",
"params": {
"textDocument": {
"uri": "file://",
"languageId": "python",
"version": 1,
"text": ""
},
"position": {
"line": 5,
"character": 12,
"offset": 0
},
"contet": {
"triggerKind": 3,
"triggerCharacter": ""
}
}
}
definition_request = {
"method": "textDocument/definition",
"params": {
"textDocument": {
"uri": "file://",
"languageId": "python",
"version": 1,
"text": ""
},
"position": {
"line": 5,
"character": 12,
"offset": 0
}
}
}
references_request = {
"method": "textDocument/references",
"params": {
"context": {
"includeDeclaration": False
},
"textDocument": {
"uri": "file://",
"languageId": "python",
"version": 1,
"text": ""
},
"position": {
"line": 30,
"character": 13,
"offset": 0
}
}
}
symbols_request = {
"method": "textDocument/documentSymbol",
"params": {
"textDocument": {
"uri": "file://",
"languageId": "python",
"version": 1,
"text": ""
}
}
}

View File

@@ -0,0 +1,571 @@
# Python imports
import enum
# Lib imports
# Application imports
def to_type(o, new_type):
'''
Helper funciton that receives an object or a dict and convert it to a new
given type.
:param object|dict o: The object to convert
:param Type new_type: The type to convert to.
'''
return o if new_type == type(o) else new_type(**o)
class Position(object):
def __init__(self, line, character):
"""
Constructs a new Position instance.
:param int line: Line position in a document (zero-based).
:param int character: Character offset on a line in a document
(zero-based).
"""
self.line = line
self.character = character
class Range(object):
def __init__(self, start, end):
"""
Constructs a new Range instance.
:param Position start: The range's start position.
:param Position end: The range's end position.
"""
self.start = to_type(start, Position)
self.end = to_type(end, Position)
class Location(object):
"""
Represents a location inside a resource, such as a line inside a text file.
"""
def __init__(self, uri, range):
"""
Constructs a new Location instance.
:param str uri: Resource file.
:param Range range: The range inside the file
"""
self.uri = uri
self.range = to_type(range, Range)
class LocationLink(object):
"""
Represents a link between a source and a target location.
"""
def __init__(self, originSelectionRange, targetUri, targetRange, targetSelectionRange):
"""
Constructs a new LocationLink instance.
:param Range originSelectionRange: Span of the origin of this link.
Used as the underlined span for mouse interaction. Defaults to the word range at the mouse position.
:param str targetUri: The target resource identifier of this link.
:param Range targetRange: The full target range of this link. If the target for example is a symbol then target
range is the range enclosing this symbol not including leading/trailing whitespace but everything else
like comments. This information is typically used to highlight the range in the editor.
:param Range targetSelectionRange: The range that should be selected and revealed when this link is being followed,
e.g the name of a function. Must be contained by the the `targetRange`. See also `DocumentSymbol#range`
"""
self.originSelectionRange = to_type(originSelectionRange, Range)
self.targetUri = targetUri
self.targetRange = to_type(targetRange, Range)
self.targetSelectionRange = to_type(targetSelectionRange, Range)
class Diagnostic(object):
def __init__(self, range, severity, code, source, message, relatedInformation):
"""
Constructs a new Diagnostic instance.
:param Range range: The range at which the message applies.Resource file.
:param int severity: The diagnostic's severity. Can be omitted. If omitted it is up to the
client to interpret diagnostics as error, warning, info or hint.
:param str code: The diagnostic's code, which might appear in the user interface.
:param str source: A human-readable string describing the source of this
diagnostic, e.g. 'typescript' or 'super lint'.
:param str message: The diagnostic's message.
:param list relatedInformation: An array of related diagnostic information, e.g. when symbol-names within
a scope collide all definitions can be marked via this property.
"""
self.range = range
self.severity = severity
self.code = code
self.source = source
self.message = message
self.relatedInformation = relatedInformation
class DiagnosticSeverity(object):
Error = 1
Warning = 2 # TODO: warning is known in python
Information = 3
Hint = 4
class DiagnosticRelatedInformation(object):
def __init__(self, location, message):
"""
Constructs a new Diagnostic instance.
:param Location location: The location of this related diagnostic information.
:param str message: The message of this related diagnostic information.
"""
self.location = location
self.message = message
class Command(object):
def __init__(self, title, command, arguments):
"""
Constructs a new Diagnostic instance.
:param str title: Title of the command, like `save`.
:param str command: The identifier of the actual command handler.
:param list argusments: Arguments that the command handler should be invoked with.
"""
self.title = title
self.command = command
self.arguments = arguments
class TextDocumentItem(object):
"""
An item to transfer a text document from the client to the server.
"""
def __init__(self, uri, languageId, version, text):
"""
Constructs a new Diagnostic instance.
:param DocumentUri uri: uri file path.
:param str languageId: The identifier of the actual command handler.
:param int version: Arguments that the command handler should be invoked with.
:param str text: Arguments that the command handler should be invoked with.
"""
self.uri = uri
self.languageId = languageId
self.version = version
self.text = text
class TextDocumentIdentifier(object):
"""
Text documents are identified using a URI. On the protocol level, URIs are passed as strings.
"""
def __init__(self, uri):
"""
Constructs a new TextDocumentIdentifier instance.
:param DocumentUri uri: The text document's URI.
"""
self.uri = uri
class VersionedTextDocumentIdentifier(TextDocumentIdentifier):
"""
An identifier to denote a specific version of a text document.
"""
def __init__(self, uri, version):
"""
Constructs a new TextDocumentIdentifier instance.
:param DocumentUri uri: The text document's URI.
:param int version: The version number of this document. If a versioned
text document identifier is sent from the server to the client and
the file is not open in the editor (the server has not received an
open notification before) the server can send `null` to indicate
that the version is known and the content on disk is the truth (as
speced with document content ownership).
The version number of a document will increase after each change, including
undo/redo. The number doesn't need to be consecutive.
"""
super(VersionedTextDocumentIdentifier, self).__init__(uri)
self.version = version
class TextDocumentContentChangeEvent(object):
"""
An event describing a change to a text document. If range and rangeLength are omitted
the new text is considered to be the full content of the document.
"""
def __init__(self, range, rangeLength, text):
"""
Constructs a new TextDocumentContentChangeEvent instance.
:param Range range: The range of the document that changed.
:param int rangeLength: The length of the range that got replaced.
:param str text: The new text of the range/document.
"""
self.range = range
self.rangeLength = rangeLength
self.text = text
class TextDocumentPositionParams(object):
"""
A parameter literal used in requests to pass a text document and a position inside that document.
"""
def __init__(self, textDocument, position):
"""
Constructs a new TextDocumentPositionParams instance.
:param TextDocumentIdentifier textDocument: The text document.
:param Position position: The position inside the text document.
"""
self.textDocument = textDocument
self.position = position
class LANGUAGE_IDENTIFIER(object):
BAT = "bat"
BIBTEX = "bibtex"
CLOJURE = "clojure"
COFFESCRIPT = "coffeescript"
C = "c"
CPP = "cpp"
CSHARP = "csharp"
CSS = "css"
DIFF = "diff"
DOCKERFILE = "dockerfile"
FSHARP = "fsharp"
GIT_COMMIT = "git-commit"
GIT_REBASE = "git-rebase"
GO = "go"
GROOVY = "groovy"
HANDLEBARS = "handlebars"
HTML = "html"
INI = "ini"
JAVA = "java"
JAVASCRIPT = "javascript"
JSON = "json"
LATEX = "latex"
LESS = "less"
LUA = "lua"
MAKEFILE = "makefile"
MARKDOWN = "markdown"
OBJECTIVE_C = "objective-c"
OBJECTIVE_CPP = "objective-cpp"
Perl = "perl"
PHP = "php"
POWERSHELL = "powershell"
PUG = "jade"
PYTHON = "python"
R = "r"
RAZOR = "razor"
RUBY = "ruby"
RUST = "rust"
SASS = "sass"
SCSS = "scss"
ShaderLab = "shaderlab"
SHELL_SCRIPT = "shellscript"
SQL = "sql"
SWIFT = "swift"
TYPE_SCRIPT = "typescript"
TEX = "tex"
VB = "vb"
XML = "xml"
XSL = "xsl"
YAML = "yaml"
class SymbolKind(enum.Enum):
File = 1
Module = 2
Namespace = 3
Package = 4
Class = 5
Method = 6
Property = 7
Field = 8
Constructor = 9
Enum = 10
Interface = 11
Function = 12
Variable = 13
Constant = 14
String = 15
Number = 16
Boolean = 17
Array = 18
Object = 19
Key = 20
Null = 21
EnumMember = 22
Struct = 23
Event = 24
Operator = 25
TypeParameter = 26
class SymbolInformation(object):
"""
Represents information about programming constructs like variables, classes, interfaces etc.
"""
def __init__(self, name, kind, location, containerName = None, deprecated = False):
"""
Constructs a new SymbolInformation instance.
:param str name: The name of this symbol.
:param int kind: The kind of this symbol.
:param bool Location: The location of this symbol. The location's range is used by a tool
to reveal the location in the editor. If the symbol is selected in the
tool the range's start information is used to position the cursor. So
the range usually spans more then the actual symbol's name and does
normally include things like visibility modifiers.
The range doesn't have to denote a node range in the sense of a abstract
syntax tree. It can therefore not be used to re-construct a hierarchy of
the symbols.
:param str containerName: The name of the symbol containing this symbol. This information is for
user interface purposes (e.g. to render a qualifier in the user interface
if necessary). It can't be used to re-infer a hierarchy for the document
symbols.
:param bool deprecated: Indicates if this symbol is deprecated.
"""
self.name = name
self.kind = SymbolKind(kind)
self.deprecated = deprecated
self.location = to_type(location, Location)
self.containerName = containerName
class ParameterInformation(object):
"""
Represents a parameter of a callable-signature. A parameter can
have a label and a doc-comment.
"""
def __init__(self, label, documentation = ""):
"""
Constructs a new ParameterInformation instance.
:param str label: The label of this parameter. Will be shown in the UI.
:param str documentation: The human-readable doc-comment of this parameter. Will be shown in the UI but can be omitted.
"""
self.label = label
self.documentation = documentation
class SignatureInformation(object):
"""
Represents the signature of something callable. A signature
can have a label, like a function-name, a doc-comment, and
a set of parameters.
"""
def __init__(self, label, documentation = "", parameters = []):
"""
Constructs a new SignatureInformation instance.
:param str label: The label of this signature. Will be shown in the UI.
:param str documentation: The human-readable doc-comment of this signature. Will be shown in the UI but can be omitted.
:param ParameterInformation[] parameters: The parameters of this signature.
"""
self.label = label
self.documentation = documentation
self.parameters = [to_type(parameter, ParameterInformation) for parameter in parameters]
class SignatureHelp(object):
"""
Signature help represents the signature of something
callable. There can be multiple signature but only one
active and only one active parameter.
"""
def __init__(self, signatures, activeSignature = 0, activeParameter = 0):
"""
Constructs a new SignatureHelp instance.
:param SignatureInformation[] signatures: One or more signatures.
:param int activeSignature:
:param int activeParameter:
"""
self.signatures = [to_type(signature, SignatureInformation) for signature in signatures]
self.activeSignature = activeSignature
self.activeParameter = activeParameter
class CompletionTriggerKind(object):
Invoked = 1
TriggerCharacter = 2
TriggerForIncompleteCompletions = 3
class CompletionContext(object):
"""
Contains additional information about the context in which a completion request is triggered.
"""
def __init__(self, triggerKind, triggerCharacter = None):
"""
Constructs a new CompletionContext instance.
:param CompletionTriggerKind triggerKind: How the completion was triggered.
:param str triggerCharacter: The trigger character (a single character) that has trigger code complete.
Is undefined if `triggerKind !== CompletionTriggerKind.TriggerCharacter`
"""
self.triggerKind = triggerKind
if triggerCharacter:
self.triggerCharacter = triggerCharacter
class TextEdit(object):
"""
A textual edit applicable to a text document.
"""
def __init__(self, range, newText):
"""
:param Range range: The range of the text document to be manipulated. To insert
text into a document create a range where start === end.
:param str newText: The string to be inserted. For delete operations use an empty string.
"""
self.range = range
self.newText = newText
class InsertTextFormat(object):
PlainText = 1
Snippet = 2
class CompletionItem(object):
"""
"""
def __init__(self, label, \
kind = None, \
detail = None, \
documentation = None, \
deprecated = None, \
preselect = None, \
sortText = None, \
filterText = None, \
insertText = None, \
insertTextFormat = None, \
textEdit = None, \
additionalTextEdits = None, \
commitCharacters = None, \
command = None, \
data = None, \
score = 0.0
):
"""
:param str label: The label of this completion item. By default also the text that is inserted when selecting
this completion.
:param int kind: The kind of this completion item. Based of the kind an icon is chosen by the editor.
:param str detail: A human-readable string with additional information about this item, like type or symbol information.
:param tr ocumentation: A human-readable string that represents a doc-comment.
:param bool deprecated: Indicates if this item is deprecated.
:param bool preselect: Select this item when showing. Note: that only one completion item can be selected and that the
tool / client decides which item that is. The rule is that the first item of those that match best is selected.
:param str sortText: A string that should be used when comparing this item with other items. When `falsy` the label is used.
:param str filterText: A string that should be used when filtering a set of completion items. When `falsy` the label is used.
:param str insertText: A string that should be inserted into a document when selecting this completion. When `falsy` the label is used.
The `insertText` is subject to interpretation by the client side. Some tools might not take the string literally. For example
VS Code when code complete is requested in this example `con<cursor position>` and a completion item with an `insertText` of `console` is provided it
will only insert `sole`. Therefore it is recommended to use `textEdit` instead since it avoids additional client side interpretation.
@deprecated Use textEdit instead.
:param InsertTextFormat insertTextFormat: The format of the insert text. The format applies to both the `insertText` property
and the `newText` property of a provided `textEdit`.
:param TextEdit textEdit: An edit which is applied to a document when selecting this completion. When an edit is provided the value of `insertText` is ignored.
Note:* The range of the edit must be a single line range and it must contain the position at which completion
has been requested.
:param TextEdit additionalTextEdits: An optional array of additional text edits that are applied when selecting this completion.
Edits must not overlap (including the same insert position) with the main edit nor with themselves.
Additional text edits should be used to change text unrelated to the current cursor position
(for example adding an import statement at the top of the file if the completion item will
insert an unqualified type).
:param str commitCharacters: An optional set of characters that when pressed while this completion is active will accept it first and
then type that character. *Note* that all commit characters should have `length=1` and that superfluous
characters will be ignored.
:param Command command: An optional command that is executed *after* inserting this completion. Note: that
additional modifications to the current document should be described with the additionalTextEdits-property.
:param data: An data entry field that is preserved on a completion item between a completion and a completion resolve request.
:param float score: Score of the code completion item.
"""
self.label = label
self.kind = kind
self.detail = detail
self.documentation = documentation
self.deprecated = deprecated
self.preselect = preselect
self.sortText = sortText
self.filterText = filterText
self.insertText = insertText
self.insertTextFormat = insertTextFormat
self.textEdit = textEdit
self.additionalTextEdits = additionalTextEdits
self.commitCharacters = commitCharacters
self.command = command
self.data = data
self.score = score
class CompletionItemKind(enum.Enum):
Text = 1
Method = 2
Function = 3
Constructor = 4
Field = 5
Variable = 6
Class = 7
Interface = 8
Module = 9
Property = 10
Unit = 11
Value = 12
Enum = 13
Keyword = 14
Snippet = 15
Color = 16
File = 17
Reference = 18
Folder = 19
EnumMember = 20
Constant = 21
Struct = 22
Event = 23
Operator = 24
TypeParameter = 25
class CompletionList(object):
"""
Represents a collection of [completion items](#CompletionItem) to be preselect in the editor.
"""
def __init__(self, isIncomplete, items):
"""
Constructs a new CompletionContext instance.
:param bool isIncomplete: This list it not complete. Further typing should result in recomputing this list.
:param CompletionItem items: The completion items.
"""
self.isIncomplete = isIncomplete
self.items = [to_type(i, CompletionItem) for i in items]
class ErrorCodes(enum.Enum):
# Defined by JSON RPC
ParseError = -32700
InvalidRequest = -32600
MethodNotFound = -32601
InvalidParams = -32602
InternalError = -32603
serverErrorStart = -32099
serverErrorEnd = -32000
ServerNotInitialized = -32002
UnknownErrorCode = -32001
# Defined by the protocol.
RequestCancelled = -32800
ContentModified = -32801
class ResponseError(Exception):
def __init__(self, code, message, data = None):
self.code = code
self.message = message
if data:
self.data = data

View File

@@ -0,0 +1,3 @@
"""
Pligin Libs Module
"""

View File

@@ -0,0 +1,27 @@
"""
__init__.py
websocket - WebSocket client library for Python
Copyright 2024 engn33r
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from ._abnf import *
from ._app import WebSocketApp as WebSocketApp, setReconnect as setReconnect
from ._core import *
from ._exceptions import *
from ._logging import *
from ._socket import *
__version__ = "1.8.0"

View File

@@ -0,0 +1,453 @@
import array
import os
import struct
import sys
from threading import Lock
from typing import Callable, Optional, Union
from ._exceptions import WebSocketPayloadException, WebSocketProtocolException
from ._utils import validate_utf8
"""
_abnf.py
websocket - WebSocket client library for Python
Copyright 2024 engn33r
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
try:
# If wsaccel is available, use compiled routines to mask data.
# wsaccel only provides around a 10% speed boost compared
# to the websocket-client _mask() implementation.
# Note that wsaccel is unmaintained.
from wsaccel.xormask import XorMaskerSimple
def _mask(mask_value: array.array, data_value: array.array) -> bytes:
mask_result: bytes = XorMaskerSimple(mask_value).process(data_value)
return mask_result
except ImportError:
# wsaccel is not available, use websocket-client _mask()
native_byteorder = sys.byteorder
def _mask(mask_value: array.array, data_value: array.array) -> bytes:
datalen = len(data_value)
int_data_value = int.from_bytes(data_value, native_byteorder)
int_mask_value = int.from_bytes(
mask_value * (datalen // 4) + mask_value[: datalen % 4], native_byteorder
)
return (int_data_value ^ int_mask_value).to_bytes(datalen, native_byteorder)
__all__ = [
"ABNF",
"continuous_frame",
"frame_buffer",
"STATUS_NORMAL",
"STATUS_GOING_AWAY",
"STATUS_PROTOCOL_ERROR",
"STATUS_UNSUPPORTED_DATA_TYPE",
"STATUS_STATUS_NOT_AVAILABLE",
"STATUS_ABNORMAL_CLOSED",
"STATUS_INVALID_PAYLOAD",
"STATUS_POLICY_VIOLATION",
"STATUS_MESSAGE_TOO_BIG",
"STATUS_INVALID_EXTENSION",
"STATUS_UNEXPECTED_CONDITION",
"STATUS_BAD_GATEWAY",
"STATUS_TLS_HANDSHAKE_ERROR",
]
# closing frame status codes.
STATUS_NORMAL = 1000
STATUS_GOING_AWAY = 1001
STATUS_PROTOCOL_ERROR = 1002
STATUS_UNSUPPORTED_DATA_TYPE = 1003
STATUS_STATUS_NOT_AVAILABLE = 1005
STATUS_ABNORMAL_CLOSED = 1006
STATUS_INVALID_PAYLOAD = 1007
STATUS_POLICY_VIOLATION = 1008
STATUS_MESSAGE_TOO_BIG = 1009
STATUS_INVALID_EXTENSION = 1010
STATUS_UNEXPECTED_CONDITION = 1011
STATUS_SERVICE_RESTART = 1012
STATUS_TRY_AGAIN_LATER = 1013
STATUS_BAD_GATEWAY = 1014
STATUS_TLS_HANDSHAKE_ERROR = 1015
VALID_CLOSE_STATUS = (
STATUS_NORMAL,
STATUS_GOING_AWAY,
STATUS_PROTOCOL_ERROR,
STATUS_UNSUPPORTED_DATA_TYPE,
STATUS_INVALID_PAYLOAD,
STATUS_POLICY_VIOLATION,
STATUS_MESSAGE_TOO_BIG,
STATUS_INVALID_EXTENSION,
STATUS_UNEXPECTED_CONDITION,
STATUS_SERVICE_RESTART,
STATUS_TRY_AGAIN_LATER,
STATUS_BAD_GATEWAY,
)
class ABNF:
"""
ABNF frame class.
See http://tools.ietf.org/html/rfc5234
and http://tools.ietf.org/html/rfc6455#section-5.2
"""
# operation code values.
OPCODE_CONT = 0x0
OPCODE_TEXT = 0x1
OPCODE_BINARY = 0x2
OPCODE_CLOSE = 0x8
OPCODE_PING = 0x9
OPCODE_PONG = 0xA
# available operation code value tuple
OPCODES = (
OPCODE_CONT,
OPCODE_TEXT,
OPCODE_BINARY,
OPCODE_CLOSE,
OPCODE_PING,
OPCODE_PONG,
)
# opcode human readable string
OPCODE_MAP = {
OPCODE_CONT: "cont",
OPCODE_TEXT: "text",
OPCODE_BINARY: "binary",
OPCODE_CLOSE: "close",
OPCODE_PING: "ping",
OPCODE_PONG: "pong",
}
# data length threshold.
LENGTH_7 = 0x7E
LENGTH_16 = 1 << 16
LENGTH_63 = 1 << 63
def __init__(
self,
fin: int = 0,
rsv1: int = 0,
rsv2: int = 0,
rsv3: int = 0,
opcode: int = OPCODE_TEXT,
mask_value: int = 1,
data: Union[str, bytes, None] = "",
) -> None:
"""
Constructor for ABNF. Please check RFC for arguments.
"""
self.fin = fin
self.rsv1 = rsv1
self.rsv2 = rsv2
self.rsv3 = rsv3
self.opcode = opcode
self.mask_value = mask_value
if data is None:
data = ""
self.data = data
self.get_mask_key = os.urandom
def validate(self, skip_utf8_validation: bool = False) -> None:
"""
Validate the ABNF frame.
Parameters
----------
skip_utf8_validation: skip utf8 validation.
"""
if self.rsv1 or self.rsv2 or self.rsv3:
raise WebSocketProtocolException("rsv is not implemented, yet")
if self.opcode not in ABNF.OPCODES:
raise WebSocketProtocolException("Invalid opcode %r", self.opcode)
if self.opcode == ABNF.OPCODE_PING and not self.fin:
raise WebSocketProtocolException("Invalid ping frame.")
if self.opcode == ABNF.OPCODE_CLOSE:
l = len(self.data)
if not l:
return
if l == 1 or l >= 126:
raise WebSocketProtocolException("Invalid close frame.")
if l > 2 and not skip_utf8_validation and not validate_utf8(self.data[2:]):
raise WebSocketProtocolException("Invalid close frame.")
code = 256 * int(self.data[0]) + int(self.data[1])
if not self._is_valid_close_status(code):
raise WebSocketProtocolException("Invalid close opcode %r", code)
@staticmethod
def _is_valid_close_status(code: int) -> bool:
return code in VALID_CLOSE_STATUS or (3000 <= code < 5000)
def __str__(self) -> str:
return f"fin={self.fin} opcode={self.opcode} data={self.data}"
@staticmethod
def create_frame(data: Union[bytes, str], opcode: int, fin: int = 1) -> "ABNF":
"""
Create frame to send text, binary and other data.
Parameters
----------
data: str
data to send. This is string value(byte array).
If opcode is OPCODE_TEXT and this value is unicode,
data value is converted into unicode string, automatically.
opcode: int
operation code. please see OPCODE_MAP.
fin: int
fin flag. if set to 0, create continue fragmentation.
"""
if opcode == ABNF.OPCODE_TEXT and isinstance(data, str):
data = data.encode("utf-8")
# mask must be set if send data from client
return ABNF(fin, 0, 0, 0, opcode, 1, data)
def format(self) -> bytes:
"""
Format this object to string(byte array) to send data to server.
"""
if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]):
raise ValueError("not 0 or 1")
if self.opcode not in ABNF.OPCODES:
raise ValueError("Invalid OPCODE")
length = len(self.data)
if length >= ABNF.LENGTH_63:
raise ValueError("data is too long")
frame_header = chr(
self.fin << 7
| self.rsv1 << 6
| self.rsv2 << 5
| self.rsv3 << 4
| self.opcode
).encode("latin-1")
if length < ABNF.LENGTH_7:
frame_header += chr(self.mask_value << 7 | length).encode("latin-1")
elif length < ABNF.LENGTH_16:
frame_header += chr(self.mask_value << 7 | 0x7E).encode("latin-1")
frame_header += struct.pack("!H", length)
else:
frame_header += chr(self.mask_value << 7 | 0x7F).encode("latin-1")
frame_header += struct.pack("!Q", length)
if not self.mask_value:
if isinstance(self.data, str):
self.data = self.data.encode("utf-8")
return frame_header + self.data
mask_key = self.get_mask_key(4)
return frame_header + self._get_masked(mask_key)
def _get_masked(self, mask_key: Union[str, bytes]) -> bytes:
s = ABNF.mask(mask_key, self.data)
if isinstance(mask_key, str):
mask_key = mask_key.encode("utf-8")
return mask_key + s
@staticmethod
def mask(mask_key: Union[str, bytes], data: Union[str, bytes]) -> bytes:
"""
Mask or unmask data. Just do xor for each byte
Parameters
----------
mask_key: bytes or str
4 byte mask.
data: bytes or str
data to mask/unmask.
"""
if data is None:
data = ""
if isinstance(mask_key, str):
mask_key = mask_key.encode("latin-1")
if isinstance(data, str):
data = data.encode("latin-1")
return _mask(array.array("B", mask_key), array.array("B", data))
class frame_buffer:
_HEADER_MASK_INDEX = 5
_HEADER_LENGTH_INDEX = 6
def __init__(
self, recv_fn: Callable[[int], int], skip_utf8_validation: bool
) -> None:
self.recv = recv_fn
self.skip_utf8_validation = skip_utf8_validation
# Buffers over the packets from the layer beneath until desired amount
# bytes of bytes are received.
self.recv_buffer: list = []
self.clear()
self.lock = Lock()
def clear(self) -> None:
self.header: Optional[tuple] = None
self.length: Optional[int] = None
self.mask_value: Union[bytes, str, None] = None
def has_received_header(self) -> bool:
return self.header is None
def recv_header(self) -> None:
header = self.recv_strict(2)
b1 = header[0]
fin = b1 >> 7 & 1
rsv1 = b1 >> 6 & 1
rsv2 = b1 >> 5 & 1
rsv3 = b1 >> 4 & 1
opcode = b1 & 0xF
b2 = header[1]
has_mask = b2 >> 7 & 1
length_bits = b2 & 0x7F
self.header = (fin, rsv1, rsv2, rsv3, opcode, has_mask, length_bits)
def has_mask(self) -> Union[bool, int]:
if not self.header:
return False
header_val: int = self.header[frame_buffer._HEADER_MASK_INDEX]
return header_val
def has_received_length(self) -> bool:
return self.length is None
def recv_length(self) -> None:
bits = self.header[frame_buffer._HEADER_LENGTH_INDEX]
length_bits = bits & 0x7F
if length_bits == 0x7E:
v = self.recv_strict(2)
self.length = struct.unpack("!H", v)[0]
elif length_bits == 0x7F:
v = self.recv_strict(8)
self.length = struct.unpack("!Q", v)[0]
else:
self.length = length_bits
def has_received_mask(self) -> bool:
return self.mask_value is None
def recv_mask(self) -> None:
self.mask_value = self.recv_strict(4) if self.has_mask() else ""
def recv_frame(self) -> ABNF:
with self.lock:
# Header
if self.has_received_header():
self.recv_header()
(fin, rsv1, rsv2, rsv3, opcode, has_mask, _) = self.header
# Frame length
if self.has_received_length():
self.recv_length()
length = self.length
# Mask
if self.has_received_mask():
self.recv_mask()
mask_value = self.mask_value
# Payload
payload = self.recv_strict(length)
if has_mask:
payload = ABNF.mask(mask_value, payload)
# Reset for next frame
self.clear()
frame = ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload)
frame.validate(self.skip_utf8_validation)
return frame
def recv_strict(self, bufsize: int) -> bytes:
shortage = bufsize - sum(map(len, self.recv_buffer))
while shortage > 0:
# Limit buffer size that we pass to socket.recv() to avoid
# fragmenting the heap -- the number of bytes recv() actually
# reads is limited by socket buffer and is relatively small,
# yet passing large numbers repeatedly causes lots of large
# buffers allocated and then shrunk, which results in
# fragmentation.
bytes_ = self.recv(min(16384, shortage))
self.recv_buffer.append(bytes_)
shortage -= len(bytes_)
unified = b"".join(self.recv_buffer)
if shortage == 0:
self.recv_buffer = []
return unified
else:
self.recv_buffer = [unified[bufsize:]]
return unified[:bufsize]
class continuous_frame:
def __init__(self, fire_cont_frame: bool, skip_utf8_validation: bool) -> None:
self.fire_cont_frame = fire_cont_frame
self.skip_utf8_validation = skip_utf8_validation
self.cont_data: Optional[list] = None
self.recving_frames: Optional[int] = None
def validate(self, frame: ABNF) -> None:
if not self.recving_frames and frame.opcode == ABNF.OPCODE_CONT:
raise WebSocketProtocolException("Illegal frame")
if self.recving_frames and frame.opcode in (
ABNF.OPCODE_TEXT,
ABNF.OPCODE_BINARY,
):
raise WebSocketProtocolException("Illegal frame")
def add(self, frame: ABNF) -> None:
if self.cont_data:
self.cont_data[1] += frame.data
else:
if frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY):
self.recving_frames = frame.opcode
self.cont_data = [frame.opcode, frame.data]
if frame.fin:
self.recving_frames = None
def is_fire(self, frame: ABNF) -> Union[bool, int]:
return frame.fin or self.fire_cont_frame
def extract(self, frame: ABNF) -> tuple:
data = self.cont_data
self.cont_data = None
frame.data = data[1]
if (
not self.fire_cont_frame
and data[0] == ABNF.OPCODE_TEXT
and not self.skip_utf8_validation
and not validate_utf8(frame.data)
):
raise WebSocketPayloadException(f"cannot decode: {repr(frame.data)}")
return data[0], frame

View File

@@ -0,0 +1,677 @@
import inspect
import selectors
import socket
import threading
import time
from typing import Any, Callable, Optional, Union
from . import _logging
from ._abnf import ABNF
from ._core import WebSocket, getdefaulttimeout
from ._exceptions import (
WebSocketConnectionClosedException,
WebSocketException,
WebSocketTimeoutException,
)
from ._ssl_compat import SSLEOFError
from ._url import parse_url
"""
_app.py
websocket - WebSocket client library for Python
Copyright 2024 engn33r
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
__all__ = ["WebSocketApp"]
RECONNECT = 0
def setReconnect(reconnectInterval: int) -> None:
global RECONNECT
RECONNECT = reconnectInterval
class DispatcherBase:
"""
DispatcherBase
"""
def __init__(self, app: Any, ping_timeout: Union[float, int, None]) -> None:
self.app = app
self.ping_timeout = ping_timeout
def timeout(self, seconds: Union[float, int, None], callback: Callable) -> None:
time.sleep(seconds)
callback()
def reconnect(self, seconds: int, reconnector: Callable) -> None:
try:
_logging.info(
f"reconnect() - retrying in {seconds} seconds [{len(inspect.stack())} frames in stack]"
)
time.sleep(seconds)
reconnector(reconnecting=True)
except KeyboardInterrupt as e:
_logging.info(f"User exited {e}")
raise e
class Dispatcher(DispatcherBase):
"""
Dispatcher
"""
def read(
self,
sock: socket.socket,
read_callback: Callable,
check_callback: Callable,
) -> None:
sel = selectors.DefaultSelector()
sel.register(self.app.sock.sock, selectors.EVENT_READ)
try:
while self.app.keep_running:
if sel.select(self.ping_timeout):
if not read_callback():
break
check_callback()
finally:
sel.close()
class SSLDispatcher(DispatcherBase):
"""
SSLDispatcher
"""
def read(
self,
sock: socket.socket,
read_callback: Callable,
check_callback: Callable,
) -> None:
sock = self.app.sock.sock
sel = selectors.DefaultSelector()
sel.register(sock, selectors.EVENT_READ)
try:
while self.app.keep_running:
if self.select(sock, sel):
if not read_callback():
break
check_callback()
finally:
sel.close()
def select(self, sock, sel: selectors.DefaultSelector):
sock = self.app.sock.sock
if sock.pending():
return [
sock,
]
r = sel.select(self.ping_timeout)
if len(r) > 0:
return r[0][0]
class WrappedDispatcher:
"""
WrappedDispatcher
"""
def __init__(self, app, ping_timeout: Union[float, int, None], dispatcher) -> None:
self.app = app
self.ping_timeout = ping_timeout
self.dispatcher = dispatcher
dispatcher.signal(2, dispatcher.abort) # keyboard interrupt
def read(
self,
sock: socket.socket,
read_callback: Callable,
check_callback: Callable,
) -> None:
self.dispatcher.read(sock, read_callback)
self.ping_timeout and self.timeout(self.ping_timeout, check_callback)
def timeout(self, seconds: float, callback: Callable) -> None:
self.dispatcher.timeout(seconds, callback)
def reconnect(self, seconds: int, reconnector: Callable) -> None:
self.timeout(seconds, reconnector)
class WebSocketApp:
"""
Higher level of APIs are provided. The interface is like JavaScript WebSocket object.
"""
def __init__(
self,
url: str,
header: Union[list, dict, Callable, None] = None,
on_open: Optional[Callable[[WebSocket], None]] = None,
on_reconnect: Optional[Callable[[WebSocket], None]] = None,
on_message: Optional[Callable[[WebSocket, Any], None]] = None,
on_error: Optional[Callable[[WebSocket, Any], None]] = None,
on_close: Optional[Callable[[WebSocket, Any, Any], None]] = None,
on_ping: Optional[Callable] = None,
on_pong: Optional[Callable] = None,
on_cont_message: Optional[Callable] = None,
keep_running: bool = True,
get_mask_key: Optional[Callable] = None,
cookie: Optional[str] = None,
subprotocols: Optional[list] = None,
on_data: Optional[Callable] = None,
socket: Optional[socket.socket] = None,
) -> None:
"""
WebSocketApp initialization
Parameters
----------
url: str
Websocket url.
header: list or dict or Callable
Custom header for websocket handshake.
If the parameter is a callable object, it is called just before the connection attempt.
The returned dict or list is used as custom header value.
This could be useful in order to properly setup timestamp dependent headers.
on_open: function
Callback object which is called at opening websocket.
on_open has one argument.
The 1st argument is this class object.
on_reconnect: function
Callback object which is called at reconnecting websocket.
on_reconnect has one argument.
The 1st argument is this class object.
on_message: function
Callback object which is called when received data.
on_message has 2 arguments.
The 1st argument is this class object.
The 2nd argument is utf-8 data received from the server.
on_error: function
Callback object which is called when we get error.
on_error has 2 arguments.
The 1st argument is this class object.
The 2nd argument is exception object.
on_close: function
Callback object which is called when connection is closed.
on_close has 3 arguments.
The 1st argument is this class object.
The 2nd argument is close_status_code.
The 3rd argument is close_msg.
on_cont_message: function
Callback object which is called when a continuation
frame is received.
on_cont_message has 3 arguments.
The 1st argument is this class object.
The 2nd argument is utf-8 string which we get from the server.
The 3rd argument is continue flag. if 0, the data continue
to next frame data
on_data: function
Callback object which is called when a message received.
This is called before on_message or on_cont_message,
and then on_message or on_cont_message is called.
on_data has 4 argument.
The 1st argument is this class object.
The 2nd argument is utf-8 string which we get from the server.
The 3rd argument is data type. ABNF.OPCODE_TEXT or ABNF.OPCODE_BINARY will be came.
The 4th argument is continue flag. If 0, the data continue
keep_running: bool
This parameter is obsolete and ignored.
get_mask_key: function
A callable function to get new mask keys, see the
WebSocket.set_mask_key's docstring for more information.
cookie: str
Cookie value.
subprotocols: list
List of available sub protocols. Default is None.
socket: socket
Pre-initialized stream socket.
"""
self.url = url
self.header = header if header is not None else []
self.cookie = cookie
self.on_open = on_open
self.on_reconnect = on_reconnect
self.on_message = on_message
self.on_data = on_data
self.on_error = on_error
self.on_close = on_close
self.on_ping = on_ping
self.on_pong = on_pong
self.on_cont_message = on_cont_message
self.keep_running = False
self.get_mask_key = get_mask_key
self.sock: Optional[WebSocket] = None
self.last_ping_tm = float(0)
self.last_pong_tm = float(0)
self.ping_thread: Optional[threading.Thread] = None
self.stop_ping: Optional[threading.Event] = None
self.ping_interval = float(0)
self.ping_timeout: Union[float, int, None] = None
self.ping_payload = ""
self.subprotocols = subprotocols
self.prepared_socket = socket
self.has_errored = False
self.has_done_teardown = False
self.has_done_teardown_lock = threading.Lock()
def send(self, data: Union[bytes, str], opcode: int = ABNF.OPCODE_TEXT) -> None:
"""
send message
Parameters
----------
data: str
Message to send. If you set opcode to OPCODE_TEXT,
data must be utf-8 string or unicode.
opcode: int
Operation code of data. Default is OPCODE_TEXT.
"""
if not self.sock or self.sock.send(data, opcode) == 0:
raise WebSocketConnectionClosedException("Connection is already closed.")
def send_text(self, text_data: str) -> None:
"""
Sends UTF-8 encoded text.
"""
if not self.sock or self.sock.send(text_data, ABNF.OPCODE_TEXT) == 0:
raise WebSocketConnectionClosedException("Connection is already closed.")
def send_bytes(self, data: Union[bytes, bytearray]) -> None:
"""
Sends a sequence of bytes.
"""
if not self.sock or self.sock.send(data, ABNF.OPCODE_BINARY) == 0:
raise WebSocketConnectionClosedException("Connection is already closed.")
def close(self, **kwargs) -> None:
"""
Close websocket connection.
"""
self.keep_running = False
if self.sock:
self.sock.close(**kwargs)
self.sock = None
def _start_ping_thread(self) -> None:
self.last_ping_tm = self.last_pong_tm = float(0)
self.stop_ping = threading.Event()
self.ping_thread = threading.Thread(target=self._send_ping)
self.ping_thread.daemon = True
self.ping_thread.start()
def _stop_ping_thread(self) -> None:
if self.stop_ping:
self.stop_ping.set()
if self.ping_thread and self.ping_thread.is_alive():
self.ping_thread.join(3)
self.last_ping_tm = self.last_pong_tm = float(0)
def _send_ping(self) -> None:
if self.stop_ping.wait(self.ping_interval) or self.keep_running is False:
return
while not self.stop_ping.wait(self.ping_interval) and self.keep_running is True:
if self.sock:
self.last_ping_tm = time.time()
try:
_logging.debug("Sending ping")
self.sock.ping(self.ping_payload)
except Exception as e:
_logging.debug(f"Failed to send ping: {e}")
def run_forever(
self,
sockopt: tuple = None,
sslopt: dict = None,
ping_interval: Union[float, int] = 0,
ping_timeout: Union[float, int, None] = None,
ping_payload: str = "",
http_proxy_host: str = None,
http_proxy_port: Union[int, str] = None,
http_no_proxy: list = None,
http_proxy_auth: tuple = None,
http_proxy_timeout: Optional[float] = None,
skip_utf8_validation: bool = False,
host: str = None,
origin: str = None,
dispatcher=None,
suppress_origin: bool = False,
proxy_type: str = None,
reconnect: int = None,
) -> bool:
"""
Run event loop for WebSocket framework.
This loop is an infinite loop and is alive while websocket is available.
Parameters
----------
sockopt: tuple
Values for socket.setsockopt.
sockopt must be tuple
and each element is argument of sock.setsockopt.
sslopt: dict
Optional dict object for ssl socket option.
ping_interval: int or float
Automatically send "ping" command
every specified period (in seconds).
If set to 0, no ping is sent periodically.
ping_timeout: int or float
Timeout (in seconds) if the pong message is not received.
ping_payload: str
Payload message to send with each ping.
http_proxy_host: str
HTTP proxy host name.
http_proxy_port: int or str
HTTP proxy port. If not set, set to 80.
http_no_proxy: list
Whitelisted host names that don't use the proxy.
http_proxy_timeout: int or float
HTTP proxy timeout, default is 60 sec as per python-socks.
http_proxy_auth: tuple
HTTP proxy auth information. tuple of username and password. Default is None.
skip_utf8_validation: bool
skip utf8 validation.
host: str
update host header.
origin: str
update origin header.
dispatcher: Dispatcher object
customize reading data from socket.
suppress_origin: bool
suppress outputting origin header.
proxy_type: str
type of proxy from: http, socks4, socks4a, socks5, socks5h
reconnect: int
delay interval when reconnecting
Returns
-------
teardown: bool
False if the `WebSocketApp` is closed or caught KeyboardInterrupt,
True if any other exception was raised during a loop.
"""
if reconnect is None:
reconnect = RECONNECT
if ping_timeout is not None and ping_timeout <= 0:
raise WebSocketException("Ensure ping_timeout > 0")
if ping_interval is not None and ping_interval < 0:
raise WebSocketException("Ensure ping_interval >= 0")
if ping_timeout and ping_interval and ping_interval <= ping_timeout:
raise WebSocketException("Ensure ping_interval > ping_timeout")
if not sockopt:
sockopt = ()
if not sslopt:
sslopt = {}
if self.sock:
raise WebSocketException("socket is already opened")
self.ping_interval = ping_interval
self.ping_timeout = ping_timeout
self.ping_payload = ping_payload
self.has_done_teardown = False
self.keep_running = True
def teardown(close_frame: ABNF = None):
"""
Tears down the connection.
Parameters
----------
close_frame: ABNF frame
If close_frame is set, the on_close handler is invoked
with the statusCode and reason from the provided frame.
"""
# teardown() is called in many code paths to ensure resources are cleaned up and on_close is fired.
# To ensure the work is only done once, we use this bool and lock.
with self.has_done_teardown_lock:
if self.has_done_teardown:
return
self.has_done_teardown = True
self._stop_ping_thread()
self.keep_running = False
if self.sock:
self.sock.close()
close_status_code, close_reason = self._get_close_args(
close_frame if close_frame else None
)
self.sock = None
# Finally call the callback AFTER all teardown is complete
self._callback(self.on_close, close_status_code, close_reason)
def setSock(reconnecting: bool = False) -> None:
if reconnecting and self.sock:
self.sock.shutdown()
self.sock = WebSocket(
self.get_mask_key,
sockopt=sockopt,
sslopt=sslopt,
fire_cont_frame=self.on_cont_message is not None,
skip_utf8_validation=skip_utf8_validation,
enable_multithread=True,
)
self.sock.settimeout(getdefaulttimeout())
try:
header = self.header() if callable(self.header) else self.header
self.sock.connect(
self.url,
header=header,
cookie=self.cookie,
http_proxy_host=http_proxy_host,
http_proxy_port=http_proxy_port,
http_no_proxy=http_no_proxy,
http_proxy_auth=http_proxy_auth,
http_proxy_timeout=http_proxy_timeout,
subprotocols=self.subprotocols,
host=host,
origin=origin,
suppress_origin=suppress_origin,
proxy_type=proxy_type,
socket=self.prepared_socket,
)
_logging.info("Websocket connected")
if self.ping_interval:
self._start_ping_thread()
if reconnecting and self.on_reconnect:
self._callback(self.on_reconnect)
else:
self._callback(self.on_open)
dispatcher.read(self.sock.sock, read, check)
except (
WebSocketConnectionClosedException,
ConnectionRefusedError,
KeyboardInterrupt,
SystemExit,
Exception,
) as e:
handleDisconnect(e, reconnecting)
def read() -> bool:
if not self.keep_running:
return teardown()
try:
op_code, frame = self.sock.recv_data_frame(True)
except (
WebSocketConnectionClosedException,
KeyboardInterrupt,
SSLEOFError,
) as e:
if custom_dispatcher:
return handleDisconnect(e, bool(reconnect))
else:
raise e
if op_code == ABNF.OPCODE_CLOSE:
return teardown(frame)
elif op_code == ABNF.OPCODE_PING:
self._callback(self.on_ping, frame.data)
elif op_code == ABNF.OPCODE_PONG:
self.last_pong_tm = time.time()
self._callback(self.on_pong, frame.data)
elif op_code == ABNF.OPCODE_CONT and self.on_cont_message:
self._callback(self.on_data, frame.data, frame.opcode, frame.fin)
self._callback(self.on_cont_message, frame.data, frame.fin)
else:
data = frame.data
if op_code == ABNF.OPCODE_TEXT and not skip_utf8_validation:
data = data.decode("utf-8")
self._callback(self.on_data, data, frame.opcode, True)
self._callback(self.on_message, data)
return True
def check() -> bool:
if self.ping_timeout:
has_timeout_expired = (
time.time() - self.last_ping_tm > self.ping_timeout
)
has_pong_not_arrived_after_last_ping = (
self.last_pong_tm - self.last_ping_tm < 0
)
has_pong_arrived_too_late = (
self.last_pong_tm - self.last_ping_tm > self.ping_timeout
)
if (
self.last_ping_tm
and has_timeout_expired
and (
has_pong_not_arrived_after_last_ping
or has_pong_arrived_too_late
)
):
raise WebSocketTimeoutException("ping/pong timed out")
return True
def handleDisconnect(
e: Union[
WebSocketConnectionClosedException,
ConnectionRefusedError,
KeyboardInterrupt,
SystemExit,
Exception,
],
reconnecting: bool = False,
) -> bool:
self.has_errored = True
self._stop_ping_thread()
if not reconnecting:
self._callback(self.on_error, e)
if isinstance(e, (KeyboardInterrupt, SystemExit)):
teardown()
# Propagate further
raise
if reconnect:
_logging.info(f"{e} - reconnect")
if custom_dispatcher:
_logging.debug(
f"Calling custom dispatcher reconnect [{len(inspect.stack())} frames in stack]"
)
dispatcher.reconnect(reconnect, setSock)
else:
_logging.error(f"{e} - goodbye")
teardown()
custom_dispatcher = bool(dispatcher)
dispatcher = self.create_dispatcher(
ping_timeout, dispatcher, parse_url(self.url)[3]
)
try:
setSock()
if not custom_dispatcher and reconnect:
while self.keep_running:
_logging.debug(
f"Calling dispatcher reconnect [{len(inspect.stack())} frames in stack]"
)
dispatcher.reconnect(reconnect, setSock)
except (KeyboardInterrupt, Exception) as e:
_logging.info(f"tearing down on exception {e}")
teardown()
finally:
if not custom_dispatcher:
# Ensure teardown was called before returning from run_forever
teardown()
return self.has_errored
def create_dispatcher(
self,
ping_timeout: Union[float, int, None],
dispatcher: Optional[DispatcherBase] = None,
is_ssl: bool = False,
) -> Union[Dispatcher, SSLDispatcher, WrappedDispatcher]:
if dispatcher: # If custom dispatcher is set, use WrappedDispatcher
return WrappedDispatcher(self, ping_timeout, dispatcher)
timeout = ping_timeout or 10
if is_ssl:
return SSLDispatcher(self, timeout)
return Dispatcher(self, timeout)
def _get_close_args(self, close_frame: ABNF) -> list:
"""
_get_close_args extracts the close code and reason from the close body
if it exists (RFC6455 says WebSocket Connection Close Code is optional)
"""
# Need to catch the case where close_frame is None
# Otherwise the following if statement causes an error
if not self.on_close or not close_frame:
return [None, None]
# Extract close frame status code
if close_frame.data and len(close_frame.data) >= 2:
close_status_code = 256 * int(close_frame.data[0]) + int(
close_frame.data[1]
)
reason = close_frame.data[2:]
if isinstance(reason, bytes):
reason = reason.decode("utf-8")
return [close_status_code, reason]
else:
# Most likely reached this because len(close_frame_data.data) < 2
return [None, None]
def _callback(self, callback, *args) -> None:
if callback:
try:
callback(self, *args)
except Exception as e:
_logging.error(f"error from callback {callback}: {e}")
if self.on_error:
self.on_error(self, e)

View File

@@ -0,0 +1,75 @@
import http.cookies
from typing import Optional
"""
_cookiejar.py
websocket - WebSocket client library for Python
Copyright 2024 engn33r
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
class SimpleCookieJar:
def __init__(self) -> None:
self.jar: dict = {}
def add(self, set_cookie: Optional[str]) -> None:
if set_cookie:
simple_cookie = http.cookies.SimpleCookie(set_cookie)
for v in simple_cookie.values():
if domain := v.get("domain"):
if not domain.startswith("."):
domain = f".{domain}"
cookie = (
self.jar.get(domain)
if self.jar.get(domain)
else http.cookies.SimpleCookie()
)
cookie.update(simple_cookie)
self.jar[domain.lower()] = cookie
def set(self, set_cookie: str) -> None:
if set_cookie:
simple_cookie = http.cookies.SimpleCookie(set_cookie)
for v in simple_cookie.values():
if domain := v.get("domain"):
if not domain.startswith("."):
domain = f".{domain}"
self.jar[domain.lower()] = simple_cookie
def get(self, host: str) -> str:
if not host:
return ""
cookies = []
for domain, _ in self.jar.items():
host = host.lower()
if host.endswith(domain) or host == domain[1:]:
cookies.append(self.jar.get(domain))
return "; ".join(
filter(
None,
sorted(
[
f"{k}={v.value}"
for cookie in filter(None, cookies)
for k, v in cookie.items()
]
),
)
)

View File

@@ -0,0 +1,647 @@
import socket
import struct
import threading
import time
from typing import Optional, Union
# websocket modules
from ._abnf import ABNF, STATUS_NORMAL, continuous_frame, frame_buffer
from ._exceptions import WebSocketProtocolException, WebSocketConnectionClosedException
from ._handshake import SUPPORTED_REDIRECT_STATUSES, handshake
from ._http import connect, proxy_info
from ._logging import debug, error, trace, isEnabledForError, isEnabledForTrace
from ._socket import getdefaulttimeout, recv, send, sock_opt
from ._ssl_compat import ssl
from ._utils import NoLock
"""
_core.py
websocket - WebSocket client library for Python
Copyright 2024 engn33r
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
__all__ = ["WebSocket", "create_connection"]
class WebSocket:
"""
Low level WebSocket interface.
This class is based on the WebSocket protocol `draft-hixie-thewebsocketprotocol-76 <http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76>`_
We can connect to the websocket server and send/receive data.
The following example is an echo client.
>>> import websocket
>>> ws = websocket.WebSocket()
>>> ws.connect("ws://echo.websocket.events")
>>> ws.recv()
'echo.websocket.events sponsored by Lob.com'
>>> ws.send("Hello, Server")
19
>>> ws.recv()
'Hello, Server'
>>> ws.close()
Parameters
----------
get_mask_key: func
A callable function to get new mask keys, see the
WebSocket.set_mask_key's docstring for more information.
sockopt: tuple
Values for socket.setsockopt.
sockopt must be tuple and each element is argument of sock.setsockopt.
sslopt: dict
Optional dict object for ssl socket options. See FAQ for details.
fire_cont_frame: bool
Fire recv event for each cont frame. Default is False.
enable_multithread: bool
If set to True, lock send method.
skip_utf8_validation: bool
Skip utf8 validation.
"""
def __init__(
self,
get_mask_key=None,
sockopt=None,
sslopt=None,
fire_cont_frame: bool = False,
enable_multithread: bool = True,
skip_utf8_validation: bool = False,
**_,
):
"""
Initialize WebSocket object.
Parameters
----------
sslopt: dict
Optional dict object for ssl socket options. See FAQ for details.
"""
self.sock_opt = sock_opt(sockopt, sslopt)
self.handshake_response = None
self.sock: Optional[socket.socket] = None
self.connected = False
self.get_mask_key = get_mask_key
# These buffer over the build-up of a single frame.
self.frame_buffer = frame_buffer(self._recv, skip_utf8_validation)
self.cont_frame = continuous_frame(fire_cont_frame, skip_utf8_validation)
if enable_multithread:
self.lock = threading.Lock()
self.readlock = threading.Lock()
else:
self.lock = NoLock()
self.readlock = NoLock()
def __iter__(self):
"""
Allow iteration over websocket, implying sequential `recv` executions.
"""
while True:
yield self.recv()
def __next__(self):
return self.recv()
def next(self):
return self.__next__()
def fileno(self):
return self.sock.fileno()
def set_mask_key(self, func):
"""
Set function to create mask key. You can customize mask key generator.
Mainly, this is for testing purpose.
Parameters
----------
func: func
callable object. the func takes 1 argument as integer.
The argument means length of mask key.
This func must return string(byte array),
which length is argument specified.
"""
self.get_mask_key = func
def gettimeout(self) -> Union[float, int, None]:
"""
Get the websocket timeout (in seconds) as an int or float
Returns
----------
timeout: int or float
returns timeout value (in seconds). This value could be either float/integer.
"""
return self.sock_opt.timeout
def settimeout(self, timeout: Union[float, int, None]):
"""
Set the timeout to the websocket.
Parameters
----------
timeout: int or float
timeout time (in seconds). This value could be either float/integer.
"""
self.sock_opt.timeout = timeout
if self.sock:
self.sock.settimeout(timeout)
timeout = property(gettimeout, settimeout)
def getsubprotocol(self):
"""
Get subprotocol
"""
if self.handshake_response:
return self.handshake_response.subprotocol
else:
return None
subprotocol = property(getsubprotocol)
def getstatus(self):
"""
Get handshake status
"""
if self.handshake_response:
return self.handshake_response.status
else:
return None
status = property(getstatus)
def getheaders(self):
"""
Get handshake response header
"""
if self.handshake_response:
return self.handshake_response.headers
else:
return None
def is_ssl(self):
try:
return isinstance(self.sock, ssl.SSLSocket)
except:
return False
headers = property(getheaders)
def connect(self, url, **options):
"""
Connect to url. url is websocket url scheme.
ie. ws://host:port/resource
You can customize using 'options'.
If you set "header" list object, you can set your own custom header.
>>> ws = WebSocket()
>>> ws.connect("ws://echo.websocket.events",
... header=["User-Agent: MyProgram",
... "x-custom: header"])
Parameters
----------
header: list or dict
Custom http header list or dict.
cookie: str
Cookie value.
origin: str
Custom origin url.
connection: str
Custom connection header value.
Default value "Upgrade" set in _handshake.py
suppress_origin: bool
Suppress outputting origin header.
host: str
Custom host header string.
timeout: int or float
Socket timeout time. This value is an integer or float.
If you set None for this value, it means "use default_timeout value"
http_proxy_host: str
HTTP proxy host name.
http_proxy_port: str or int
HTTP proxy port. Default is 80.
http_no_proxy: list
Whitelisted host names that don't use the proxy.
http_proxy_auth: tuple
HTTP proxy auth information. Tuple of username and password. Default is None.
http_proxy_timeout: int or float
HTTP proxy timeout, default is 60 sec as per python-socks.
redirect_limit: int
Number of redirects to follow.
subprotocols: list
List of available subprotocols. Default is None.
socket: socket
Pre-initialized stream socket.
"""
self.sock_opt.timeout = options.get("timeout", self.sock_opt.timeout)
self.sock, addrs = connect(
url, self.sock_opt, proxy_info(**options), options.pop("socket", None)
)
try:
self.handshake_response = handshake(self.sock, url, *addrs, **options)
for _ in range(options.pop("redirect_limit", 3)):
if self.handshake_response.status in SUPPORTED_REDIRECT_STATUSES:
url = self.handshake_response.headers["location"]
self.sock.close()
self.sock, addrs = connect(
url,
self.sock_opt,
proxy_info(**options),
options.pop("socket", None),
)
self.handshake_response = handshake(
self.sock, url, *addrs, **options
)
self.connected = True
except:
if self.sock:
self.sock.close()
self.sock = None
raise
def send(self, payload: Union[bytes, str], opcode: int = ABNF.OPCODE_TEXT) -> int:
"""
Send the data as string.
Parameters
----------
payload: str
Payload must be utf-8 string or unicode,
If the opcode is OPCODE_TEXT.
Otherwise, it must be string(byte array).
opcode: int
Operation code (opcode) to send.
"""
frame = ABNF.create_frame(payload, opcode)
return self.send_frame(frame)
def send_text(self, text_data: str) -> int:
"""
Sends UTF-8 encoded text.
"""
return self.send(text_data, ABNF.OPCODE_TEXT)
def send_bytes(self, data: Union[bytes, bytearray]) -> int:
"""
Sends a sequence of bytes.
"""
return self.send(data, ABNF.OPCODE_BINARY)
def send_frame(self, frame) -> int:
"""
Send the data frame.
>>> ws = create_connection("ws://echo.websocket.events")
>>> frame = ABNF.create_frame("Hello", ABNF.OPCODE_TEXT)
>>> ws.send_frame(frame)
>>> cont_frame = ABNF.create_frame("My name is ", ABNF.OPCODE_CONT, 0)
>>> ws.send_frame(frame)
>>> cont_frame = ABNF.create_frame("Foo Bar", ABNF.OPCODE_CONT, 1)
>>> ws.send_frame(frame)
Parameters
----------
frame: ABNF frame
frame data created by ABNF.create_frame
"""
if self.get_mask_key:
frame.get_mask_key = self.get_mask_key
data = frame.format()
length = len(data)
if isEnabledForTrace():
trace(f"++Sent raw: {repr(data)}")
trace(f"++Sent decoded: {frame.__str__()}")
with self.lock:
while data:
l = self._send(data)
data = data[l:]
return length
def send_binary(self, payload: bytes) -> int:
"""
Send a binary message (OPCODE_BINARY).
Parameters
----------
payload: bytes
payload of message to send.
"""
return self.send(payload, ABNF.OPCODE_BINARY)
def ping(self, payload: Union[str, bytes] = ""):
"""
Send ping data.
Parameters
----------
payload: str
data payload to send server.
"""
if isinstance(payload, str):
payload = payload.encode("utf-8")
self.send(payload, ABNF.OPCODE_PING)
def pong(self, payload: Union[str, bytes] = ""):
"""
Send pong data.
Parameters
----------
payload: str
data payload to send server.
"""
if isinstance(payload, str):
payload = payload.encode("utf-8")
self.send(payload, ABNF.OPCODE_PONG)
def recv(self) -> Union[str, bytes]:
"""
Receive string data(byte array) from the server.
Returns
----------
data: string (byte array) value.
"""
with self.readlock:
opcode, data = self.recv_data()
if opcode == ABNF.OPCODE_TEXT:
data_received: Union[bytes, str] = data
if isinstance(data_received, bytes):
return data_received.decode("utf-8")
elif isinstance(data_received, str):
return data_received
elif opcode == ABNF.OPCODE_BINARY:
data_binary: bytes = data
return data_binary
else:
return ""
def recv_data(self, control_frame: bool = False) -> tuple:
"""
Receive data with operation code.
Parameters
----------
control_frame: bool
a boolean flag indicating whether to return control frame
data, defaults to False
Returns
-------
opcode, frame.data: tuple
tuple of operation code and string(byte array) value.
"""
opcode, frame = self.recv_data_frame(control_frame)
return opcode, frame.data
def recv_data_frame(self, control_frame: bool = False) -> tuple:
"""
Receive data with operation code.
If a valid ping message is received, a pong response is sent.
Parameters
----------
control_frame: bool
a boolean flag indicating whether to return control frame
data, defaults to False
Returns
-------
frame.opcode, frame: tuple
tuple of operation code and string(byte array) value.
"""
while True:
frame = self.recv_frame()
if isEnabledForTrace():
trace(f"++Rcv raw: {repr(frame.format())}")
trace(f"++Rcv decoded: {frame.__str__()}")
if not frame:
# handle error:
# 'NoneType' object has no attribute 'opcode'
raise WebSocketProtocolException(f"Not a valid frame {frame}")
elif frame.opcode in (
ABNF.OPCODE_TEXT,
ABNF.OPCODE_BINARY,
ABNF.OPCODE_CONT,
):
self.cont_frame.validate(frame)
self.cont_frame.add(frame)
if self.cont_frame.is_fire(frame):
return self.cont_frame.extract(frame)
elif frame.opcode == ABNF.OPCODE_CLOSE:
self.send_close()
return frame.opcode, frame
elif frame.opcode == ABNF.OPCODE_PING:
if len(frame.data) < 126:
self.pong(frame.data)
else:
raise WebSocketProtocolException("Ping message is too long")
if control_frame:
return frame.opcode, frame
elif frame.opcode == ABNF.OPCODE_PONG:
if control_frame:
return frame.opcode, frame
def recv_frame(self):
"""
Receive data as frame from server.
Returns
-------
self.frame_buffer.recv_frame(): ABNF frame object
"""
return self.frame_buffer.recv_frame()
def send_close(self, status: int = STATUS_NORMAL, reason: bytes = b""):
"""
Send close data to the server.
Parameters
----------
status: int
Status code to send. See STATUS_XXX.
reason: str or bytes
The reason to close. This must be string or UTF-8 bytes.
"""
if status < 0 or status >= ABNF.LENGTH_16:
raise ValueError("code is invalid range")
self.connected = False
self.send(struct.pack("!H", status) + reason, ABNF.OPCODE_CLOSE)
def close(self, status: int = STATUS_NORMAL, reason: bytes = b"", timeout: int = 3):
"""
Close Websocket object
Parameters
----------
status: int
Status code to send. See VALID_CLOSE_STATUS in ABNF.
reason: bytes
The reason to close in UTF-8.
timeout: int or float
Timeout until receive a close frame.
If None, it will wait forever until receive a close frame.
"""
if not self.connected:
return
if status < 0 or status >= ABNF.LENGTH_16:
raise ValueError("code is invalid range")
try:
self.connected = False
self.send(struct.pack("!H", status) + reason, ABNF.OPCODE_CLOSE)
sock_timeout = self.sock.gettimeout()
self.sock.settimeout(timeout)
start_time = time.time()
while timeout is None or time.time() - start_time < timeout:
try:
frame = self.recv_frame()
if frame.opcode != ABNF.OPCODE_CLOSE:
continue
if isEnabledForError():
recv_status = struct.unpack("!H", frame.data[0:2])[0]
if recv_status >= 3000 and recv_status <= 4999:
debug(f"close status: {repr(recv_status)}")
elif recv_status != STATUS_NORMAL:
error(f"close status: {repr(recv_status)}")
break
except:
break
self.sock.settimeout(sock_timeout)
self.sock.shutdown(socket.SHUT_RDWR)
except:
pass
self.shutdown()
def abort(self):
"""
Low-level asynchronous abort, wakes up other threads that are waiting in recv_*
"""
if self.connected:
self.sock.shutdown(socket.SHUT_RDWR)
def shutdown(self):
"""
close socket, immediately.
"""
if self.sock:
self.sock.close()
self.sock = None
self.connected = False
def _send(self, data: Union[str, bytes]):
return send(self.sock, data)
def _recv(self, bufsize):
try:
return recv(self.sock, bufsize)
except WebSocketConnectionClosedException:
if self.sock:
self.sock.close()
self.sock = None
self.connected = False
raise
def create_connection(url: str, timeout=None, class_=WebSocket, **options):
"""
Connect to url and return websocket object.
Connect to url and return the WebSocket object.
Passing optional timeout parameter will set the timeout on the socket.
If no timeout is supplied,
the global default timeout setting returned by getdefaulttimeout() is used.
You can customize using 'options'.
If you set "header" list object, you can set your own custom header.
>>> conn = create_connection("ws://echo.websocket.events",
... header=["User-Agent: MyProgram",
... "x-custom: header"])
Parameters
----------
class_: class
class to instantiate when creating the connection. It has to implement
settimeout and connect. It's __init__ should be compatible with
WebSocket.__init__, i.e. accept all of it's kwargs.
header: list or dict
custom http header list or dict.
cookie: str
Cookie value.
origin: str
custom origin url.
suppress_origin: bool
suppress outputting origin header.
host: str
custom host header string.
timeout: int or float
socket timeout time. This value could be either float/integer.
If set to None, it uses the default_timeout value.
http_proxy_host: str
HTTP proxy host name.
http_proxy_port: str or int
HTTP proxy port. If not set, set to 80.
http_no_proxy: list
Whitelisted host names that don't use the proxy.
http_proxy_auth: tuple
HTTP proxy auth information. tuple of username and password. Default is None.
http_proxy_timeout: int or float
HTTP proxy timeout, default is 60 sec as per python-socks.
enable_multithread: bool
Enable lock for multithread.
redirect_limit: int
Number of redirects to follow.
sockopt: tuple
Values for socket.setsockopt.
sockopt must be a tuple and each element is an argument of sock.setsockopt.
sslopt: dict
Optional dict object for ssl socket options. See FAQ for details.
subprotocols: list
List of available subprotocols. Default is None.
skip_utf8_validation: bool
Skip utf8 validation.
socket: socket
Pre-initialized stream socket.
"""
sockopt = options.pop("sockopt", [])
sslopt = options.pop("sslopt", {})
fire_cont_frame = options.pop("fire_cont_frame", False)
enable_multithread = options.pop("enable_multithread", True)
skip_utf8_validation = options.pop("skip_utf8_validation", False)
websock = class_(
sockopt=sockopt,
sslopt=sslopt,
fire_cont_frame=fire_cont_frame,
enable_multithread=enable_multithread,
skip_utf8_validation=skip_utf8_validation,
**options,
)
websock.settimeout(timeout if timeout is not None else getdefaulttimeout())
websock.connect(url, **options)
return websock

View File

@@ -0,0 +1,94 @@
"""
_exceptions.py
websocket - WebSocket client library for Python
Copyright 2024 engn33r
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
class WebSocketException(Exception):
"""
WebSocket exception class.
"""
pass
class WebSocketProtocolException(WebSocketException):
"""
If the WebSocket protocol is invalid, this exception will be raised.
"""
pass
class WebSocketPayloadException(WebSocketException):
"""
If the WebSocket payload is invalid, this exception will be raised.
"""
pass
class WebSocketConnectionClosedException(WebSocketException):
"""
If remote host closed the connection or some network error happened,
this exception will be raised.
"""
pass
class WebSocketTimeoutException(WebSocketException):
"""
WebSocketTimeoutException will be raised at socket timeout during read/write data.
"""
pass
class WebSocketProxyException(WebSocketException):
"""
WebSocketProxyException will be raised when proxy error occurred.
"""
pass
class WebSocketBadStatusException(WebSocketException):
"""
WebSocketBadStatusException will be raised when we get bad handshake status code.
"""
def __init__(
self,
message: str,
status_code: int,
status_message=None,
resp_headers=None,
resp_body=None,
):
super().__init__(message)
self.status_code = status_code
self.resp_headers = resp_headers
self.resp_body = resp_body
class WebSocketAddressException(WebSocketException):
"""
If the websocket address info cannot be found, this exception will be raised.
"""
pass

View File

@@ -0,0 +1,203 @@
"""
_handshake.py
websocket - WebSocket client library for Python
Copyright 2024 engn33r
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import hashlib
import hmac
import os
from base64 import encodebytes as base64encode
from http import HTTPStatus
from ._cookiejar import SimpleCookieJar
from ._exceptions import WebSocketException, WebSocketBadStatusException
from ._http import read_headers
from ._logging import dump, error
from ._socket import send
__all__ = ["handshake_response", "handshake", "SUPPORTED_REDIRECT_STATUSES"]
# websocket supported version.
VERSION = 13
SUPPORTED_REDIRECT_STATUSES = (
HTTPStatus.MOVED_PERMANENTLY,
HTTPStatus.FOUND,
HTTPStatus.SEE_OTHER,
HTTPStatus.TEMPORARY_REDIRECT,
HTTPStatus.PERMANENT_REDIRECT,
)
SUCCESS_STATUSES = SUPPORTED_REDIRECT_STATUSES + (HTTPStatus.SWITCHING_PROTOCOLS,)
CookieJar = SimpleCookieJar()
class handshake_response:
def __init__(self, status: int, headers: dict, subprotocol):
self.status = status
self.headers = headers
self.subprotocol = subprotocol
CookieJar.add(headers.get("set-cookie"))
def handshake(
sock, url: str, hostname: str, port: int, resource: str, **options
) -> handshake_response:
headers, key = _get_handshake_headers(resource, url, hostname, port, options)
header_str = "\r\n".join(headers)
send(sock, header_str)
dump("request header", header_str)
status, resp = _get_resp_headers(sock)
if status in SUPPORTED_REDIRECT_STATUSES:
return handshake_response(status, resp, None)
success, subproto = _validate(resp, key, options.get("subprotocols"))
if not success:
raise WebSocketException("Invalid WebSocket Header")
return handshake_response(status, resp, subproto)
def _pack_hostname(hostname: str) -> str:
# IPv6 address
if ":" in hostname:
return f"[{hostname}]"
return hostname
def _get_handshake_headers(
resource: str, url: str, host: str, port: int, options: dict
) -> tuple:
headers = [f"GET {resource} HTTP/1.1", "Upgrade: websocket"]
if port in [80, 443]:
hostport = _pack_hostname(host)
else:
hostport = f"{_pack_hostname(host)}:{port}"
if options.get("host"):
headers.append(f'Host: {options["host"]}')
else:
headers.append(f"Host: {hostport}")
# scheme indicates whether http or https is used in Origin
# The same approach is used in parse_url of _url.py to set default port
scheme, url = url.split(":", 1)
if not options.get("suppress_origin"):
if "origin" in options and options["origin"] is not None:
headers.append(f'Origin: {options["origin"]}')
elif scheme == "wss":
headers.append(f"Origin: https://{hostport}")
else:
headers.append(f"Origin: http://{hostport}")
key = _create_sec_websocket_key()
# Append Sec-WebSocket-Key & Sec-WebSocket-Version if not manually specified
if not options.get("header") or "Sec-WebSocket-Key" not in options["header"]:
headers.append(f"Sec-WebSocket-Key: {key}")
else:
key = options["header"]["Sec-WebSocket-Key"]
if not options.get("header") or "Sec-WebSocket-Version" not in options["header"]:
headers.append(f"Sec-WebSocket-Version: {VERSION}")
if not options.get("connection"):
headers.append("Connection: Upgrade")
else:
headers.append(options["connection"])
if subprotocols := options.get("subprotocols"):
headers.append(f'Sec-WebSocket-Protocol: {",".join(subprotocols)}')
if header := options.get("header"):
if isinstance(header, dict):
header = [": ".join([k, v]) for k, v in header.items() if v is not None]
headers.extend(header)
server_cookie = CookieJar.get(host)
client_cookie = options.get("cookie", None)
if cookie := "; ".join(filter(None, [server_cookie, client_cookie])):
headers.append(f"Cookie: {cookie}")
headers.extend(("", ""))
return headers, key
def _get_resp_headers(sock, success_statuses: tuple = SUCCESS_STATUSES) -> tuple:
status, resp_headers, status_message = read_headers(sock)
if status not in success_statuses:
content_len = resp_headers.get("content-length")
if content_len:
response_body = sock.recv(
int(content_len)
) # read the body of the HTTP error message response and include it in the exception
else:
response_body = None
raise WebSocketBadStatusException(
f"Handshake status {status} {status_message} -+-+- {resp_headers} -+-+- {response_body}",
status,
status_message,
resp_headers,
response_body,
)
return status, resp_headers
_HEADERS_TO_CHECK = {
"upgrade": "websocket",
"connection": "upgrade",
}
def _validate(headers, key: str, subprotocols) -> tuple:
subproto = None
for k, v in _HEADERS_TO_CHECK.items():
r = headers.get(k, None)
if not r:
return False, None
r = [x.strip().lower() for x in r.split(",")]
if v not in r:
return False, None
if subprotocols:
subproto = headers.get("sec-websocket-protocol", None)
if not subproto or subproto.lower() not in [s.lower() for s in subprotocols]:
error(f"Invalid subprotocol: {subprotocols}")
return False, None
subproto = subproto.lower()
result = headers.get("sec-websocket-accept", None)
if not result:
return False, None
result = result.lower()
if isinstance(result, str):
result = result.encode("utf-8")
value = f"{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11".encode("utf-8")
hashed = base64encode(hashlib.sha1(value).digest()).strip().lower()
if hmac.compare_digest(hashed, result):
return True, subproto
else:
return False, None
def _create_sec_websocket_key() -> str:
randomness = os.urandom(16)
return base64encode(randomness).decode("utf-8").strip()

View File

@@ -0,0 +1,374 @@
"""
_http.py
websocket - WebSocket client library for Python
Copyright 2024 engn33r
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import errno
import os
import socket
from base64 import encodebytes as base64encode
from ._exceptions import (
WebSocketAddressException,
WebSocketException,
WebSocketProxyException,
)
from ._logging import debug, dump, trace
from ._socket import DEFAULT_SOCKET_OPTION, recv_line, send
from ._ssl_compat import HAVE_SSL, ssl
from ._url import get_proxy_info, parse_url
__all__ = ["proxy_info", "connect", "read_headers"]
try:
from python_socks._errors import *
from python_socks._types import ProxyType
from python_socks.sync import Proxy
HAVE_PYTHON_SOCKS = True
except:
HAVE_PYTHON_SOCKS = False
class ProxyError(Exception):
pass
class ProxyTimeoutError(Exception):
pass
class ProxyConnectionError(Exception):
pass
class proxy_info:
def __init__(self, **options):
self.proxy_host = options.get("http_proxy_host", None)
if self.proxy_host:
self.proxy_port = options.get("http_proxy_port", 0)
self.auth = options.get("http_proxy_auth", None)
self.no_proxy = options.get("http_no_proxy", None)
self.proxy_protocol = options.get("proxy_type", "http")
# Note: If timeout not specified, default python-socks timeout is 60 seconds
self.proxy_timeout = options.get("http_proxy_timeout", None)
if self.proxy_protocol not in [
"http",
"socks4",
"socks4a",
"socks5",
"socks5h",
]:
raise ProxyError(
"Only http, socks4, socks5 proxy protocols are supported"
)
else:
self.proxy_port = 0
self.auth = None
self.no_proxy = None
self.proxy_protocol = "http"
def _start_proxied_socket(url: str, options, proxy) -> tuple:
if not HAVE_PYTHON_SOCKS:
raise WebSocketException(
"Python Socks is needed for SOCKS proxying but is not available"
)
hostname, port, resource, is_secure = parse_url(url)
if proxy.proxy_protocol == "socks4":
rdns = False
proxy_type = ProxyType.SOCKS4
# socks4a sends DNS through proxy
elif proxy.proxy_protocol == "socks4a":
rdns = True
proxy_type = ProxyType.SOCKS4
elif proxy.proxy_protocol == "socks5":
rdns = False
proxy_type = ProxyType.SOCKS5
# socks5h sends DNS through proxy
elif proxy.proxy_protocol == "socks5h":
rdns = True
proxy_type = ProxyType.SOCKS5
ws_proxy = Proxy.create(
proxy_type=proxy_type,
host=proxy.proxy_host,
port=int(proxy.proxy_port),
username=proxy.auth[0] if proxy.auth else None,
password=proxy.auth[1] if proxy.auth else None,
rdns=rdns,
)
sock = ws_proxy.connect(hostname, port, timeout=proxy.proxy_timeout)
if is_secure:
if HAVE_SSL:
sock = _ssl_socket(sock, options.sslopt, hostname)
else:
raise WebSocketException("SSL not available.")
return sock, (hostname, port, resource)
def connect(url: str, options, proxy, socket):
# Use _start_proxied_socket() only for socks4 or socks5 proxy
# Use _tunnel() for http proxy
# TODO: Use python-socks for http protocol also, to standardize flow
if proxy.proxy_host and not socket and proxy.proxy_protocol != "http":
return _start_proxied_socket(url, options, proxy)
hostname, port_from_url, resource, is_secure = parse_url(url)
if socket:
return socket, (hostname, port_from_url, resource)
addrinfo_list, need_tunnel, auth = _get_addrinfo_list(
hostname, port_from_url, is_secure, proxy
)
if not addrinfo_list:
raise WebSocketException(f"Host not found.: {hostname}:{port_from_url}")
sock = None
try:
sock = _open_socket(addrinfo_list, options.sockopt, options.timeout)
if need_tunnel:
sock = _tunnel(sock, hostname, port_from_url, auth)
if is_secure:
if HAVE_SSL:
sock = _ssl_socket(sock, options.sslopt, hostname)
else:
raise WebSocketException("SSL not available.")
return sock, (hostname, port_from_url, resource)
except:
if sock:
sock.close()
raise
def _get_addrinfo_list(hostname, port: int, is_secure: bool, proxy) -> tuple:
phost, pport, pauth = get_proxy_info(
hostname,
is_secure,
proxy.proxy_host,
proxy.proxy_port,
proxy.auth,
proxy.no_proxy,
)
try:
# when running on windows 10, getaddrinfo without socktype returns a socktype 0.
# This generates an error exception: `_on_error: exception Socket type must be stream or datagram, not 0`
# or `OSError: [Errno 22] Invalid argument` when creating socket. Force the socket type to SOCK_STREAM.
if not phost:
addrinfo_list = socket.getaddrinfo(
hostname, port, 0, socket.SOCK_STREAM, socket.SOL_TCP
)
return addrinfo_list, False, None
else:
pport = pport and pport or 80
# when running on windows 10, the getaddrinfo used above
# returns a socktype 0. This generates an error exception:
# _on_error: exception Socket type must be stream or datagram, not 0
# Force the socket type to SOCK_STREAM
addrinfo_list = socket.getaddrinfo(
phost, pport, 0, socket.SOCK_STREAM, socket.SOL_TCP
)
return addrinfo_list, True, pauth
except socket.gaierror as e:
raise WebSocketAddressException(e)
def _open_socket(addrinfo_list, sockopt, timeout):
err = None
for addrinfo in addrinfo_list:
family, socktype, proto = addrinfo[:3]
sock = socket.socket(family, socktype, proto)
sock.settimeout(timeout)
for opts in DEFAULT_SOCKET_OPTION:
sock.setsockopt(*opts)
for opts in sockopt:
sock.setsockopt(*opts)
address = addrinfo[4]
err = None
while not err:
try:
sock.connect(address)
except socket.error as error:
sock.close()
error.remote_ip = str(address[0])
try:
eConnRefused = (
errno.ECONNREFUSED,
errno.WSAECONNREFUSED,
errno.ENETUNREACH,
)
except AttributeError:
eConnRefused = (errno.ECONNREFUSED, errno.ENETUNREACH)
if error.errno not in eConnRefused:
raise error
err = error
continue
else:
break
else:
continue
break
else:
if err:
raise err
return sock
def _wrap_sni_socket(sock: socket.socket, sslopt: dict, hostname, check_hostname):
context = sslopt.get("context", None)
if not context:
context = ssl.SSLContext(sslopt.get("ssl_version", ssl.PROTOCOL_TLS_CLIENT))
# Non default context need to manually enable SSLKEYLOGFILE support by setting the keylog_filename attribute.
# For more details see also:
# * https://docs.python.org/3.8/library/ssl.html?highlight=sslkeylogfile#context-creation
# * https://docs.python.org/3.8/library/ssl.html?highlight=sslkeylogfile#ssl.SSLContext.keylog_filename
context.keylog_filename = os.environ.get("SSLKEYLOGFILE", None)
if sslopt.get("cert_reqs", ssl.CERT_NONE) != ssl.CERT_NONE:
cafile = sslopt.get("ca_certs", None)
capath = sslopt.get("ca_cert_path", None)
if cafile or capath:
context.load_verify_locations(cafile=cafile, capath=capath)
elif hasattr(context, "load_default_certs"):
context.load_default_certs(ssl.Purpose.SERVER_AUTH)
if sslopt.get("certfile", None):
context.load_cert_chain(
sslopt["certfile"],
sslopt.get("keyfile", None),
sslopt.get("password", None),
)
# Python 3.10 switch to PROTOCOL_TLS_CLIENT defaults to "cert_reqs = ssl.CERT_REQUIRED" and "check_hostname = True"
# If both disabled, set check_hostname before verify_mode
# see https://github.com/liris/websocket-client/commit/b96a2e8fa765753e82eea531adb19716b52ca3ca#commitcomment-10803153
if sslopt.get("cert_reqs", ssl.CERT_NONE) == ssl.CERT_NONE and not sslopt.get(
"check_hostname", False
):
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
else:
context.check_hostname = sslopt.get("check_hostname", True)
context.verify_mode = sslopt.get("cert_reqs", ssl.CERT_REQUIRED)
if "ciphers" in sslopt:
context.set_ciphers(sslopt["ciphers"])
if "cert_chain" in sslopt:
certfile, keyfile, password = sslopt["cert_chain"]
context.load_cert_chain(certfile, keyfile, password)
if "ecdh_curve" in sslopt:
context.set_ecdh_curve(sslopt["ecdh_curve"])
return context.wrap_socket(
sock,
do_handshake_on_connect=sslopt.get("do_handshake_on_connect", True),
suppress_ragged_eofs=sslopt.get("suppress_ragged_eofs", True),
server_hostname=hostname,
)
def _ssl_socket(sock: socket.socket, user_sslopt: dict, hostname):
sslopt: dict = {"cert_reqs": ssl.CERT_REQUIRED}
sslopt.update(user_sslopt)
cert_path = os.environ.get("WEBSOCKET_CLIENT_CA_BUNDLE")
if (
cert_path
and os.path.isfile(cert_path)
and user_sslopt.get("ca_certs", None) is None
):
sslopt["ca_certs"] = cert_path
elif (
cert_path
and os.path.isdir(cert_path)
and user_sslopt.get("ca_cert_path", None) is None
):
sslopt["ca_cert_path"] = cert_path
if sslopt.get("server_hostname", None):
hostname = sslopt["server_hostname"]
check_hostname = sslopt.get("check_hostname", True)
sock = _wrap_sni_socket(sock, sslopt, hostname, check_hostname)
return sock
def _tunnel(sock: socket.socket, host, port: int, auth) -> socket.socket:
debug("Connecting proxy...")
connect_header = f"CONNECT {host}:{port} HTTP/1.1\r\n"
connect_header += f"Host: {host}:{port}\r\n"
# TODO: support digest auth.
if auth and auth[0]:
auth_str = auth[0]
if auth[1]:
auth_str += f":{auth[1]}"
encoded_str = base64encode(auth_str.encode()).strip().decode().replace("\n", "")
connect_header += f"Proxy-Authorization: Basic {encoded_str}\r\n"
connect_header += "\r\n"
dump("request header", connect_header)
send(sock, connect_header)
try:
status, _, _ = read_headers(sock)
except Exception as e:
raise WebSocketProxyException(str(e))
if status != 200:
raise WebSocketProxyException(f"failed CONNECT via proxy status: {status}")
return sock
def read_headers(sock: socket.socket) -> tuple:
status = None
status_message = None
headers: dict = {}
trace("--- response header ---")
while True:
line = recv_line(sock)
line = line.decode("utf-8").strip()
if not line:
break
trace(line)
if not status:
status_info = line.split(" ", 2)
status = int(status_info[1])
if len(status_info) > 2:
status_message = status_info[2]
else:
kv = line.split(":", 1)
if len(kv) != 2:
raise WebSocketException("Invalid header")
key, value = kv
if key.lower() == "set-cookie" and headers.get("set-cookie"):
headers["set-cookie"] = headers.get("set-cookie") + "; " + value.strip()
else:
headers[key.lower()] = value.strip()
trace("-----------------------")
return status, headers, status_message

View File

@@ -0,0 +1,106 @@
import logging
"""
_logging.py
websocket - WebSocket client library for Python
Copyright 2024 engn33r
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
_logger = logging.getLogger("websocket")
try:
from logging import NullHandler
except ImportError:
class NullHandler(logging.Handler):
def emit(self, record) -> None:
pass
_logger.addHandler(NullHandler())
_traceEnabled = False
__all__ = [
"enableTrace",
"dump",
"error",
"warning",
"debug",
"trace",
"isEnabledForError",
"isEnabledForDebug",
"isEnabledForTrace",
]
def enableTrace(
traceable: bool,
handler: logging.StreamHandler = logging.StreamHandler(),
level: str = "DEBUG",
) -> None:
"""
Turn on/off the traceability.
Parameters
----------
traceable: bool
If set to True, traceability is enabled.
"""
global _traceEnabled
_traceEnabled = traceable
if traceable:
_logger.addHandler(handler)
_logger.setLevel(getattr(logging, level))
def dump(title: str, message: str) -> None:
if _traceEnabled:
_logger.debug(f"--- {title} ---")
_logger.debug(message)
_logger.debug("-----------------------")
def error(msg: str) -> None:
_logger.error(msg)
def warning(msg: str) -> None:
_logger.warning(msg)
def debug(msg: str) -> None:
_logger.debug(msg)
def info(msg: str) -> None:
_logger.info(msg)
def trace(msg: str) -> None:
if _traceEnabled:
_logger.debug(msg)
def isEnabledForError() -> bool:
return _logger.isEnabledFor(logging.ERROR)
def isEnabledForDebug() -> bool:
return _logger.isEnabledFor(logging.DEBUG)
def isEnabledForTrace() -> bool:
return _traceEnabled

View File

@@ -0,0 +1,188 @@
import errno
import selectors
import socket
from typing import Union
from ._exceptions import (
WebSocketConnectionClosedException,
WebSocketTimeoutException,
)
from ._ssl_compat import SSLError, SSLWantReadError, SSLWantWriteError
from ._utils import extract_error_code, extract_err_message
"""
_socket.py
websocket - WebSocket client library for Python
Copyright 2024 engn33r
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
DEFAULT_SOCKET_OPTION = [(socket.SOL_TCP, socket.TCP_NODELAY, 1)]
if hasattr(socket, "SO_KEEPALIVE"):
DEFAULT_SOCKET_OPTION.append((socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1))
if hasattr(socket, "TCP_KEEPIDLE"):
DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPIDLE, 30))
if hasattr(socket, "TCP_KEEPINTVL"):
DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPINTVL, 10))
if hasattr(socket, "TCP_KEEPCNT"):
DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPCNT, 3))
_default_timeout = None
__all__ = [
"DEFAULT_SOCKET_OPTION",
"sock_opt",
"setdefaulttimeout",
"getdefaulttimeout",
"recv",
"recv_line",
"send",
]
class sock_opt:
def __init__(self, sockopt: list, sslopt: dict) -> None:
if sockopt is None:
sockopt = []
if sslopt is None:
sslopt = {}
self.sockopt = sockopt
self.sslopt = sslopt
self.timeout = None
def setdefaulttimeout(timeout: Union[int, float, None]) -> None:
"""
Set the global timeout setting to connect.
Parameters
----------
timeout: int or float
default socket timeout time (in seconds)
"""
global _default_timeout
_default_timeout = timeout
def getdefaulttimeout() -> Union[int, float, None]:
"""
Get default timeout
Returns
----------
_default_timeout: int or float
Return the global timeout setting (in seconds) to connect.
"""
return _default_timeout
def recv(sock: socket.socket, bufsize: int) -> bytes:
if not sock:
raise WebSocketConnectionClosedException("socket is already closed.")
def _recv():
try:
return sock.recv(bufsize)
except SSLWantReadError:
pass
except socket.error as exc:
error_code = extract_error_code(exc)
if error_code not in [errno.EAGAIN, errno.EWOULDBLOCK]:
raise
sel = selectors.DefaultSelector()
sel.register(sock, selectors.EVENT_READ)
r = sel.select(sock.gettimeout())
sel.close()
if r:
return sock.recv(bufsize)
try:
if sock.gettimeout() == 0:
bytes_ = sock.recv(bufsize)
else:
bytes_ = _recv()
except TimeoutError:
raise WebSocketTimeoutException("Connection timed out")
except socket.timeout as e:
message = extract_err_message(e)
raise WebSocketTimeoutException(message)
except SSLError as e:
message = extract_err_message(e)
if isinstance(message, str) and "timed out" in message:
raise WebSocketTimeoutException(message)
else:
raise
if not bytes_:
raise WebSocketConnectionClosedException("Connection to remote host was lost.")
return bytes_
def recv_line(sock: socket.socket) -> bytes:
line = []
while True:
c = recv(sock, 1)
line.append(c)
if c == b"\n":
break
return b"".join(line)
def send(sock: socket.socket, data: Union[bytes, str]) -> int:
if isinstance(data, str):
data = data.encode("utf-8")
if not sock:
raise WebSocketConnectionClosedException("socket is already closed.")
def _send():
try:
return sock.send(data)
except SSLWantWriteError:
pass
except socket.error as exc:
error_code = extract_error_code(exc)
if error_code is None:
raise
if error_code not in [errno.EAGAIN, errno.EWOULDBLOCK]:
raise
sel = selectors.DefaultSelector()
sel.register(sock, selectors.EVENT_WRITE)
w = sel.select(sock.gettimeout())
sel.close()
if w:
return sock.send(data)
try:
if sock.gettimeout() == 0:
return sock.send(data)
else:
return _send()
except socket.timeout as e:
message = extract_err_message(e)
raise WebSocketTimeoutException(message)
except Exception as e:
message = extract_err_message(e)
if isinstance(message, str) and "timed out" in message:
raise WebSocketTimeoutException(message)
else:
raise

View File

@@ -0,0 +1,49 @@
"""
_ssl_compat.py
websocket - WebSocket client library for Python
Copyright 2024 engn33r
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
__all__ = [
"HAVE_SSL",
"ssl",
"SSLError",
"SSLEOFError",
"SSLWantReadError",
"SSLWantWriteError",
]
try:
import ssl
from ssl import SSLError, SSLEOFError, SSLWantReadError, SSLWantWriteError
HAVE_SSL = True
except ImportError:
# dummy class of SSLError for environment without ssl support
class SSLError(Exception):
pass
class SSLEOFError(Exception):
pass
class SSLWantReadError(Exception):
pass
class SSLWantWriteError(Exception):
pass
ssl = None
HAVE_SSL = False

View File

@@ -0,0 +1,190 @@
import os
import socket
import struct
from typing import Optional
from urllib.parse import unquote, urlparse
from ._exceptions import WebSocketProxyException
"""
_url.py
websocket - WebSocket client library for Python
Copyright 2024 engn33r
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
__all__ = ["parse_url", "get_proxy_info"]
def parse_url(url: str) -> tuple:
"""
parse url and the result is tuple of
(hostname, port, resource path and the flag of secure mode)
Parameters
----------
url: str
url string.
"""
if ":" not in url:
raise ValueError("url is invalid")
scheme, url = url.split(":", 1)
parsed = urlparse(url, scheme="http")
if parsed.hostname:
hostname = parsed.hostname
else:
raise ValueError("hostname is invalid")
port = 0
if parsed.port:
port = parsed.port
is_secure = False
if scheme == "ws":
if not port:
port = 80
elif scheme == "wss":
is_secure = True
if not port:
port = 443
else:
raise ValueError("scheme %s is invalid" % scheme)
if parsed.path:
resource = parsed.path
else:
resource = "/"
if parsed.query:
resource += f"?{parsed.query}"
return hostname, port, resource, is_secure
DEFAULT_NO_PROXY_HOST = ["localhost", "127.0.0.1"]
def _is_ip_address(addr: str) -> bool:
try:
socket.inet_aton(addr)
except socket.error:
return False
else:
return True
def _is_subnet_address(hostname: str) -> bool:
try:
addr, netmask = hostname.split("/")
return _is_ip_address(addr) and 0 <= int(netmask) < 32
except ValueError:
return False
def _is_address_in_network(ip: str, net: str) -> bool:
ipaddr: int = struct.unpack("!I", socket.inet_aton(ip))[0]
netaddr, netmask = net.split("/")
netaddr: int = struct.unpack("!I", socket.inet_aton(netaddr))[0]
netmask = (0xFFFFFFFF << (32 - int(netmask))) & 0xFFFFFFFF
return ipaddr & netmask == netaddr
def _is_no_proxy_host(hostname: str, no_proxy: Optional[list]) -> bool:
if not no_proxy:
if v := os.environ.get("no_proxy", os.environ.get("NO_PROXY", "")).replace(
" ", ""
):
no_proxy = v.split(",")
if not no_proxy:
no_proxy = DEFAULT_NO_PROXY_HOST
if "*" in no_proxy:
return True
if hostname in no_proxy:
return True
if _is_ip_address(hostname):
return any(
[
_is_address_in_network(hostname, subnet)
for subnet in no_proxy
if _is_subnet_address(subnet)
]
)
for domain in [domain for domain in no_proxy if domain.startswith(".")]:
if hostname.endswith(domain):
return True
return False
def get_proxy_info(
hostname: str,
is_secure: bool,
proxy_host: Optional[str] = None,
proxy_port: int = 0,
proxy_auth: Optional[tuple] = None,
no_proxy: Optional[list] = None,
proxy_type: str = "http",
) -> tuple:
"""
Try to retrieve proxy host and port from environment
if not provided in options.
Result is (proxy_host, proxy_port, proxy_auth).
proxy_auth is tuple of username and password
of proxy authentication information.
Parameters
----------
hostname: str
Websocket server name.
is_secure: bool
Is the connection secure? (wss) looks for "https_proxy" in env
instead of "http_proxy"
proxy_host: str
http proxy host name.
proxy_port: str or int
http proxy port.
no_proxy: list
Whitelisted host names that don't use the proxy.
proxy_auth: tuple
HTTP proxy auth information. Tuple of username and password. Default is None.
proxy_type: str
Specify the proxy protocol (http, socks4, socks4a, socks5, socks5h). Default is "http".
Use socks4a or socks5h if you want to send DNS requests through the proxy.
"""
if _is_no_proxy_host(hostname, no_proxy):
return None, 0, None
if proxy_host:
if not proxy_port:
raise WebSocketProxyException("Cannot use port 0 when proxy_host specified")
port = proxy_port
auth = proxy_auth
return proxy_host, port, auth
env_key = "https_proxy" if is_secure else "http_proxy"
value = os.environ.get(env_key, os.environ.get(env_key.upper(), "")).replace(
" ", ""
)
if value:
proxy = urlparse(value)
auth = (
(unquote(proxy.username), unquote(proxy.password))
if proxy.username
else None
)
return proxy.hostname, proxy.port, auth
return None, 0, None

Some files were not shown because too many files have changed in this diff Show More