From 62f356402060fa2a1ffc4cb3a644481270d537aa Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Sat, 4 Jan 2025 08:28:14 -0500 Subject: [PATCH 01/88] Change subpackage name --- choreographer/DIR_INDEX.txt | 2 +- choreographer/__init__.py | 2 +- choreographer/_browser.py | 14 +++++++------- choreographer/_tab.py | 2 +- .../__init__.py | 2 ++ .../_protocol.py | 0 .../_session.py | 0 .../_target.py | 0 8 files changed, 12 insertions(+), 10 deletions(-) rename choreographer/{_devtools_protocol_layer => protocol}/__init__.py (79%) rename choreographer/{_devtools_protocol_layer => protocol}/_protocol.py (100%) rename choreographer/{_devtools_protocol_layer => protocol}/_session.py (100%) rename choreographer/{_devtools_protocol_layer => protocol}/_target.py (100%) diff --git a/choreographer/DIR_INDEX.txt b/choreographer/DIR_INDEX.txt index 709f3915..7d4779ab 100644 --- a/choreographer/DIR_INDEX.txt +++ b/choreographer/DIR_INDEX.txt @@ -21,7 +21,7 @@ which has "Tabs". Directories ----------- -_devtools_protocol_layer/ +protocol/ The browser-tab interface is intuitive, but here we have the interface that Chrome's Devtools Protocol actually provides. diff --git a/choreographer/__init__.py b/choreographer/__init__.py index 6c5fa1a4..473a428d 100644 --- a/choreographer/__init__.py +++ b/choreographer/__init__.py @@ -1,6 +1,6 @@ """choreographer is a browser controller for python.""" -import choreographer._devtools_protocol_layer as protocol +from choreographer import protocol from ._browser import Browser, BrowserClosedError, browser_which, get_browser_path from ._cli_utils import get_browser, get_browser_sync diff --git a/choreographer/_browser.py b/choreographer/_browser.py index 69c8997c..b0448211 100644 --- a/choreographer/_browser.py +++ b/choreographer/_browser.py @@ -11,18 +11,18 @@ from pathlib import Path from threading import Thread -from ._devtools_protocol_layer._protocol import ( +from ._pipe import Pipe, PipeClosedError +from ._system_utils._system import browser_which +from ._system_utils._tempfile import TempDirectory, TempDirWarning +from ._tab import Tab +from .protocol._protocol import ( TARGET_NOT_FOUND, DevtoolsProtocolError, ExperimentalFeatureWarning, Protocol, ) -from ._devtools_protocol_layer._session import Session -from ._devtools_protocol_layer._target import Target -from ._pipe import Pipe, PipeClosedError -from ._system_utils._system import browser_which -from ._system_utils._tempfile import TempDirectory, TempDirWarning -from ._tab import Tab +from .protocol._session import Session +from .protocol._target import Target # importing the below via __file__ causes __name__ weirdness when its exe'd ??? chromewrapper_path = ( diff --git a/choreographer/_tab.py b/choreographer/_tab.py index 7b38db5a..078922e0 100644 --- a/choreographer/_tab.py +++ b/choreographer/_tab.py @@ -1,4 +1,4 @@ -from ._devtools_protocol_layer._target import Target +from .protocol._target import Target class Tab(Target): diff --git a/choreographer/_devtools_protocol_layer/__init__.py b/choreographer/protocol/__init__.py similarity index 79% rename from choreographer/_devtools_protocol_layer/__init__.py rename to choreographer/protocol/__init__.py index 82a69b76..04e0299a 100644 --- a/choreographer/_devtools_protocol_layer/__init__.py +++ b/choreographer/protocol/__init__.py @@ -1,3 +1,5 @@ +"""choreographer.protocol provides classes and tools for Chrome Devtools Protocol.""" + from ._protocol import ( DevtoolsProtocolError, ExperimentalFeatureWarning, diff --git a/choreographer/_devtools_protocol_layer/_protocol.py b/choreographer/protocol/_protocol.py similarity index 100% rename from choreographer/_devtools_protocol_layer/_protocol.py rename to choreographer/protocol/_protocol.py diff --git a/choreographer/_devtools_protocol_layer/_session.py b/choreographer/protocol/_session.py similarity index 100% rename from choreographer/_devtools_protocol_layer/_session.py rename to choreographer/protocol/_session.py diff --git a/choreographer/_devtools_protocol_layer/_target.py b/choreographer/protocol/_target.py similarity index 100% rename from choreographer/_devtools_protocol_layer/_target.py rename to choreographer/protocol/_target.py From cc0898a588cb11b3f3b9cd364fbc3dad94e4f923 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Sun, 5 Jan 2025 01:04:44 -0500 Subject: [PATCH 02/88] Remove system() shortcut --- .../_system_utils/_chrome_wrapper.py | 5 ++--- choreographer/_system_utils/_system.py | 6 ++---- choreographer/protocol/_session.py | 1 + choreographer/protocol/_target.py | 20 ++++++++++++++----- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/choreographer/_system_utils/_chrome_wrapper.py b/choreographer/_system_utils/_chrome_wrapper.py index 0dfd5d9d..bc9be127 100644 --- a/choreographer/_system_utils/_chrome_wrapper.py +++ b/choreographer/_system_utils/_chrome_wrapper.py @@ -17,8 +17,7 @@ _inheritable = True -system = platform.system() -if system == "Windows": +if platform.system() == "Windows": import msvcrt else: os.set_inheritable(4, _inheritable) @@ -59,7 +58,7 @@ def open_browser( # noqa: PLR0913 too many args in func cli.append("--headless") system_dependent = {} - if system == "Windows": + if platform.system() == "Windows": to_chromium_handle = msvcrt.get_osfhandle(to_chromium) os.set_handle_inheritable(to_chromium_handle, _inheritable) from_chromium_handle = msvcrt.get_osfhandle(from_chromium) diff --git a/choreographer/_system_utils/_system.py b/choreographer/_system_utils/_system.py index de5f1a04..d6da3548 100644 --- a/choreographer/_system_utils/_system.py +++ b/choreographer/_system_utils/_system.py @@ -20,17 +20,15 @@ # firefox = // this needs to be tested # brave = // this needs to be tested -system = platform.system() - default_path_chrome = None -if system == "Windows": +if platform.system() == "Windows": default_path_chrome = [ r"c:\Program Files\Google\Chrome\Application\chrome.exe", f"c:\\Users\\{os.environ.get('USER', 'default')}\\AppData\\" "Local\\Google\\Chrome\\Application\\chrome.exe", ] -elif system == "Linux": +elif platform.system() == "Linux": default_path_chrome = [ "/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", diff --git a/choreographer/protocol/_session.py b/choreographer/protocol/_session.py index 2c2058d8..76656919 100644 --- a/choreographer/protocol/_session.py +++ b/choreographer/protocol/_session.py @@ -1,4 +1,5 @@ class Session: + # points to browser, bad def __init__(self, browser, session_id): if not isinstance(session_id, str): raise TypeError("session_id must be a string") diff --git a/choreographer/protocol/_target.py b/choreographer/protocol/_target.py index 59392c12..837e355d 100644 --- a/choreographer/protocol/_target.py +++ b/choreographer/protocol/_target.py @@ -6,6 +6,7 @@ class Target: + # points to browser, bad def __init__(self, target_id, browser): if not isinstance(target_id, str): raise TypeError("target_id must be string") @@ -16,18 +17,21 @@ def __init__(self, target_id, browser): self.sessions = OrderedDict() self.target_id = target_id + # sync def _add_session(self, session): if not isinstance(session, Session): raise TypeError("session must be an object of class Session") self.sessions[session.session_id] = session self.browser.protocol.sessions[session.session_id] = session + # sync def _remove_session(self, session_id): if isinstance(session_id, Session): session_id = session_id.session_id _ = self.sessions.pop(session_id, None) _ = self.browser.protocol.sessions.pop(session_id, None) + # async only async def create_session(self): if not self.browser.loop: raise RuntimeError( @@ -47,6 +51,7 @@ async def create_session(self): self._add_session(new_session) return new_session + # async only async def close_session(self, session_id): if not self.browser.loop: raise RuntimeError( @@ -67,11 +72,7 @@ async def close_session(self, session_id): print(f"The session {session_id} has been closed", file=sys.stderr) return response - def send_command(self, command, params=None): - if not self.sessions.values(): - raise RuntimeError("Cannot send_command without at least one valid session") - return next(iter(self.sessions.values())).send_command(command, params) - + # internal def _get_first_session(self): if not self.sessions.values(): raise RuntimeError( @@ -79,14 +80,23 @@ def _get_first_session(self): ) return next(iter(self.sessions.values())) + # wrapper + def send_command(self, command, params=None): + if not self.sessions.values(): + raise RuntimeError("Cannot send_command without at least one valid session") + return self._get_first_session().send_command(command, params) + + # async only def subscribe(self, string, callback, *, repeating=True): session = self._get_first_session() session.subscribe(string, callback, repeating=repeating) + # async only def unsubscribe(self, string): session = self._get_first_session() session.unsubscribe(string) + # async only def subscribe_once(self, string): session = self._get_first_session() return session.subscribe_once(string) From 140251cf609738698cd12827280b0c2cce031511 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Sun, 5 Jan 2025 15:17:11 -0500 Subject: [PATCH 03/88] Fix up _pipe.py for refactor --- choreographer/_pipe.py | 52 ++++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/choreographer/_pipe.py b/choreographer/_pipe.py index 1dd6e1e6..d6d66491 100644 --- a/choreographer/_pipe.py +++ b/choreographer/_pipe.py @@ -32,10 +32,28 @@ def default(self, obj): return simplejson.JSONEncoder.default(self, obj) +# whoever is setting up the process is going to look for: +# we're going to look for self.stdout_redirection +# we're going to look for self.stdin_redirection class Pipe: def __init__(self, *, debug=False, json_encoder=MultiEncoder): - self.read_from_chromium, self.write_from_chromium = list(os.pipe()) - self.read_to_chromium, self.write_to_chromium = list(os.pipe()) + # this is where pipe listens (from browser) + # so pass the write to browser + self.read_from_browser, self.write_from_browser = list(os.pipe()) + + # this is where pipe writes (to browser) + # so pass the read to browser + self.read_to_browser, self.write_to_browser = list(os.pipe()) + + # Popen will write stdout of wrapper to write_from_browser + # which is duping expected fd (3?) to stdout + self.stdout_redirection = self.write_from_browser + # Popen will read read_to_browser into stdin of wrapper + # which dupes stdin to expected fd (4?) + self.stdin_redirection = self.read_to_browser + # these won't be used on windows directly + # but we let the process manager handle platform idiosyncrasies + self.debug = debug self.json_encoder = json_encoder @@ -65,7 +83,7 @@ def write_json(self, obj, debug=None): if debug: print(f"write_json: {encoded_message}", file=sys.stderr) try: - os.write(self.write_to_chromium, encoded_message) + os.write(self.write_to_browser, encoded_message) except OSError as e: self.close() raise PipeClosedError from e @@ -90,14 +108,14 @@ def read_jsons(self, *, blocking=True, debug=None): # noqa: PLR0912, C901 branc jsons = [] try: if with_block: - os.set_blocking(self.read_from_chromium, blocking) + os.set_blocking(self.read_from_browser, blocking) except OSError as e: self.close() raise PipeClosedError from e try: raw_buffer = None # if we fail in read, we already defined raw_buffer = os.read( - self.read_from_chromium, + self.read_from_browser, 10000, ) # 10MB buffer, nbd, doesn't matter w/ this if not raw_buffer or raw_buffer == b"{bye}\n": @@ -108,8 +126,8 @@ def read_jsons(self, *, blocking=True, debug=None): # noqa: PLR0912, C901 branc while raw_buffer[-1] != 0: # still not great, return what you have if with_block: - os.set_blocking(self.read_from_chromium, True) - raw_buffer += os.read(self.read_from_chromium, 10000) + os.set_blocking(self.read_from_browser, True) + raw_buffer += os.read(self.read_from_browser, 10000) except BlockingIOError: if debug: print("read_jsons: BlockingIOError caught.", file=sys.stderr) @@ -159,9 +177,9 @@ def _close_fd(self, fd): print(f"Expected error closing {fd!s}: {e!s}", file=sys.stderr) def _fake_bye(self): - self._unblock_fd(self.write_from_chromium) + self._unblock_fd(self.write_from_browser) try: - os.write(self.write_from_chromium, b"{bye}\n") + os.write(self.write_from_browser, b"{bye}\n") except BaseException as e: # noqa: BLE001 OS errors are not consistent, catch blind # also, best effort. if self.debug: @@ -174,11 +192,11 @@ def close(self): if self.shutdown_lock.acquire(blocking=False): if platform.system() == "Windows": self._fake_bye() - self._unblock_fd(self.write_from_chromium) - self._unblock_fd(self.read_from_chromium) - self._unblock_fd(self.write_to_chromium) - self._unblock_fd(self.read_to_chromium) - self._close_fd(self.write_to_chromium) # no more writes - self._close_fd(self.write_from_chromium) # we're done with writes - self._close_fd(self.read_from_chromium) # no more attempts at read - self._close_fd(self.read_to_chromium) + self._unblock_fd(self.write_from_browser) + self._unblock_fd(self.read_from_browser) + self._unblock_fd(self.write_to_browser) + self._unblock_fd(self.read_to_browser) + self._close_fd(self.write_to_browser) # no more writes + self._close_fd(self.write_from_browser) # we're done with writes + self._close_fd(self.read_from_browser) # no more attempts at read + self._close_fd(self.read_to_browser) From 46c58d62d93983fa8316e9ea883f98471168fa83 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Sun, 5 Jan 2025 16:13:39 -0500 Subject: [PATCH 04/88] Better organize _pipe for refactor: Pipe will eventually be one of two options (the other being websocket), and since we're currently writing a CLI+ENV var generator for chromium/chrome/edge (eventually supporting firefox), Pipe is now a bit better organized so that these other classes can detect it and better interpret it. --- choreographer/_pipe.py | 71 +++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/choreographer/_pipe.py b/choreographer/_pipe.py index d6d66491..33862fa0 100644 --- a/choreographer/_pipe.py +++ b/choreographer/_pipe.py @@ -32,33 +32,32 @@ def default(self, obj): return simplejson.JSONEncoder.default(self, obj) -# whoever is setting up the process is going to look for: -# we're going to look for self.stdout_redirection -# we're going to look for self.stdin_redirection +# if we're a pipe we expect these public attributes class Pipe: def __init__(self, *, debug=False, json_encoder=MultiEncoder): - # this is where pipe listens (from browser) - # so pass the write to browser - self.read_from_browser, self.write_from_browser = list(os.pipe()) - - # this is where pipe writes (to browser) - # so pass the read to browser - self.read_to_browser, self.write_to_browser = list(os.pipe()) - - # Popen will write stdout of wrapper to write_from_browser - # which is duping expected fd (3?) to stdout - self.stdout_redirection = self.write_from_browser - # Popen will read read_to_browser into stdin of wrapper + # This is where pipe listens (from browser) + # So pass the write to browser + self._read_from_browser, self._write_from_browser = list(os.pipe()) + + # This is where pipe writes (to browser) + # So pass the read to browser + self._read_to_browser, self._write_to_browser = list(os.pipe()) + + # Popen will write stdout of wrapper to this (dupping 4) + # Browser will write directly to this if not using wrapper + self.from_external_to_choreo = self._write_from_browser + # Popen will read this into stdin of wrapper (dupping 3) + # Browser will read directly from this if not using wrapper # which dupes stdin to expected fd (4?) - self.stdin_redirection = self.read_to_browser - # these won't be used on windows directly - # but we let the process manager handle platform idiosyncrasies + self.from_choreo_to_external = self._read_to_browser + # These won't be used on windows directly, they'll be t-formed to + # windows-style handles. But let another layer handle that. - self.debug = debug - self.json_encoder = json_encoder + self.debug = debug # should be private + self.json_encoder = json_encoder # should be private # this is just a convenience to prevent multiple shutdowns - self.shutdown_lock = Lock() + self.shutdown_lock = Lock() # should be private def serialize(self, obj): message = simplejson.dumps( @@ -83,7 +82,7 @@ def write_json(self, obj, debug=None): if debug: print(f"write_json: {encoded_message}", file=sys.stderr) try: - os.write(self.write_to_browser, encoded_message) + os.write(self._write_to_browser, encoded_message) except OSError as e: self.close() raise PipeClosedError from e @@ -108,14 +107,14 @@ def read_jsons(self, *, blocking=True, debug=None): # noqa: PLR0912, C901 branc jsons = [] try: if with_block: - os.set_blocking(self.read_from_browser, blocking) + os.set_blocking(self._read_from_browser, blocking) except OSError as e: self.close() raise PipeClosedError from e try: raw_buffer = None # if we fail in read, we already defined raw_buffer = os.read( - self.read_from_browser, + self._read_from_browser, 10000, ) # 10MB buffer, nbd, doesn't matter w/ this if not raw_buffer or raw_buffer == b"{bye}\n": @@ -126,8 +125,8 @@ def read_jsons(self, *, blocking=True, debug=None): # noqa: PLR0912, C901 branc while raw_buffer[-1] != 0: # still not great, return what you have if with_block: - os.set_blocking(self.read_from_browser, True) - raw_buffer += os.read(self.read_from_browser, 10000) + os.set_blocking(self._read_from_browser, True) + raw_buffer += os.read(self._read_from_browser, 10000) except BlockingIOError: if debug: print("read_jsons: BlockingIOError caught.", file=sys.stderr) @@ -177,9 +176,9 @@ def _close_fd(self, fd): print(f"Expected error closing {fd!s}: {e!s}", file=sys.stderr) def _fake_bye(self): - self._unblock_fd(self.write_from_browser) + self._unblock_fd(self._write_from_browser) try: - os.write(self.write_from_browser, b"{bye}\n") + os.write(self._write_from_browser, b"{bye}\n") except BaseException as e: # noqa: BLE001 OS errors are not consistent, catch blind # also, best effort. if self.debug: @@ -192,11 +191,11 @@ def close(self): if self.shutdown_lock.acquire(blocking=False): if platform.system() == "Windows": self._fake_bye() - self._unblock_fd(self.write_from_browser) - self._unblock_fd(self.read_from_browser) - self._unblock_fd(self.write_to_browser) - self._unblock_fd(self.read_to_browser) - self._close_fd(self.write_to_browser) # no more writes - self._close_fd(self.write_from_browser) # we're done with writes - self._close_fd(self.read_from_browser) # no more attempts at read - self._close_fd(self.read_to_browser) + self._unblock_fd(self._write_from_browser) + self._unblock_fd(self._read_from_browser) + self._unblock_fd(self._write_to_browser) + self._unblock_fd(self._read_to_browser) + self._close_fd(self._write_to_browser) # no more writes + self._close_fd(self._write_from_browser) # we're done with writes + self._close_fd(self._read_from_browser) # no more attempts at read + self._close_fd(self._read_to_browser) From 15f920fb03f69ab83a913784227645b297e36508 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Sun, 5 Jan 2025 16:24:43 -0500 Subject: [PATCH 05/88] Create a new browser package for brand support: sys utils needs to be refactored before we can finish browser since sys utils will provide mechanisms to find chromium issue where BROWSER_PATH might now also be firefox but I suppose that just means you would have to specify firefox I think brave will work with this --- choreographer/_browser/__init__.py | 0 .../_browser/_unix_pipe_chromium_wrapper.py | 49 ++++++++ choreographer/_browser/chromium.py | 75 +++++++++++ .../_system_utils/_chrome_wrapper.py | 117 ------------------ 4 files changed, 124 insertions(+), 117 deletions(-) create mode 100644 choreographer/_browser/__init__.py create mode 100644 choreographer/_browser/_unix_pipe_chromium_wrapper.py create mode 100644 choreographer/_browser/chromium.py delete mode 100644 choreographer/_system_utils/_chrome_wrapper.py diff --git a/choreographer/_browser/__init__.py b/choreographer/_browser/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/choreographer/_browser/_unix_pipe_chromium_wrapper.py b/choreographer/_browser/_unix_pipe_chromium_wrapper.py new file mode 100644 index 00000000..f36d461e --- /dev/null +++ b/choreographer/_browser/_unix_pipe_chromium_wrapper.py @@ -0,0 +1,49 @@ +""" +_unix_pipe_chromium_wrapper.py provides proper fds to chrome. + +By running chromium in a new process (this wrapper), we guarantee +the user hasn't stolen one of our desired file descriptors, which +the OS gives away first-come-first-serve everytime someone opens a +file. chromium demands we use 3 and 4. +""" + +import os + +# importing modules has side effects, so we do this before imports +# ruff: noqa: E402 + +# chromium reads on 3, writes on 4 +os.dup2(0, 3) # make our stdin their input +os.dup2(1, 4) # make our stdout their output + +_inheritable = True +os.set_inheritable(4, _inheritable) +os.set_inheritable(3, _inheritable) + +import signal +import subprocess +import sys +from functools import partial + +# we're a wrapper, the cli is everything that came after us +cli = sys.argv[1:] +process = subprocess.Popen(cli, pass_fds=(3, 4)) # noqa: S603 untrusted input + + +def kill_proc(process, _sig_num, _frame): + process.terminate() + process.wait(5) # 5 seconds to clean up nicely, it's a lot + process.kill() + + +kp = partial(kill_proc, process) +signal.signal(signal.SIGTERM, kp) +signal.signal(signal.SIGINT, kp) + +process.wait() + +# not great but it seems that +# pipe isn't always closed when chrome closes +# so we pretend to be chrome and send a bye instead +# also, above depends on async/sync, platform, etc +print("{bye}") diff --git a/choreographer/_browser/chromium.py b/choreographer/_browser/chromium.py new file mode 100644 index 00000000..c3dde512 --- /dev/null +++ b/choreographer/_browser/chromium.py @@ -0,0 +1,75 @@ +"""chromium.py provides a class proving tools for running chromium browsers.""" + +import os +import platform +import sys +from pathlib import Path + +# TODO(Andrew): move this to its own subpackage comm # noqa: FIX002, TD003 +from choreographer._pipe import Pipe, WebSocket + +if platform.system() == "Windows": + import msvcrt + + +class Chromium: + def __init__(self, pipe): + self._comm = pipe + # extra information from pipe + + # where do we get user data dir + def get_cli(self, temp_dir, **kwargs): + gpu_enabled = kwargs.pop("with_gpu", False) + headless = kwargs.pop("headless", True) + sandbox = kwargs.pop("with_sandbox", False) + if kwargs: + raise RuntimeError( + "Chromium.get_cli() received " f"invalid args: {kwargs.keys()}", + ) + path = None # TODO(Andrew): not legit # noqa: FIX002,TD003 + chromium_wrapper_path = Path(__file__).resolve().parent / "chromium_wrapper.py" + if platform.system() != "Windows": + cli = [ + sys.executable, + chromium_wrapper_path, + path, + ] + else: + cli = [ + path, + ] + + cli.extend( + [ + "--disable-breakpad", + "--allow-file-access-from-files", + "--enable-logging=stderr", + f"--user-data-dir={temp_dir}", + "--no-first-run", + "--enable-unsafe-swiftshader", + ], + ) + if not gpu_enabled: + cli.append("--disable-gpu") + if headless: + cli.append("--headless") + if not sandbox: + cli.append("--no-sandbox") + + if isinstance(self._comm, Pipe): + cli.append("--remote-debugging-pipe") + if platform.system() == "Windows": + _inheritable = True + write_handle = msvcrt.get_osfhandle(self._comm.from_choreo_to_external) + read_handle = msvcrt.get_osfhandle(self._comm.from_external_to_choreo) + os.set_handle_inheritable(write_handle, _inheritable) + os.set_handle_inheritable(read_handle, _inheritable) + cli += [ + f"--remote-debugging-io-pipes={read_handle!s},{write_handle!s}", + ] + elif isinstance(self._comm, WebSocket): + raise NotImplementedError("Websocket style comms are not implemented yet") + + +def get_env(): + return os.environ().copy() diff --git a/choreographer/_system_utils/_chrome_wrapper.py b/choreographer/_system_utils/_chrome_wrapper.py deleted file mode 100644 index bc9be127..00000000 --- a/choreographer/_system_utils/_chrome_wrapper.py +++ /dev/null @@ -1,117 +0,0 @@ -import os - -# importing modules has side effects, so we do this before imports - -# not everyone uses the wrapper as a process -if __name__ == "__main__": - # chromium reads on 3, writes on 4 - os.dup2(0, 3) # make our stdin their input - os.dup2(1, 4) # make our stdout their output - -import asyncio -import platform -import signal -import subprocess -import sys -from functools import partial - -_inheritable = True - -if platform.system() == "Windows": - import msvcrt -else: - os.set_inheritable(4, _inheritable) - os.set_inheritable(3, _inheritable) - - -def open_browser( # noqa: PLR0913 too many args in func - to_chromium, - from_chromium, - stderr=sys.stderr, - env=None, - loop=None, - *, - loop_hack=False, -): - path = env.get("BROWSER_PATH") - if not path: - raise RuntimeError("No browser path was passed to run") - - user_data_dir = env["USER_DATA_DIR"] - - cli = [ - path, - "--remote-debugging-pipe", - "--disable-breakpad", - "--allow-file-access-from-files", - "--enable-logging=stderr", - f"--user-data-dir={user_data_dir}", - "--no-first-run", - "--enable-unsafe-swiftshader", - ] - if not env.get("GPU_ENABLED", False): - cli.append("--disable-gpu") - if not env.get("SANDBOX_ENABLED", False): - cli.append("--no-sandbox") - - if "HEADLESS" in env: - cli.append("--headless") - - system_dependent = {} - if platform.system() == "Windows": - to_chromium_handle = msvcrt.get_osfhandle(to_chromium) - os.set_handle_inheritable(to_chromium_handle, _inheritable) - from_chromium_handle = msvcrt.get_osfhandle(from_chromium) - os.set_handle_inheritable(from_chromium_handle, _inheritable) - cli += [ - f"--remote-debugging-io-pipes={to_chromium_handle!s},{from_chromium_handle!s}", - ] - system_dependent["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP - system_dependent["close_fds"] = False - else: - system_dependent["pass_fds"] = (to_chromium, from_chromium) - - if not loop: - return subprocess.Popen( # noqa: S603 input fine. - cli, - stderr=stderr, - **system_dependent, - ) - elif loop_hack: - - def run(): - return subprocess.Popen( # noqa: S603 input fine. - cli, - stderr=stderr, - **system_dependent, - ) - - return asyncio.to_thread(run) - else: - return asyncio.create_subprocess_exec( - cli[0], - *cli[1:], - stderr=stderr, - **system_dependent, - ) - - -def kill_proc(process, _sig_num, _frame): - process.terminate() - process.wait(5) # 5 seconds to clean up nicely, it's a lot - process.kill() - - -if __name__ == "__main__": - process = open_browser(to_chromium=3, from_chromium=4, env=os.environ) - kp = partial(kill_proc, process) - signal.signal(signal.SIGTERM, kp) - signal.signal(signal.SIGINT, kp) - - process.wait() - - # not great but it seems that - # pipe isn't always closed when chrome closes - # so we pretend to be chrome and send a bye instead - # also, above depends on async/sync, platform, etc - print("{bye}") From c997d51ce21dcab1317f62d44b8a780d24f08c59 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Sun, 5 Jan 2025 16:41:01 -0500 Subject: [PATCH 06/88] Fix formatting --- choreographer/_browser/chromium.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/choreographer/_browser/chromium.py b/choreographer/_browser/chromium.py index c3dde512..bf0f53fe 100644 --- a/choreographer/_browser/chromium.py +++ b/choreographer/_browser/chromium.py @@ -5,7 +5,7 @@ import sys from pathlib import Path -# TODO(Andrew): move this to its own subpackage comm # noqa: FIX002, TD003 +# TODO(Andrew): move to own subpackage during channel refactor # noqa: FIX002, TD003 from choreographer._pipe import Pipe, WebSocket if platform.system() == "Windows": @@ -13,8 +13,8 @@ class Chromium: - def __init__(self, pipe): - self._comm = pipe + def __init__(self, channel): + self._comm = channel # extra information from pipe # where do we get user data dir @@ -56,19 +56,19 @@ def get_cli(self, temp_dir, **kwargs): if not sandbox: cli.append("--no-sandbox") - if isinstance(self._comm, Pipe): + if isinstance(self._channel, Pipe): cli.append("--remote-debugging-pipe") if platform.system() == "Windows": _inheritable = True - write_handle = msvcrt.get_osfhandle(self._comm.from_choreo_to_external) - read_handle = msvcrt.get_osfhandle(self._comm.from_external_to_choreo) - os.set_handle_inheritable(write_handle, _inheritable) - os.set_handle_inheritable(read_handle, _inheritable) + w_handle = msvcrt.get_osfhandle(self._channel.from_choreo_to_external) + r_handle = msvcrt.get_osfhandle(self._channel.from_external_to_choreo) + os.set_handle_inheritable(w_handle, _inheritable) + os.set_handle_inheritable(r_handle, _inheritable) cli += [ - f"--remote-debugging-io-pipes={read_handle!s},{write_handle!s}", + f"--remote-debugging-io-pipes={r_handle!s},{w_handle!s}", ] - elif isinstance(self._comm, WebSocket): - raise NotImplementedError("Websocket style comms are not implemented yet") + elif isinstance(self._channel, WebSocket): + raise NotImplementedError("Websocket style channels not implemented yet") def get_env(): From d8dbfecfa8975e57c97923add10256da07879b8e Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Sun, 5 Jan 2025 16:59:00 -0500 Subject: [PATCH 07/88] Further fix up namespaces --- choreographer/DIR_INDEX.txt | 31 ------------------- choreographer/__init__.py | 2 +- .../{_browser => _browsers}/__init__.py | 0 .../_unix_pipe_chromium_wrapper.py | 0 .../{_browser => _browsers}/chromium.py | 2 +- choreographer/_channels/__init__.py | 0 choreographer/{_pipe.py => _channels/pipe.py} | 0 choreographer/_cli_utils/__init__.py | 0 choreographer/{ => _cli_utils}/_cli_utils.py | 0 .../{ => _cli_utils}/_cli_utils_no_qa.py | 0 10 files changed, 2 insertions(+), 33 deletions(-) delete mode 100644 choreographer/DIR_INDEX.txt rename choreographer/{_browser => _browsers}/__init__.py (100%) rename choreographer/{_browser => _browsers}/_unix_pipe_chromium_wrapper.py (100%) rename choreographer/{_browser => _browsers}/chromium.py (97%) create mode 100644 choreographer/_channels/__init__.py rename choreographer/{_pipe.py => _channels/pipe.py} (100%) create mode 100644 choreographer/_cli_utils/__init__.py rename choreographer/{ => _cli_utils}/_cli_utils.py (100%) rename choreographer/{ => _cli_utils}/_cli_utils_no_qa.py (100%) diff --git a/choreographer/DIR_INDEX.txt b/choreographer/DIR_INDEX.txt deleted file mode 100644 index 7d4779ab..00000000 --- a/choreographer/DIR_INDEX.txt +++ /dev/null @@ -1,31 +0,0 @@ -Files ------ - -_browser.py - -A behemoth: contains process control, future storage, and user's primary interface. - -_cli_utils.py - -Functions to be used as scripts from commandline. - -_pipe.py - -The communication layer between choreographer and the browser. - -_tab.py - -Also part of user's primary interface. Intuitive, the user interacts with a "Browser" -which has "Tabs". - -Directories ------------ - -protocol/ - -The browser-tab interface is intuitive, but here we have the interface that Chrome's -Devtools Protocol actually provides. - -_system_utils/ - -Some utilities we use to encourage cross-platform compatibility. diff --git a/choreographer/__init__.py b/choreographer/__init__.py index 473a428d..b6ff30ed 100644 --- a/choreographer/__init__.py +++ b/choreographer/__init__.py @@ -3,8 +3,8 @@ from choreographer import protocol from ._browser import Browser, BrowserClosedError, browser_which, get_browser_path +from ._channels.pipe import BlockWarning, PipeClosedError from ._cli_utils import get_browser, get_browser_sync -from ._pipe import BlockWarning, PipeClosedError from ._system_utils._tempfile import TempDirectory, TempDirWarning from ._tab import Tab diff --git a/choreographer/_browser/__init__.py b/choreographer/_browsers/__init__.py similarity index 100% rename from choreographer/_browser/__init__.py rename to choreographer/_browsers/__init__.py diff --git a/choreographer/_browser/_unix_pipe_chromium_wrapper.py b/choreographer/_browsers/_unix_pipe_chromium_wrapper.py similarity index 100% rename from choreographer/_browser/_unix_pipe_chromium_wrapper.py rename to choreographer/_browsers/_unix_pipe_chromium_wrapper.py diff --git a/choreographer/_browser/chromium.py b/choreographer/_browsers/chromium.py similarity index 97% rename from choreographer/_browser/chromium.py rename to choreographer/_browsers/chromium.py index bf0f53fe..7c785621 100644 --- a/choreographer/_browser/chromium.py +++ b/choreographer/_browsers/chromium.py @@ -6,7 +6,7 @@ from pathlib import Path # TODO(Andrew): move to own subpackage during channel refactor # noqa: FIX002, TD003 -from choreographer._pipe import Pipe, WebSocket +from choreographer._channels.pipe import Pipe, WebSocket if platform.system() == "Windows": import msvcrt diff --git a/choreographer/_channels/__init__.py b/choreographer/_channels/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/choreographer/_pipe.py b/choreographer/_channels/pipe.py similarity index 100% rename from choreographer/_pipe.py rename to choreographer/_channels/pipe.py diff --git a/choreographer/_cli_utils/__init__.py b/choreographer/_cli_utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/choreographer/_cli_utils.py b/choreographer/_cli_utils/_cli_utils.py similarity index 100% rename from choreographer/_cli_utils.py rename to choreographer/_cli_utils/_cli_utils.py diff --git a/choreographer/_cli_utils_no_qa.py b/choreographer/_cli_utils/_cli_utils_no_qa.py similarity index 100% rename from choreographer/_cli_utils_no_qa.py rename to choreographer/_cli_utils/_cli_utils_no_qa.py From c2ebdf6ae9dde4c607b31140c843a8481521efd7 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Sun, 5 Jan 2025 20:44:58 -0500 Subject: [PATCH 08/88] Refactor sys/cli --- choreographer/__init__.py | 4 +- choreographer/_browsers/chromium.py | 5 +- choreographer/_cli_utils/__init__.py | 15 ++ choreographer/_cli_utils/_cli_utils.py | 155 +++++++++--------- choreographer/_sys_utils/__init__.py | 9 + .../_tempfile.py | 0 .../_system.py => _sys_utils/_which.py} | 92 +++++------ choreographer/_system_utils/__init__.py | 0 8 files changed, 152 insertions(+), 128 deletions(-) create mode 100644 choreographer/_sys_utils/__init__.py rename choreographer/{_system_utils => _sys_utils}/_tempfile.py (100%) rename choreographer/{_system_utils/_system.py => _sys_utils/_which.py} (54%) delete mode 100644 choreographer/_system_utils/__init__.py diff --git a/choreographer/__init__.py b/choreographer/__init__.py index b6ff30ed..fa047cc5 100644 --- a/choreographer/__init__.py +++ b/choreographer/__init__.py @@ -2,10 +2,10 @@ from choreographer import protocol -from ._browser import Browser, BrowserClosedError, browser_which, get_browser_path +from ._browser import Browser, BrowserClosedError from ._channels.pipe import BlockWarning, PipeClosedError from ._cli_utils import get_browser, get_browser_sync -from ._system_utils._tempfile import TempDirectory, TempDirWarning +from ._sys_utils import TempDirectory, TempDirWarning, browser_which, get_browser_path from ._tab import Tab __all__ = [ diff --git a/choreographer/_browsers/chromium.py b/choreographer/_browsers/chromium.py index 7c785621..b6857d71 100644 --- a/choreographer/_browsers/chromium.py +++ b/choreographer/_browsers/chromium.py @@ -5,12 +5,11 @@ import sys from pathlib import Path -# TODO(Andrew): move to own subpackage during channel refactor # noqa: FIX002, TD003 -from choreographer._channels.pipe import Pipe, WebSocket - if platform.system() == "Windows": import msvcrt +from choreographer._channels.pipe import Pipe, WebSocket + class Chromium: def __init__(self, channel): diff --git a/choreographer/_cli_utils/__init__.py b/choreographer/_cli_utils/__init__.py index e69de29b..f5ec7439 100644 --- a/choreographer/_cli_utils/__init__.py +++ b/choreographer/_cli_utils/__init__.py @@ -0,0 +1,15 @@ +from ._cli_utils import ( + get_chrome, + get_chrome_cli, + get_chrome_download_path, + get_chrome_sync, +) +from ._cli_utils_no_qa import diagnose + +__all__ = [ + "diagnose", + "get_chrome", + "get_chrome_cli", + "get_chrome_download_path", + "get_chrome_sync", +] diff --git a/choreographer/_cli_utils/_cli_utils.py b/choreographer/_cli_utils/_cli_utils.py index a0bbdd58..c825333f 100644 --- a/choreographer/_cli_utils/_cli_utils.py +++ b/choreographer/_cli_utils/_cli_utils.py @@ -9,44 +9,48 @@ import zipfile from pathlib import Path -# we use arch instead of platform when singular since platform is a package -platforms = ["linux64", "win32", "win64", "mac-x64", "mac-arm64"] - -default_local_exe_path = Path(__file__).resolve().parent / "browser_exe" - -platform_detected = platform.system() -arch_size_detected = "64" if sys.maxsize > 2**32 else "32" -arch_detected = "arm" if platform.processor() == "arm" else "x" - -if platform_detected == "Windows": - chrome_platform_detected = "win" + arch_size_detected -elif platform_detected == "Linux": - chrome_platform_detected = "linux" + arch_size_detected -elif platform_detected == "Darwin": - chrome_platform_detected = "mac-" + arch_detected + arch_size_detected - -default_exe_name = None -if platform_detected.startswith("Linux"): - default_exe_name = ( - default_local_exe_path / f"chrome-{chrome_platform_detected}" / "chrome" +_default_download_path = Path(__file__).resolve().parent / "browser_exe" + +_chrome_for_testing_url = "https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json" + +_platforms = ["linux64", "win32", "win64", "mac-x64", "mac-arm64"] + +_arch_size_detected = "64" if sys.maxsize > 2**32 else "32" +_arch_detected = "arm" if platform.processor() == "arm" else "x" + +if platform.system() == "Windows": + _chrome_platform_detected = "win" + _arch_size_detected +elif platform.system() == "Linux": + _chrome_platform_detected = "linux" + _arch_size_detected +elif platform.system() == "Darwin": + _chrome_platform_detected = "mac-" + _arch_detected + _arch_size_detected + +_default_exe_path = None +if platform.system().startswith("Linux"): + _default_exe_path = ( + _default_download_path / f"chrome-{_chrome_platform_detected}" / "chrome" ) -elif platform_detected.startswith("Darwin"): - default_exe_name = ( - default_local_exe_path - / f"chrome-{chrome_platform_detected}" +elif platform.system().startswith("Darwin"): + _default_exe_path = ( + _default_download_path + / f"chrome-{_chrome_platform_detected}" / "Google Chrome for Testing.app" / "Contents" / "MacOS" / "Google Chrome for Testing" ) -elif platform_detected.startswith("Win"): - default_exe_name = ( - default_local_exe_path / f"chrome-{chrome_platform_detected}" / "chrome.exe" +elif platform.system().startswith("Win"): + _default_exe_path = ( + _default_download_path / f"chrome-{_chrome_platform_detected}" / "chrome.exe" ) +def get_chrome_download_path(): + return _default_exe_path + + # https://stackoverflow.com/questions/39296101/python-zipfile-removes-execute-permissions-from-binaries -class ZipFilePermissions(zipfile.ZipFile): +class _ZipFilePermissions(zipfile.ZipFile): def _extract_member(self, member, targetpath, pwd): if not isinstance(member, zipfile.ZipInfo): member = self.getinfo(member) @@ -59,54 +63,17 @@ def _extract_member(self, member, targetpath, pwd): return path -def get_browser_cli(): - if "ubuntu" in platform.version().lower(): - warnings.warn( # noqa: B028 - "You are using `get_browser()` on Ubuntu." - " Ubuntu is **very strict** about where binaries come from." - " You have to disable the sandbox with use_sandbox=False" - " when you initialize the browser OR you can install from Ubuntu's" - " package manager.", - UserWarning, - ) - parser = argparse.ArgumentParser(description="tool to help debug problems") - parser.add_argument("--i", "-i", type=int, dest="i") - parser.add_argument("--arch", dest="arch") - parser.add_argument("--path", dest="path") - parser.add_argument( - "-v", - "--verbose", - dest="verbose", - action="store_true", - ) - parser.set_defaults(i=-1) - parser.set_defaults(path=default_local_exe_path) - parser.set_defaults(arch=chrome_platform_detected) - parser.set_defaults(verbose=False) - parsed = parser.parse_args() - i = parsed.i - arch = parsed.arch - path = Path(parsed.path) - verbose = parsed.verbose - if not arch or arch not in platforms: - raise RuntimeError( - "You must specify a platform: " - f"linux64, win32, win64, mac-x64, mac-arm64, not {platform}", - ) - print(get_browser_sync(arch=arch, i=i, path=path, verbose=verbose)) - - -def get_browser_sync( - arch=chrome_platform_detected, +def get_chrome_sync( + arch=_chrome_platform_detected, i=-1, - path=default_local_exe_path, + path=_default_exe_path, *, verbose=False, ): path = Path(path) browser_list = json.loads( - urllib.request.urlopen( - "https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json", + urllib.request.urlopen( # noqa: S310 audit url for schemes + _chrome_for_testing_url, ).read(), ) version_obj = browser_list["versions"][i] @@ -124,7 +91,7 @@ def get_browser_sync( filename = path / "chrome.zip" with urllib.request.urlopen(url) as response, filename.open("wb") as out_file: # noqa: S310 audit url shutil.copyfileobj(response, out_file) - with ZipFilePermissions(filename, "r") as zip_ref: + with _ZipFilePermissions(filename, "r") as zip_ref: zip_ref.extractall(path) if arch.startswith("linux"): @@ -144,10 +111,46 @@ def get_browser_sync( return exe_name -# to_thread everything -async def get_browser( - arch=chrome_platform_detected, +async def get_chrome( + arch=_chrome_platform_detected, i=-1, - path=default_local_exe_path, + path=_default_exe_path, ): - return await asyncio.to_thread(get_browser_sync, arch=arch, i=i, path=path) + return await asyncio.to_thread(get_chrome_sync, arch=arch, i=i, path=path) + + +def get_chrome_cli(): + if "ubuntu" in platform.version().lower(): + warnings.warn( # noqa: B028 + "You are using `get_browser()` on Ubuntu." + " Ubuntu is **very strict** about where binaries come from." + " You have to disable the sandbox with use_sandbox=False" + " when you initialize the browser OR you can install from Ubuntu's" + " package manager.", + UserWarning, + ) + parser = argparse.ArgumentParser(description="tool to help debug problems") + parser.add_argument("--i", "-i", type=int, dest="i") + parser.add_argument("--arch", dest="arch") + parser.add_argument("--path", dest="path") + parser.add_argument( + "-v", + "--verbose", + dest="verbose", + action="store_true", + ) + parser.set_defaults(i=-1) + parser.set_defaults(path=_default_exe_path) + parser.set_defaults(arch=_chrome_platform_detected) + parser.set_defaults(verbose=False) + parsed = parser.parse_args() + i = parsed.i + arch = parsed.arch + path = Path(parsed.path) + verbose = parsed.verbose + if not arch or arch not in _platforms: + raise RuntimeError( + "You must specify a platform: " + f"linux64, win32, win64, mac-x64, mac-arm64, not {platform}", + ) + print(get_chrome_sync(arch=arch, i=i, path=path, verbose=verbose)) diff --git a/choreographer/_sys_utils/__init__.py b/choreographer/_sys_utils/__init__.py new file mode 100644 index 00000000..7c5daf99 --- /dev/null +++ b/choreographer/_sys_utils/__init__.py @@ -0,0 +1,9 @@ +from ._tempfile import TempDirectory, TempDirWarning +from ._which import browser_which, get_browser_path + +__all__ = [ + "TempDirWarning", + "TempDirectory", + "browser_which", + "get_browser_path", +] diff --git a/choreographer/_system_utils/_tempfile.py b/choreographer/_sys_utils/_tempfile.py similarity index 100% rename from choreographer/_system_utils/_tempfile.py rename to choreographer/_sys_utils/_tempfile.py diff --git a/choreographer/_system_utils/_system.py b/choreographer/_sys_utils/_which.py similarity index 54% rename from choreographer/_system_utils/_system.py rename to choreographer/_sys_utils/_which.py index d6da3548..dc68d979 100644 --- a/choreographer/_system_utils/_system.py +++ b/choreographer/_sys_utils/_which.py @@ -1,11 +1,12 @@ import os import platform import shutil -import sys -from choreographer._cli_utils import default_exe_name +from choreographer._cli_utils import get_chrome_download_path -chrome = [ +chromium_names = ["chromium", "chromium-browser"] + +chrome_names = [ "chrome", "Chrome", "google-chrome", @@ -13,34 +14,35 @@ "Chrome.app", "Google Chrome", "Google Chrome.app", - "chromium", - "chromium-browser", -] -chromium = ["chromium", "chromium-browser"] -# firefox = // this needs to be tested -# brave = // this needs to be tested - -default_path_chrome = None +].extend(chromium_names) +typical_chrome_paths = None if platform.system() == "Windows": - default_path_chrome = [ + typical_chrome_paths = [ r"c:\Program Files\Google\Chrome\Application\chrome.exe", f"c:\\Users\\{os.environ.get('USER', 'default')}\\AppData\\" "Local\\Google\\Chrome\\Application\\chrome.exe", ] elif platform.system() == "Linux": - default_path_chrome = [ + typical_chrome_paths = [ "/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/usr/bin/chrome", ] else: # assume mac, or system == "Darwin" - default_path_chrome = [ + typical_chrome_paths = [ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", ] -def which_windows_chrome(): +def _is_exe(path): + try: + return os.access(path, os.X_OK) + except: # noqa: E722 bare except ok, weird errors, best effort. + return False + + +def _which_from_windows_reg(): try: import re import winreg @@ -57,48 +59,44 @@ def which_windows_chrome(): exe = re.search('"(.*?)"', command).group(1) except BaseException: # noqa: BLE001 don't care why, best effort search return None + return exe -def _is_exe(path): - try: - return os.access(path, os.X_OK) - except: # noqa: E722 bare except ok, weird errors, best effort. - return False +def browser_which(executable_names=chrome_names, *, skip_local=False): # noqa: C901 + path = None + if isinstance(executable_names, str): + executable_name = [executable_names] + + local_chrome = get_chrome_download_path() + if ( + local_chrome.exists() + and not skip_local + and local_chrome.name in executable_names + ): + return local_chrome -def browser_which(executable_name=chrome, *, debug=False, skip_local=False): # noqa: PLR0912, C901 - if debug: - print(f"Checking {default_exe_name}", file=sys.stderr) - if not skip_local and default_exe_name.exists(): - if debug: - print(f"Found {default_exe_name}", file=sys.stderr) - return default_exe_name - path = None - if isinstance(executable_name, str): - executable_name = [executable_name] if platform.system() == "Windows": os.environ["NoDefaultCurrentDirectoryInExePath"] = "0" # noqa: SIM112 var name set by windows + for exe in executable_name: - if debug: - print(f"looking for {exe}", file=sys.stderr) if platform.system() == "Windows" and exe == "chrome": - path = which_windows_chrome() - if path and _is_exe(path): - return path + path = _which_from_windows_reg() + if path and _is_exe(path): + return path path = shutil.which(exe) - if debug: - print(f"looking for {path}", file=sys.stderr) if path and _is_exe(path): return path - default_path = [] - if executable_name == chrome: - default_path = default_path_chrome - for candidate in default_path: - if debug: - print(f"Looking at {candidate}", file=sys.stderr) - if _is_exe(candidate): - return candidate - if debug: - print("Found nothing...", file=sys.stderr) + # which didn't work + + # hail mary + if "chrome" in executable_names: + for candidate in typical_chrome_paths: + if _is_exe(candidate): + return candidate return None + + +def get_browser_path(**kwargs): + return os.environ.get("BROWSER_PATH", browser_which(**kwargs)) diff --git a/choreographer/_system_utils/__init__.py b/choreographer/_system_utils/__init__.py deleted file mode 100644 index e69de29b..00000000 From d51fccc0fafba84312c48600e663a279bc3ab081 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Sun, 5 Jan 2025 20:58:52 -0500 Subject: [PATCH 09/88] Change names of protocol a bit --- choreographer/protocol/__init__.py | 42 ++++++++++++++++--- .../{_protocol.py => _protocol_sync.py} | 32 ++------------ 2 files changed, 40 insertions(+), 34 deletions(-) rename choreographer/protocol/{_protocol.py => _protocol_sync.py} (72%) diff --git a/choreographer/protocol/__init__.py b/choreographer/protocol/__init__.py index 04e0299a..6a37d741 100644 --- a/choreographer/protocol/__init__.py +++ b/choreographer/protocol/__init__.py @@ -1,14 +1,44 @@ """choreographer.protocol provides classes and tools for Chrome Devtools Protocol.""" -from ._protocol import ( - DevtoolsProtocolError, - ExperimentalFeatureWarning, - MessageTypeError, - MissingKeyError, -) from ._session import Session from ._target import Target + +class DevtoolsProtocolError(Exception): + """.""" + + def __init__(self, response): + """.""" + super().__init__(response) + self.code = response["error"]["code"] + self.message = response["error"]["message"] + + +class MessageTypeError(TypeError): + """.""" + + def __init__(self, key, value, expected_type): + """.""" + value = type(value) if not isinstance(value, type) else value + super().__init__( + f"Message with key {key} must have type {expected_type}, not {value}.", + ) + + +class MissingKeyError(ValueError): + """.""" + + def __init__(self, key, obj): + """.""" + super().__init__( + f"Message missing required key/s {key}. Message received: {obj}", + ) + + +class ExperimentalFeatureWarning(UserWarning): + """.""" + + __all__ = [ "DevtoolsProtocolError", "ExperimentalFeatureWarning", diff --git a/choreographer/protocol/_protocol.py b/choreographer/protocol/_protocol_sync.py similarity index 72% rename from choreographer/protocol/_protocol.py rename to choreographer/protocol/_protocol_sync.py index b8dbe402..2bed0031 100644 --- a/choreographer/protocol/_protocol.py +++ b/choreographer/protocol/_protocol_sync.py @@ -1,30 +1,6 @@ -TARGET_NOT_FOUND = -32602 - - -class DevtoolsProtocolError(Exception): - def __init__(self, response): - super().__init__(response) - self.code = response["error"]["code"] - self.message = response["error"]["message"] - - -class MessageTypeError(TypeError): - def __init__(self, key, value, expected_type): - value = type(value) if not isinstance(value, type) else value - super().__init__( - f"Message with key {key} must have type {expected_type}, not {value}.", - ) +from choreographer import protocol - -class MissingKeyError(ValueError): - def __init__(self, key, obj): - super().__init__( - f"Message missing required key/s {key}. Message received: {obj}", - ) - - -class ExperimentalFeatureWarning(UserWarning): - pass +TARGET_NOT_FOUND = -32602 class Protocol: @@ -49,9 +25,9 @@ def verify_json(self, obj): required_keys = {"id": int, "method": str} for key, type_key in required_keys.items(): if key not in obj: - raise MissingKeyError(key, obj) + raise protocol.MissingKeyError(key, obj) if not isinstance(obj[key], type_key): - raise MessageTypeError(key, type(obj[key]), type_key) + raise protocol.MessageTypeError(key, type(obj[key]), type_key) n_keys += 2 if "params" in obj: From 241960d203b3c16b032926687a3c85c05654d1ff Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Sun, 5 Jan 2025 21:28:32 -0500 Subject: [PATCH 10/88] Change namespace of protocol --- choreographer/protocol/__init__.py | 44 ++++-------------- .../{_protocol_sync.py => _protocol.py} | 45 +++++++++++++++++-- 2 files changed, 49 insertions(+), 40 deletions(-) rename choreographer/protocol/{_protocol_sync.py => _protocol.py} (68%) diff --git a/choreographer/protocol/__init__.py b/choreographer/protocol/__init__.py index 6a37d741..5a98d2f4 100644 --- a/choreographer/protocol/__init__.py +++ b/choreographer/protocol/__init__.py @@ -1,46 +1,18 @@ """choreographer.protocol provides classes and tools for Chrome Devtools Protocol.""" +from ._protocol import ( + DevtoolsProtocolError, + Ecode, + ExperimentalFeatureWarning, + MessageTypeError, + MissingKeyError, +) from ._session import Session from ._target import Target - -class DevtoolsProtocolError(Exception): - """.""" - - def __init__(self, response): - """.""" - super().__init__(response) - self.code = response["error"]["code"] - self.message = response["error"]["message"] - - -class MessageTypeError(TypeError): - """.""" - - def __init__(self, key, value, expected_type): - """.""" - value = type(value) if not isinstance(value, type) else value - super().__init__( - f"Message with key {key} must have type {expected_type}, not {value}.", - ) - - -class MissingKeyError(ValueError): - """.""" - - def __init__(self, key, obj): - """.""" - super().__init__( - f"Message missing required key/s {key}. Message received: {obj}", - ) - - -class ExperimentalFeatureWarning(UserWarning): - """.""" - - __all__ = [ "DevtoolsProtocolError", + "Ecode", "ExperimentalFeatureWarning", "MessageTypeError", "MissingKeyError", diff --git a/choreographer/protocol/_protocol_sync.py b/choreographer/protocol/_protocol.py similarity index 68% rename from choreographer/protocol/_protocol_sync.py rename to choreographer/protocol/_protocol.py index 2bed0031..925347d5 100644 --- a/choreographer/protocol/_protocol_sync.py +++ b/choreographer/protocol/_protocol.py @@ -1,6 +1,43 @@ -from choreographer import protocol +from enum import Enum -TARGET_NOT_FOUND = -32602 + +class Ecode(Enum): + TARGET_NOT_FOUND = -32602 + + +class DevtoolsProtocolError(Exception): + """.""" + + def __init__(self, response): + """.""" + super().__init__(response) + self.code = response["error"]["code"] + self.message = response["error"]["message"] + + +class MessageTypeError(TypeError): + """.""" + + def __init__(self, key, value, expected_type): + """.""" + value = type(value) if not isinstance(value, type) else value + super().__init__( + f"Message with key {key} must have type {expected_type}, not {value}.", + ) + + +class MissingKeyError(ValueError): + """.""" + + def __init__(self, key, obj): + """.""" + super().__init__( + f"Message missing required key/s {key}. Message received: {obj}", + ) + + +class ExperimentalFeatureWarning(UserWarning): + """.""" class Protocol: @@ -25,9 +62,9 @@ def verify_json(self, obj): required_keys = {"id": int, "method": str} for key, type_key in required_keys.items(): if key not in obj: - raise protocol.MissingKeyError(key, obj) + raise MissingKeyError(key, obj) if not isinstance(obj[key], type_key): - raise protocol.MessageTypeError(key, type(obj[key]), type_key) + raise MessageTypeError(key, type(obj[key]), type_key) n_keys += 2 if "params" in obj: From 34d30c391babaa1481e742aff5abcc9495a256ad Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Sun, 5 Jan 2025 21:30:03 -0500 Subject: [PATCH 11/88] Add factored sys into chromium.py --- choreographer/_browsers/chromium.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/choreographer/_browsers/chromium.py b/choreographer/_browsers/chromium.py index b6857d71..809d0852 100644 --- a/choreographer/_browsers/chromium.py +++ b/choreographer/_browsers/chromium.py @@ -9,6 +9,7 @@ import msvcrt from choreographer._channels.pipe import Pipe, WebSocket +from choreographer._sys_utils import get_browser_path class Chromium: @@ -25,7 +26,10 @@ def get_cli(self, temp_dir, **kwargs): raise RuntimeError( "Chromium.get_cli() received " f"invalid args: {kwargs.keys()}", ) - path = None # TODO(Andrew): not legit # noqa: FIX002,TD003 + path = get_browser_path() + if not path: + raise RuntimeError("Browser not found.") + chromium_wrapper_path = Path(__file__).resolve().parent / "chromium_wrapper.py" if platform.system() != "Windows": cli = [ From 4e6eee422cec1b517e290c4fe741ddc0b98f5ce3 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Sun, 5 Jan 2025 22:16:27 -0500 Subject: [PATCH 12/88] Refactor wire out of pipe --- choreographer/_channels/__init__.py | 9 ++++ choreographer/_channels/_errors.py | 10 +++++ choreographer/_channels/_wire.py | 35 +++++++++++++++ choreographer/_channels/pipe.py | 69 +++++++---------------------- 4 files changed, 71 insertions(+), 52 deletions(-) create mode 100644 choreographer/_channels/_errors.py create mode 100644 choreographer/_channels/_wire.py diff --git a/choreographer/_channels/__init__.py b/choreographer/_channels/__init__.py index e69de29b..c905250e 100644 --- a/choreographer/_channels/__init__.py +++ b/choreographer/_channels/__init__.py @@ -0,0 +1,9 @@ +from ._errors import BlockWarning, ChannelClosedError, JSONError +from .pipe import Pipe + +__all__ = [ + "BlockWarning", + "ChannelClosedError", + "JSONError", + "Pipe", +] diff --git a/choreographer/_channels/_errors.py b/choreographer/_channels/_errors.py new file mode 100644 index 00000000..e2f878b8 --- /dev/null +++ b/choreographer/_channels/_errors.py @@ -0,0 +1,10 @@ +class BlockWarning(UserWarning): + pass + + +class ChannelClosedError(IOError): + pass + + +class JSONError(RuntimeError): + pass diff --git a/choreographer/_channels/_wire.py b/choreographer/_channels/_wire.py new file mode 100644 index 00000000..3bf5ffda --- /dev/null +++ b/choreographer/_channels/_wire.py @@ -0,0 +1,35 @@ +import simplejson +from channel._errors import JSONError + + +class MultiEncoder(simplejson.JSONEncoder): + """Special json encoder for numpy types.""" + + def default(self, obj): + if hasattr(obj, "dtype") and obj.dtype.kind == "i" and obj.shape == (): + return int(obj) + elif hasattr(obj, "dtype") and obj.dtype.kind == "f" and obj.shape == (): + return float(obj) + elif hasattr(obj, "dtype") and obj.shape != (): + return obj.tolist() + elif hasattr(obj, "isoformat"): + return obj.isoformat() + return simplejson.JSONEncoder.default(self, obj) + + def serialize(self, obj): + try: + message = simplejson.dumps( + obj, + ensure_ascii=False, + ignore_nan=True, + cls=MultiEncoder, + ) + except simplejson.errors.JSONDecodeError as e: + raise JSONError from e + return message.encode("utf-8") + + def deserialize(self, message): + try: + return simplejson.loads(message) + except simplejson.errors.JSONDecodeError as e: + raise JSONError from e diff --git a/choreographer/_channels/pipe.py b/choreographer/_channels/pipe.py index 33862fa0..00e6ed1b 100644 --- a/choreographer/_channels/pipe.py +++ b/choreographer/_channels/pipe.py @@ -4,37 +4,15 @@ import warnings from threading import Lock -import simplejson +import channel._wire as wire +from channel._errors import BlockWarning, ChannelClosedError, JSONError -with_block = bool(sys.version_info[:3] >= (3, 12) or platform.system() != "Windows") - - -class PipeClosedError(IOError): - pass - - -class BlockWarning(UserWarning): - pass - - -class MultiEncoder(simplejson.JSONEncoder): - """Special json encoder for numpy types.""" - - def default(self, obj): - if hasattr(obj, "dtype") and obj.dtype.kind == "i" and obj.shape == (): - return int(obj) - elif hasattr(obj, "dtype") and obj.dtype.kind == "f" and obj.shape == (): - return float(obj) - elif hasattr(obj, "dtype") and obj.shape != (): - return obj.tolist() - elif hasattr(obj, "isoformat"): - return obj.isoformat() - return simplejson.JSONEncoder.default(self, obj) +_with_block = bool(sys.version_info[:3] >= (3, 12) or platform.system() != "Windows") # if we're a pipe we expect these public attributes class Pipe: - def __init__(self, *, debug=False, json_encoder=MultiEncoder): + def __init__(self, *, debug=False): # This is where pipe listens (from browser) # So pass the write to browser self._read_from_browser, self._write_from_browser = list(os.pipe()) @@ -54,45 +32,32 @@ def __init__(self, *, debug=False, json_encoder=MultiEncoder): # windows-style handles. But let another layer handle that. self.debug = debug # should be private - self.json_encoder = json_encoder # should be private # this is just a convenience to prevent multiple shutdowns self.shutdown_lock = Lock() # should be private - def serialize(self, obj): - message = simplejson.dumps( - obj, - ensure_ascii=False, - ignore_nan=True, - cls=self.json_encoder, - ) - return message.encode("utf-8") + b"\0" - - def deserialize(self, message): - return simplejson.loads(message) - def write_json(self, obj, debug=None): if self.shutdown_lock.locked(): - raise PipeClosedError + raise ChannelClosedError if not debug: debug = self.debug if debug: print(f"write_json: {obj}", file=sys.stderr) - encoded_message = self.serialize(obj) + encoded_message = wire.serialize(obj) + b"\0" if debug: print(f"write_json: {encoded_message}", file=sys.stderr) try: os.write(self._write_to_browser, encoded_message) except OSError as e: self.close() - raise PipeClosedError from e + raise ChannelClosedError from e if debug: print("wrote_json.", file=sys.stderr) def read_jsons(self, *, blocking=True, debug=None): # noqa: PLR0912, C901 branches, complexity if self.shutdown_lock.locked(): - raise PipeClosedError - if not with_block and not blocking: + raise ChannelClosedError + if not _with_block and not blocking: warnings.warn( # noqa: B028 "Windows python version < 3.12 does not support non-blocking", BlockWarning, @@ -106,11 +71,11 @@ def read_jsons(self, *, blocking=True, debug=None): # noqa: PLR0912, C901 branc ) jsons = [] try: - if with_block: + if _with_block: os.set_blocking(self._read_from_browser, blocking) except OSError as e: self.close() - raise PipeClosedError from e + raise ChannelClosedError from e try: raw_buffer = None # if we fail in read, we already defined raw_buffer = os.read( @@ -121,10 +86,10 @@ def read_jsons(self, *, blocking=True, debug=None): # noqa: PLR0912, C901 branc # we seem to need {bye} even if chrome closes NOTE if debug: print("read_jsons pipe was closed, raising", file=sys.stderr) - raise PipeClosedError + raise ChannelClosedError while raw_buffer[-1] != 0: # still not great, return what you have - if with_block: + if _with_block: os.set_blocking(self._read_from_browser, True) raw_buffer += os.read(self._read_from_browser, 10000) except BlockingIOError: @@ -136,7 +101,7 @@ def read_jsons(self, *, blocking=True, debug=None): # noqa: PLR0912, C901 branc if debug: print(f"caught OSError in read() {e!s}", file=sys.stderr) if not raw_buffer or raw_buffer == b"{bye}\n": - raise PipeClosedError from e + raise ChannelClosedError from e # this could be hard to test as it is a real OS corner case decoded_buffer = raw_buffer.decode("utf-8") if debug: @@ -144,8 +109,8 @@ def read_jsons(self, *, blocking=True, debug=None): # noqa: PLR0912, C901 branc for raw_message in decoded_buffer.split("\0"): if raw_message: try: - jsons.append(self.deserialize(raw_message)) - except simplejson.decoder.JSONDecodeError as e: + jsons.append(wire.deserialize(raw_message)) + except JSONError as e: if debug: print( f"Problem with {raw_message} in json: {e}", @@ -160,7 +125,7 @@ def read_jsons(self, *, blocking=True, debug=None): # noqa: PLR0912, C901 branc def _unblock_fd(self, fd): try: - if with_block: + if _with_block: os.set_blocking(fd, False) except BaseException as e: # noqa: BLE001 OS errors are not consistent, catch blind # also, best effort. From d7db1b5b5f8a52dedf2071594a78b58834547891 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Sun, 5 Jan 2025 22:42:43 -0500 Subject: [PATCH 13/88] Pull chrome constants out of which --- choreographer/_sys_utils/_chrome_constants.py | 32 +++++++++++++++++++ choreographer/_sys_utils/_which.py | 30 +---------------- 2 files changed, 33 insertions(+), 29 deletions(-) create mode 100644 choreographer/_sys_utils/_chrome_constants.py diff --git a/choreographer/_sys_utils/_chrome_constants.py b/choreographer/_sys_utils/_chrome_constants.py new file mode 100644 index 00000000..7dfae43f --- /dev/null +++ b/choreographer/_sys_utils/_chrome_constants.py @@ -0,0 +1,32 @@ +import os +import platform + +chromium_names = ["chromium", "chromium-browser"] + +chrome_names = [ + "chrome", + "Chrome", + "google-chrome", + "google-chrome-stable", + "Chrome.app", + "Google Chrome", + "Google Chrome.app", +].extend(chromium_names) + +typical_chrome_paths = None +if platform.system() == "Windows": + typical_chrome_paths = [ + r"c:\Program Files\Google\Chrome\Application\chrome.exe", + f"c:\\Users\\{os.environ.get('USER', 'default')}\\AppData\\" + "Local\\Google\\Chrome\\Application\\chrome.exe", + ] +elif platform.system() == "Linux": + typical_chrome_paths = [ + "/usr/bin/google-chrome-stable", + "/usr/bin/google-chrome", + "/usr/bin/chrome", + ] +else: # assume mac, or system == "Darwin" + typical_chrome_paths = [ + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + ] diff --git a/choreographer/_sys_utils/_which.py b/choreographer/_sys_utils/_which.py index dc68d979..50c639d1 100644 --- a/choreographer/_sys_utils/_which.py +++ b/choreographer/_sys_utils/_which.py @@ -4,35 +4,7 @@ from choreographer._cli_utils import get_chrome_download_path -chromium_names = ["chromium", "chromium-browser"] - -chrome_names = [ - "chrome", - "Chrome", - "google-chrome", - "google-chrome-stable", - "Chrome.app", - "Google Chrome", - "Google Chrome.app", -].extend(chromium_names) - -typical_chrome_paths = None -if platform.system() == "Windows": - typical_chrome_paths = [ - r"c:\Program Files\Google\Chrome\Application\chrome.exe", - f"c:\\Users\\{os.environ.get('USER', 'default')}\\AppData\\" - "Local\\Google\\Chrome\\Application\\chrome.exe", - ] -elif platform.system() == "Linux": - typical_chrome_paths = [ - "/usr/bin/google-chrome-stable", - "/usr/bin/google-chrome", - "/usr/bin/chrome", - ] -else: # assume mac, or system == "Darwin" - typical_chrome_paths = [ - "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", - ] +from ._chrome_constants import chrome_names, typical_chrome_paths def _is_exe(path): From 656cf71ae113ad50e315d59020e61bfb45ea5441 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Mon, 6 Jan 2025 09:05:24 -0500 Subject: [PATCH 14/88] Fix up namespaces and chromium.py _browsers/ interfaces have now changed a bit to 1. provide popen args 2. find their own path Plus we've exported a lot of needed symbols in various __init__.py's Plus organized and renamed some stuff among the tmpfiles --- choreographer/_browsers/__init__.py | 8 ++++++++ .../_chrome_constants.py | 0 choreographer/_browsers/_errors.py | 6 ++++++ choreographer/_sys_utils/__init__.py | 8 +++++--- choreographer/_sys_utils/_kill.py | 15 +++++++++++++++ .../_sys_utils/{_tempfile.py => _tmpfile.py} | 10 +++++----- choreographer/_sys_utils/_which.py | 8 +------- choreographer/protocol/__init__.py | 2 ++ 8 files changed, 42 insertions(+), 15 deletions(-) rename choreographer/{_sys_utils => _browsers}/_chrome_constants.py (100%) create mode 100644 choreographer/_browsers/_errors.py create mode 100644 choreographer/_sys_utils/_kill.py rename choreographer/_sys_utils/{_tempfile.py => _tmpfile.py} (95%) diff --git a/choreographer/_browsers/__init__.py b/choreographer/_browsers/__init__.py index e69de29b..a20568f6 100644 --- a/choreographer/_browsers/__init__.py +++ b/choreographer/_browsers/__init__.py @@ -0,0 +1,8 @@ +from ._errors import BrowserClosedError, BrowserFailedError +from .chromium import Chromium + +__all__ = [ + "BrowserClosedError", + "BrowserFailedError", + "Chromium", +] diff --git a/choreographer/_sys_utils/_chrome_constants.py b/choreographer/_browsers/_chrome_constants.py similarity index 100% rename from choreographer/_sys_utils/_chrome_constants.py rename to choreographer/_browsers/_chrome_constants.py diff --git a/choreographer/_browsers/_errors.py b/choreographer/_browsers/_errors.py new file mode 100644 index 00000000..b5b34c27 --- /dev/null +++ b/choreographer/_browsers/_errors.py @@ -0,0 +1,6 @@ +class BrowserFailedError(RuntimeError): + pass + + +class BrowserClosedError(RuntimeError): + pass diff --git a/choreographer/_sys_utils/__init__.py b/choreographer/_sys_utils/__init__.py index 7c5daf99..6f1f0414 100644 --- a/choreographer/_sys_utils/__init__.py +++ b/choreographer/_sys_utils/__init__.py @@ -1,9 +1,11 @@ -from ._tempfile import TempDirectory, TempDirWarning +from ._kill.py import kill +from ._tmpfile import TmpDirectory, TmpDirWarning from ._which import browser_which, get_browser_path __all__ = [ - "TempDirWarning", - "TempDirectory", + "TmpDirWarning", + "TmpDirectory", "browser_which", "get_browser_path", + "kill", ] diff --git a/choreographer/_sys_utils/_kill.py b/choreographer/_sys_utils/_kill.py new file mode 100644 index 00000000..bfc8c5bb --- /dev/null +++ b/choreographer/_sys_utils/_kill.py @@ -0,0 +1,15 @@ +import platform +import subprocess + + +def kill(process): + if platform.system() == "Windows": + subprocess.call( # noqa: S603, false positive, input fine + ["taskkill", "/F", "/T", "/PID", str(process.pid)], # noqa: S607 windows full path... + stderr=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + ) + else: + process.terminate() + if process.poll() is None: + process.kill() diff --git a/choreographer/_sys_utils/_tempfile.py b/choreographer/_sys_utils/_tmpfile.py similarity index 95% rename from choreographer/_sys_utils/_tempfile.py rename to choreographer/_sys_utils/_tmpfile.py index 771b57de..871f6405 100644 --- a/choreographer/_sys_utils/_tempfile.py +++ b/choreographer/_sys_utils/_tmpfile.py @@ -10,7 +10,7 @@ from threading import Thread -class TempDirWarning(UserWarning): +class TmpDirWarning(UserWarning): pass @@ -18,7 +18,7 @@ class TempDirWarning(UserWarning): # In short, they don't handle removal well, and there's # lots of API changes over recent versions. # Here we have our own class to deal with it. -class TempDirectory: +class TmpDirectory: def __init__(self, path=None, *, sneak=False): self.debug = True # temporary! TODO self._with_onexc = bool(sys.version_info[:3] >= (3, 12)) @@ -108,7 +108,7 @@ def delete_manually(self, *, check_only=False): # noqa: C901, PLR0912 warnings.warn( # noqa: B028 "The temporary directory could not be deleted, " f"execution will continue. errors: {errors}", - TempDirWarning, + TmpDirWarning, ) self.exists = True else: @@ -125,7 +125,7 @@ def clean(self): # noqa: C901 except BaseException as e: # noqa: BLE001 yes catch and report if self.debug: print( - f"First tempdir deletion failed: TempDirWarning: {e!s}", + f"First tempdir deletion failed: TmpDirWarning: {e!s}", file=sys.stderr, ) @@ -163,6 +163,6 @@ def extra_clean(): t.run() if self.debug: print( - f"Tempfile still exists?: {self.path.exists()!s}", + f"Tmpfile still exists?: {self.path.exists()!s}", file=sys.stderr, ) diff --git a/choreographer/_sys_utils/_which.py b/choreographer/_sys_utils/_which.py index 50c639d1..b6ea886f 100644 --- a/choreographer/_sys_utils/_which.py +++ b/choreographer/_sys_utils/_which.py @@ -4,8 +4,6 @@ from choreographer._cli_utils import get_chrome_download_path -from ._chrome_constants import chrome_names, typical_chrome_paths - def _is_exe(path): try: @@ -35,7 +33,7 @@ def _which_from_windows_reg(): return exe -def browser_which(executable_names=chrome_names, *, skip_local=False): # noqa: C901 +def browser_which(executable_names, *, skip_local=False): path = None if isinstance(executable_names, str): @@ -63,10 +61,6 @@ def browser_which(executable_names=chrome_names, *, skip_local=False): # noqa: # which didn't work # hail mary - if "chrome" in executable_names: - for candidate in typical_chrome_paths: - if _is_exe(candidate): - return candidate return None diff --git a/choreographer/protocol/__init__.py b/choreographer/protocol/__init__.py index 5a98d2f4..5e1860b1 100644 --- a/choreographer/protocol/__init__.py +++ b/choreographer/protocol/__init__.py @@ -6,6 +6,7 @@ ExperimentalFeatureWarning, MessageTypeError, MissingKeyError, + Protocol, ) from ._session import Session from ._target import Target @@ -16,6 +17,7 @@ "ExperimentalFeatureWarning", "MessageTypeError", "MissingKeyError", + "Protocol", "Session", "Target", ] From 73ff2e1dfdd8e93d2526fc7d742eb5b8002c7f5a Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Mon, 6 Jan 2025 09:29:15 -0500 Subject: [PATCH 15/88] Add dev readmes --- choreographer/README.txt | 10 ++++++++++ choreographer/__init__.py | 8 +++++++- choreographer/_brokers/README.txt | 13 +++++++++++++ choreographer/_browsers/README.txt | 12 ++++++++++++ choreographer/_channels/README.txt | 17 +++++++++++++++++ choreographer/_cli_utils/README.txt | 1 + choreographer/_sys_utils/README.txt | 7 +++++++ 7 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 choreographer/README.txt create mode 100644 choreographer/_brokers/README.txt create mode 100644 choreographer/_browsers/README.txt create mode 100644 choreographer/_channels/README.txt create mode 100644 choreographer/_cli_utils/README.txt create mode 100644 choreographer/_sys_utils/README.txt diff --git a/choreographer/README.txt b/choreographer/README.txt new file mode 100644 index 00000000..d73abad9 --- /dev/null +++ b/choreographer/README.txt @@ -0,0 +1,10 @@ +These READMEs (in directories) are development notes, think of them as additions +to the package-level docstrings (the one at the top of each __init__.py). + + +The browsers are the main entrypoint for the user. They: + +1. Manage the actual browser process (start/stop/kill). +2. Integrate the different resources: (see _channels/, _brokers/, _browsers/). +3. Is a list of `Tab`s, `Session`s, and `Target`s. +4. Provides a basic API. diff --git a/choreographer/__init__.py b/choreographer/__init__.py index fa047cc5..b61e52ae 100644 --- a/choreographer/__init__.py +++ b/choreographer/__init__.py @@ -1,4 +1,10 @@ -"""choreographer is a browser controller for python.""" +""" +choreographer is a browser controller for python. + +choreographer is natively async, so while there are two main entrypoints: +classes `Browser` and `BrowserSync`, the sync version is very limited, functioning +as a building block. +""" from choreographer import protocol diff --git a/choreographer/_brokers/README.txt b/choreographer/_brokers/README.txt new file mode 100644 index 00000000..9652ab6e --- /dev/null +++ b/choreographer/_brokers/README.txt @@ -0,0 +1,13 @@ +Brokers will handle all events and promises, if there are any. + +For example, the _sync.py broker only gives us the option of +starting a thread to dump all events. Writing a broker without +a concurrent structure would be fruitless- it would block and break. + + +The _async.py broker is much more complex, and will allow users to subscribe +to events, will fulfill processes when receiving responses from the browser +addressed to the user's commands, and generally absorbs all browser communication. + +Brokers do not have a standard interface, they are usually 1-1 matched with a particular +`class Browser*` implementation. diff --git a/choreographer/_browsers/README.txt b/choreographer/_browsers/README.txt new file mode 100644 index 00000000..0a42aa9a --- /dev/null +++ b/choreographer/_browsers/README.txt @@ -0,0 +1,12 @@ +Browser specific stuff is in here. + +This is not the users `Browser()`, which is abstract, this is +`Chromium()` or in the future `Firefox()`. It's an implementation +of a browser. + +The implementations provide `get_cli()`, `get_popen_args()`, `get_env()` which +`Browser()` uses to actually start the process. Browser will pass them all its +keyword arguments which it assumes are flags for starting browsers (they are +curated, not passed blindly). `Browser()` also gives each implementation a copy +of the channel being used, as the CLI arguments will change based on +information proovided by that. diff --git a/choreographer/_channels/README.txt b/choreographer/_channels/README.txt new file mode 100644 index 00000000..60174ed5 --- /dev/null +++ b/choreographer/_channels/README.txt @@ -0,0 +1,17 @@ +Browsers often accept two communication channels: websockets and pipes. + +Both classes we implement will support `write_jsons()` and `read_jsons()` +with the same interface (as well as `__init__()` and `close()`). + +But the browser implementation in _browsers/ will have to get specific +information from the pipe/websocket in order to properly build the CLI +command for the browser. + +For example, the CLI command needs to know the file numbers (file descriptors) +for reading writing if using `Pipe()`, so `Pipe()` has the attributes: +`.from_external_to_choreo` and `.from_choreo_to_external`. They're part of +the interface as well. + +In the same vein, when websockets in implemented, the CLI command will +need to know the port it's on. There may need to be a `open()` function for +after the CLI command is run. diff --git a/choreographer/_cli_utils/README.txt b/choreographer/_cli_utils/README.txt new file mode 100644 index 00000000..b500cbb4 --- /dev/null +++ b/choreographer/_cli_utils/README.txt @@ -0,0 +1 @@ +CLI tools provide various functions that are turned into scripts. diff --git a/choreographer/_sys_utils/README.txt b/choreographer/_sys_utils/README.txt new file mode 100644 index 00000000..d76759fe --- /dev/null +++ b/choreographer/_sys_utils/README.txt @@ -0,0 +1,7 @@ +sys_utils provide: + +a `which_browser()` function for finding the browser + +a more robust `TmpDirectory` class for creating and managing those + +a `kill()` function to be used when destroying processes. From 3d30378dddbbc52fa27d97f61321e469b6e6254a Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Mon, 6 Jan 2025 09:45:40 -0500 Subject: [PATCH 16/88] Create basic broker subpackage --- choreographer/_brokers/__init__.py | 5 ++ choreographer/_brokers/_sync.py | 23 ++++++++ choreographer/_browser_base.py | 21 +++++++ choreographer/_browser_sync.py | 91 ++++++++++++++++++++++++++++++ 4 files changed, 140 insertions(+) create mode 100644 choreographer/_brokers/__init__.py create mode 100644 choreographer/_brokers/_sync.py create mode 100644 choreographer/_browser_base.py create mode 100644 choreographer/_browser_sync.py diff --git a/choreographer/_brokers/__init__.py b/choreographer/_brokers/__init__.py new file mode 100644 index 00000000..c7e2efbc --- /dev/null +++ b/choreographer/_brokers/__init__.py @@ -0,0 +1,5 @@ +from ._sync import BrokerSync + +__all__ = [ + "BrokerSync", +] diff --git a/choreographer/_brokers/_sync.py b/choreographer/_brokers/_sync.py new file mode 100644 index 00000000..9b8629cf --- /dev/null +++ b/choreographer/_brokers/_sync.py @@ -0,0 +1,23 @@ +import json +from threading import Thread + +from ._channels import ChannelClosedError + + +class BrokerSync: + def __init__(self, browser, channel): + self.browser = browser + self.channel = channel + + # This is the broker + def run_output_thread(self, **kwargs): + def run_print(): + try: + while True: + responses = self.channel.read_jsons() + for response in responses: + print(json.dumps(response, indent=4), **kwargs) + except ChannelClosedError: + print("ChannelClosedError caught", **kwargs) + + Thread(target=run_print).start() diff --git a/choreographer/_browser_base.py b/choreographer/_browser_base.py new file mode 100644 index 00000000..3c57d492 --- /dev/null +++ b/choreographer/_browser_base.py @@ -0,0 +1,21 @@ +from ._tab import Tab + + +class BrowserBase: + def __init__(self): + self.tabs = {} + + def _add_tab(self, tab): + if not isinstance(tab, Tab): + raise TypeError("tab must be an object of class Tab") + self.tabs[tab.target_id] = tab + + def _remove_tab(self, target_id): + if isinstance(target_id, Tab): + target_id = target_id.target_id + del self.tabs[target_id] + + def get_tab(self): + if self.tabs.values(): + return next(iter(self.tabs.values())) + return None diff --git a/choreographer/_browser_sync.py b/choreographer/_browser_sync.py new file mode 100644 index 00000000..0328cdde --- /dev/null +++ b/choreographer/_browser_sync.py @@ -0,0 +1,91 @@ +import subprocess + +import logistro + +from ._brokers import BrokerSync +from ._browser_base import BrowserBase +from ._browsers import BrowserClosedError, BrowserFailedError, Chromium +from ._channels import ChannelClosedError, Pipe +from ._sys_utils import kill +from .protocol._target import Session, Target + +logger = logistro.getLogger(__name__) + +# TODO: protocol a commin' + + +class BrowserSync(BrowserBase, Target): + """`BrowserSync` is the sync implementation of `Browser`.""" + + def __init__(self, path=None, *, browser_cls=Chromium, channel_cls=Pipe, **kwargs): + # Compose Resources + self.channel = channel_cls() + self.broker = BrokerSync(self, self.channel) + self.browser_impl = browser_cls(self.channel, path, **kwargs) + + # Explicit supers are better IMO + super(BrowserBase, self).__init__() + super(Target, self).__init__("0", self) + self._add_session(Session(self, "")) + + def open(self): + self.subprocess = subprocess.Popen( # noqa: S603 + self.browser_impl.get_cli(), + # stderr= TODO make a pipe with logistro + env=self.browser_impl.get_env(), + **self.browser_impl.get_popen_args(), + ) + # start a watchdock + # open can only be run once? + # or depends on lock + + def __enter__(self): + self.open() + return self + + def _is_closed(self, wait=0): + if wait == 0: + return self.subprocess.poll() is None + else: + try: + self.subprocess.wait(wait) + except subprocess.TimeoutExpired: + return False + return True + + def _close(self): + if self._is_closed(): + return + + try: + self.send_command("Browser.close") + except (BrowserClosedError, BrowserFailedError): + return + except ChannelClosedError: + pass + + self.channel.close() + if self._is_closed(): + return + + # try kiling + kill(self.subprocess) + if self._is_closed(wait=4): + return + else: + raise RuntimeError("Couldn't close or kill browser subprocess") + + def close(self): + try: + self._close() + except ProcessLookupError: + pass + self.channel.close() + self.browser_impl.clean() + + def __exit__(self, exc_type, exc_value, exc_traceback): + self.close() + + # wrap our broker for convenience + def start_output_thread(self, **kwargs): + self.broker.run_output_thread(**kwargs) From 239e0d6af14ca684d02c556b78e6284041d7fd0e Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Mon, 6 Jan 2025 10:16:38 -0500 Subject: [PATCH 17/88] Init session after open --- choreographer/_browser_base.py | 2 +- choreographer/_browser_sync.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/choreographer/_browser_base.py b/choreographer/_browser_base.py index 3c57d492..90f523bc 100644 --- a/choreographer/_browser_base.py +++ b/choreographer/_browser_base.py @@ -7,7 +7,7 @@ def __init__(self): def _add_tab(self, tab): if not isinstance(tab, Tab): - raise TypeError("tab must be an object of class Tab") + raise TypeError("tab must be an object of (sub)class Tab") self.tabs[tab.target_id] = tab def _remove_tab(self, target_id): diff --git a/choreographer/_browser_sync.py b/choreographer/_browser_sync.py index 0328cdde..a8f73651 100644 --- a/choreographer/_browser_sync.py +++ b/choreographer/_browser_sync.py @@ -18,6 +18,7 @@ class BrowserSync(BrowserBase, Target): """`BrowserSync` is the sync implementation of `Browser`.""" def __init__(self, path=None, *, browser_cls=Chromium, channel_cls=Pipe, **kwargs): + self.tabs = {} # Compose Resources self.channel = channel_cls() self.broker = BrokerSync(self, self.channel) @@ -25,8 +26,7 @@ def __init__(self, path=None, *, browser_cls=Chromium, channel_cls=Pipe, **kwarg # Explicit supers are better IMO super(BrowserBase, self).__init__() - super(Target, self).__init__("0", self) - self._add_session(Session(self, "")) + # we don't init ourselves as a target until we open? def open(self): self.subprocess = subprocess.Popen( # noqa: S603 @@ -35,6 +35,8 @@ def open(self): env=self.browser_impl.get_env(), **self.browser_impl.get_popen_args(), ) + super(Target, self).__init__("0", self) + self._add_session(Session(self, "")) # start a watchdock # open can only be run once? # or depends on lock From 4d8472b50ddca41473553d08d2db345e715ce2d2 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Mon, 6 Jan 2025 10:29:43 -0500 Subject: [PATCH 18/88] Remove async version of browser before test --- choreographer/_browser.py | 748 -------------------------------------- 1 file changed, 748 deletions(-) delete mode 100644 choreographer/_browser.py diff --git a/choreographer/_browser.py b/choreographer/_browser.py deleted file mode 100644 index b0448211..00000000 --- a/choreographer/_browser.py +++ /dev/null @@ -1,748 +0,0 @@ -import asyncio -import io -import json -import os -import platform -import subprocess -import sys -import warnings -from collections import OrderedDict -from functools import partial -from pathlib import Path -from threading import Thread - -from ._pipe import Pipe, PipeClosedError -from ._system_utils._system import browser_which -from ._system_utils._tempfile import TempDirectory, TempDirWarning -from ._tab import Tab -from .protocol._protocol import ( - TARGET_NOT_FOUND, - DevtoolsProtocolError, - ExperimentalFeatureWarning, - Protocol, -) -from .protocol._session import Session -from .protocol._target import Target - -# importing the below via __file__ causes __name__ weirdness when its exe'd ??? -chromewrapper_path = ( - Path(__file__).resolve().parent / "_system_utils" / "_chrome_wrapper.py" -) - - -class UnhandledMessageWarning(UserWarning): - pass - - -class BrowserFailedError(RuntimeError): - pass - - -class BrowserClosedError(RuntimeError): - pass - - -def get_browser_path(**kwargs): - return os.environ.get("BROWSER_PATH", browser_which(**kwargs)) - - -class Browser(Target): - # Some frameworks configure windows use SelectorEventLoop, which lacks - # certain features, so we need to know. - def _check_loop(self): - # Lock - if not self.lock: - self.lock = asyncio.Lock() - if ( - platform.system() == "Windows" - and self.loop - and isinstance(self.loop, asyncio.SelectorEventLoop) - ): - if self.debug: - print("We are in a selector event loop, use loop_hack", file=sys.stderr) - self._loop_hack = True - - def __init__( # noqa: PLR0915, PLR0912, C901 It's too complex - self, - path=None, - *, - headless=True, - debug=False, - debug_browser=False, - **kwargs, - ): - ### Set some defaults - self._env = os.environ.copy() # environment for subprocesses - self._loop_hack = False # see _check_loop - self.lock = None - self.tabs = OrderedDict() - self.sandboxed = False # this is if our processes can't use /tmp - - # Browser Configuration - self.enable_sandbox = kwargs.pop("enable_sandbox", False) - if self.enable_sandbox: - self._env["SANDBOX_ENABLED"] = "true" - if not path: - skip_local = bool( - "ubuntu" in platform.version().lower() and self.enable_sandbox, - ) - path = get_browser_path(skip_local=skip_local) - if not path: - raise BrowserFailedError( - "Could not find an acceptable browser. Please call " - "`choreo.get_browser()`, set environmental variable " - "BROWSER_PATH or pass `path=/path/to/browser` into " - "the Browser() constructor. The latter two work with Edge.", - ) - if "snap" in str(path): - self.sandboxed = True # not chromium sandbox, snap sandbox - self._env["BROWSER_PATH"] = str(path) - self.headless = headless - if headless: - self._env["HEADLESS"] = "--headless" - self.debug = debug - self.enable_gpu = kwargs.pop("enable_gpu", False) - if self.enable_gpu: - self._env["GPU_ENABLED"] = "true" - - # Expert Configuration - tmp_path = kwargs.pop("tmp_path", None) - self.tmp_dir = TempDirectory(tmp_path, sneak=self.sandboxed) - - try: - self.loop = kwargs.pop("loop", asyncio.get_running_loop()) - except RuntimeError: - self.loop = False - if self.loop: - self.futures = {} - self._check_loop() - self.executor = kwargs.pop("executor", None) - - if len(kwargs): - raise ValueError(f"Unknown keyword arguments: {kwargs}") - - # Set up stderr - if debug_browser is False: # false o None - stderr = subprocess.DEVNULL - elif debug_browser is True: - stderr = sys.stderr - else: - stderr = debug_browser - - if ( - stderr - and stderr not in (subprocess.PIPE, subprocess.STDOUT, subprocess.DEVNULL) - and not isinstance(stderr, int) - ): - try: - stderr.fileno() - except io.UnsupportedOperation: - warnings.warn( # noqa: B028 - "A value has been passed to debug_browser which " - "is not compatible with python's Popen. This may " - "be because one was passed to Browser or because " - "sys.stderr has been overridden by a framework. " - "Browser logs will not be handled by python in this case.", - ) - stderr = None - - self._stderr = stderr - - if debug: - print(f"STDERR: {stderr}", file=sys.stderr) - - self._env["USER_DATA_DIR"] = str(self.tmp_dir.path) - - # Compose Resources - self.pipe = Pipe(debug=debug) - self.protocol = Protocol(debug=debug) - - # Initializing - super().__init__("0", self) # NOTE: 0 can't really be used externally - self._add_session(Session(self, "")) - - if self.debug: - print("DEBUG REPORT:", file=sys.stderr) - print(f"BROWSER_PATH: {self._env['BROWSER_PATH']}", file=sys.stderr) - print(f"USER_DATA_DIR: {self._env['USER_DATA_DIR']}", file=sys.stderr) - if not self.loop: - self._open() - - # for use with `async with Browser()` - def __aenter__(self): - self.future_self = self.loop.create_future() - self.loop.create_task(self._open_async()) - self.run_read_loop() - return self.future_self - - def __enter__(self): - return self - - # for use with `await Browser()` - def __await__(self): - return self.__aenter__().__await__() - - # ? why have to call __await__ when __aenter__ returns a future - - def _open(self): - if platform.system() != "Windows": - self.subprocess = subprocess.Popen( # noqa: S603, false positive, input fine - [ - sys.executable, - chromewrapper_path, - ], - close_fds=True, - stdin=self.pipe.read_to_chromium, - stdout=self.pipe.write_from_chromium, - stderr=self._stderr, - env=self._env, - ) - else: - from ._system_utils._chrome_wrapper import open_browser - - self.subprocess = open_browser( - to_chromium=self.pipe.read_to_chromium, - from_chromium=self.pipe.write_from_chromium, - stderr=self._stderr, - env=self._env, - loop_hack=self._loop_hack, - ) - - async def _open_async(self): - try: - if platform.system() != "Windows": - self.subprocess = await asyncio.create_subprocess_exec( - sys.executable, - chromewrapper_path, - stdin=self.pipe.read_to_chromium, - stdout=self.pipe.write_from_chromium, - stderr=self._stderr, - close_fds=True, - env=self._env, - ) - else: - from ._system_utils._chrome_wrapper import open_browser - - self.subprocess = await open_browser( - to_chromium=self.pipe.read_to_chromium, - from_chromium=self.pipe.write_from_chromium, - stderr=self._stderr, - env=self._env, - loop=True, - loop_hack=self._loop_hack, - ) - self.loop.create_task(self._watchdog()) - await self.populate_targets() - self.future_self.set_result(self) - except (BrowserClosedError, BrowserFailedError, asyncio.CancelledError) as e: - raise BrowserFailedError( - "The browser seemed to close immediately after starting. " - "Perhaps adding debug_browser=True will help.", - ) from e - - async def _is_closed_async(self, wait=0): - if self.debug: - print(f"is_closed called with wait: {wait}", file=sys.stderr) - if self._loop_hack: - # Use synchronous tools in thread - return await asyncio.to_thread(self._is_closed, wait) - waiter = self.subprocess.wait() - try: - if wait == 0: # this never works cause processing - wait = 0.15 - await asyncio.wait_for(waiter, wait) - except TimeoutError: - return False - return True - - def _is_closed(self, wait=0): - if wait == 0: - return self.subprocess.poll() is None - else: - try: - self.subprocess.wait(wait) - except subprocess.TimeoutExpired: - return False - return True - - # _sync_close and _async_close are basically the same thing - - def _sync_close(self): # noqa: PLR0912, C901 - if self._is_closed(): - if self.debug: - print("Browser was already closed.", file=sys.stderr) - return - # check if no sessions or targets - self.send_command("Browser.close") - if self._is_closed(): - if self.debug: - print("Browser.close method closed browser", file=sys.stderr) - return - self.pipe.close() - if self._is_closed(wait=1): - if self.debug: - print( - "pipe.close() (or slow Browser.close) method closed browser", - file=sys.stderr, - ) - return - - # Start a kill - if platform.system() == "Windows": - if not self._is_closed(): - subprocess.call( # noqa: S603, false positive, input fine - ["taskkill", "/F", "/T", "/PID", str(self.subprocess.pid)], # noqa: S607 windows full path... - stderr=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, - ) - if self._is_closed(wait=4): - return - else: - raise RuntimeError("Couldn't kill browser subprocess") - else: - self.subprocess.terminate() - if self._is_closed(): - if self.debug: - print("terminate() closed the browser", file=sys.stderr) - return - - self.subprocess.kill() - if self._is_closed(wait=4) and self.debug: - print("kill() closed the browser", file=sys.stderr) - return - - async def _async_close(self): # noqa: PLR0912, C901 - if await self._is_closed_async(): - if self.debug: - print("Browser was already closed.", file=sys.stderr) - return - await asyncio.wait([self.send_command("Browser.close")], timeout=1) - if await self._is_closed_async(): - if self.debug: - print("Browser.close method closed browser", file=sys.stderr) - return - self.pipe.close() - if await self._is_closed_async(wait=1): - if self.debug: - print("pipe.close() method closed browser", file=sys.stderr) - return - - # Start a kill - if platform.system() == "Windows": - if not await self._is_closed_async(): - # could we use native asyncio process here? or hackcheck? - await asyncio.to_thread( - subprocess.call, - ["taskkill", "/F", "/T", "/PID", str(self.subprocess.pid)], - stderr=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, - ) - if await self._is_closed_async(wait=4): - return - else: - raise RuntimeError("Couldn't kill browser subprocess") - else: - self.subprocess.terminate() - if await self._is_closed_async(): - if self.debug: - print("terminate() closed the browser", file=sys.stderr) - return - - self.subprocess.kill() - if await self._is_closed_async(wait=4) and self.debug: - print("kill() closed the browser", file=sys.stderr) - return - - def close(self): # noqa: C901 - if self.loop: - - async def close_task(): # noqa: PLR0912, C901 - if self.lock.locked(): - return - await self.lock.acquire() - if not self.future_self.done(): - self.future_self.set_exception( - BrowserFailedError( - "Close() was called before the browser " - "finished opening- maybe it crashed?", - ), - ) - for future in self.futures.values(): - if future.done(): - continue - future.set_exception( - BrowserClosedError( - "Command not completed because browser closed.", - ), - ) - for session in self.sessions.values(): - for futures in session.subscriptions_futures.values(): - for future in futures: - if future.done(): - continue - future.set_exception( - BrowserClosedError( - "Event not complete because browser closed.", - ), - ) - for tab in self.tabs.values(): - for session in tab.sessions.values(): - for futures in session.subscriptions_futures.values(): - for future in futures: - if future.done(): - continue - future.set_exception( - BrowserClosedError( - "Event not completed because browser closed.", - ), - ) - try: - await self._async_close() - except ProcessLookupError: - pass - self.pipe.close() - self.tmp_dir.clean() - - return asyncio.create_task(close_task()) - else: - try: - self._sync_close() - except ProcessLookupError: - pass - self.pipe.close() - self.tmp_dir.clean() - return None - - async def _watchdog(self): - self._watchdog_healthy = True - if self.debug: - print("Starting watchdog", file=sys.stderr) - await self.subprocess.wait() - if self.lock.locked(): - return - self._watchdog_healthy = False - if self.debug: - print("Browser is being closed because chrom* closed", file=sys.stderr) - await self.close() - await asyncio.sleep(1) - with warnings.catch_warnings(): - # ignore warnings here because - # watchdog killing is last resort - # and can leaves stuff in weird state - warnings.filterwarnings("ignore", category=TempDirWarning) - self.tmp_dir.clean() - if self.tmp_dir.exists: - self.tmp_dir.delete_manually() - - def __exit__(self, exc_type, exc_value, exc_traceback): - self.close() - - async def __aexit__(self, exc_type, exc_value, exc_traceback): - await self.close() - - # Basic synchronous functions - def _add_tab(self, tab): - if not isinstance(tab, Tab): - raise TypeError("tab must be an object of class Tab") - self.tabs[tab.target_id] = tab - - def _remove_tab(self, target_id): - if isinstance(target_id, Tab): - target_id = target_id.target_id - del self.tabs[target_id] - - def get_tab(self): - if self.tabs.values(): - return next(iter(self.tabs.values())) - return None - - # Better functions that require asynchronous - async def create_tab(self, url="", width=None, height=None): - if self.lock.locked(): - raise BrowserClosedError("create_tab() called on a closed browser.") - if self.headless and (width or height): - warnings.warn( # noqa: B028 - "Width and height only work for headless chrome mode, " - "they will be ignored.", - ) - width = None - height = None - params = {"url": url} - if width: - params["width"] = width - if height: - params["height"] = height - - response = await self.browser.send_command("Target.createTarget", params=params) - if "error" in response: - raise RuntimeError("Could not create tab") from DevtoolsProtocolError( - response, - ) - target_id = response["result"]["targetId"] - new_tab = Tab(target_id, self) - self._add_tab(new_tab) - await new_tab.create_session() - return new_tab - - async def close_tab(self, target_id): - if self.lock.locked(): - raise BrowserClosedError("close_tab() called on a closed browser") - if isinstance(target_id, Target): - target_id = target_id.target_id - # NOTE: we don't need to manually remove sessions because - # sessions are intrinisically handled by events - response = await self.send_command( - command="Target.closeTarget", - params={"targetId": target_id}, - ) - self._remove_tab(target_id) - if "error" in response: - raise RuntimeError("Could not close tab") from DevtoolsProtocolError( - response, - ) - return response - - async def create_session(self): - if self.lock.locked(): - raise BrowserClosedError("create_session() called on a closed browser") - warnings.warn( # noqa: B028 - "Creating new sessions on Browser() only works with some " - "versions of Chrome, it is experimental.", - ExperimentalFeatureWarning, - ) - response = await self.browser.send_command("Target.attachToBrowserTarget") - if "error" in response: - raise RuntimeError("Could not create session") from DevtoolsProtocolError( - response, - ) - session_id = response["result"]["sessionId"] - new_session = Session(self, session_id) - self._add_session(new_session) - return new_session - - async def populate_targets(self): - if self.lock.locked(): - raise BrowserClosedError("populate_targets() called on a closed browser") - response = await self.browser.send_command("Target.getTargets") - if "error" in response: - raise RuntimeError("Could not get targets") from Exception( - response["error"], - ) - - for json_response in response["result"]["targetInfos"]: - if ( - json_response["type"] == "page" - and json_response["targetId"] not in self.tabs - ): - target_id = json_response["targetId"] - new_tab = Tab(target_id, self) - try: - await new_tab.create_session() - except DevtoolsProtocolError as e: - if e.code == TARGET_NOT_FOUND: - if self.debug: - print( - f"Target {target_id} not found " - "(could be closed before)", - file=sys.stderr, - ) - continue - else: - raise - self._add_tab(new_tab) - if self.debug: - print(f"The target {target_id} was added", file=sys.stderr) - - # Output Helper for Debugging - def run_output_thread(self, debug=None): - if self.loop: - raise ValueError("You must use this method without loop in the Browser") - if not debug: - debug = self.debug - - def run_print(debug): - if debug: - print("Starting run_print loop", file=sys.stderr) - try: - while True: - responses = self.pipe.read_jsons(debug=debug) - for response in responses: - print(json.dumps(response, indent=4)) - except PipeClosedError: - if self.debug: - print("PipeClosedError caught", file=sys.stderr) - - Thread(target=run_print, args=(debug,)).start() - - def _get_target_for_session(self, session_id): - for tab in self.tabs.values(): - if session_id in tab.sessions: - return tab - if session_id in self.sessions: - return self - return None - - def run_read_loop(self): # noqa: PLR0915, C901 complexity - def check_error(result): - e = result.exception() - if e: - self.close() - if self.debug: - print(f"Error in run_read_loop: {e!s}", file=sys.stderr) - if not isinstance(e, asyncio.CancelledError): - raise e - - async def read_loop(): # noqa: PLR0915, PLR0912, C901 complexity - try: - read_jsons = partial( - self.pipe.read_jsons, - blocking=True, - debug=self.debug, - ) - responses = await self.loop.run_in_executor( - self.executor, - read_jsons, - ) - for response in responses: - error = self.protocol.get_error(response) - key = self.protocol.calculate_key(response) - if not self.protocol.has_id(response) and error: - raise DevtoolsProtocolError(response) - elif self.protocol.is_event(response): - event_session_id = response.get( - "sessionId", - "", - ) - event_session = self.protocol.sessions[event_session_id] - - subscriptions_futures = event_session.subscriptions_futures - - ### THIS IS FOR SUBSCRIBE(repeating=True|False) - for query in list(event_session.subscriptions): - match = ( - query.endswith("*") - and response["method"].startswith(query[:-1]) - ) or (response["method"] == query) - if self.debug: - print( - f"Checking subscription key: {query} " - "against event method {response['method']}", - file=sys.stderr, - ) - if match: - self.loop.create_task( - event_session.subscriptions[query][0](response), - ) - if not event_session.subscriptions[query][1]: - self.protocol.sessions[ - event_session_id - ].unsubscribe(query) - - ### THIS IS FOR SUBSCRIBE_ONCE - for query, futures in list(subscriptions_futures.items()): - match = ( - query.endswith("*") - and response["method"].startswith(query[:-1]) - ) or (response["method"] == query) - if self.debug: - print( - f"Checking subscription key: {query} against " - "event method {response['method']}", - file=sys.stderr, - ) - if match: - for future in futures: - if self.debug: - print( - f"Processing future {id(future)}", - file=sys.stderr, - ) - future.set_result(response) - if self.debug: - print( - f"Future resolved with response {future}", - file=sys.stderr, - ) - del event_session.subscriptions_futures[query] - - ### Check for closed sessions - if response["method"] == "Target.detachedFromTarget": - session_closed = response["params"].get( - "sessionId", - "", - ) - if session_closed == "": - continue # browser closing anyway - target_closed = self._get_target_for_session(session_closed) - if target_closed: - target_closed._remove_session(session_closed) # noqa: SLF001 - # TODO(Andrew): private access # noqa: FIX002, TD003 - - _ = self.protocol.sessions.pop(session_closed, None) - if self.debug: - print( - "Use intern subscription key: " - "'Target.detachedFromTarget'. " - f"Session {session_closed} was closed.", - file=sys.stderr, - ) - - elif key: - future = None - if key in self.futures: - if self.debug: - print( - f"run_read_loop() found future for key {key}", - file=sys.stderr, - ) - future = self.futures.pop(key) - elif "error" in response: - raise DevtoolsProtocolError(response) - else: - raise RuntimeError(f"Couldn't find a future for key: {key}") - if error: - future.set_result(response) - else: - future.set_result(response) - else: - warnings.warn( # noqa: B028 - f"Unhandled message type:{response!s}", - UnhandledMessageWarning, - ) - except PipeClosedError: - if self.debug: - print("PipeClosedError caught", file=sys.stderr) - return - f = self.loop.create_task(read_loop()) - f.add_done_callback(check_error) - - f = self.loop.create_task(read_loop()) - f.add_done_callback(check_error) - - def write_json(self, obj): - self.protocol.verify_json(obj) - key = self.protocol.calculate_key(obj) - if self.loop: - future = self.loop.create_future() - self.futures[key] = future - res = self.loop.run_in_executor( - self.executor, - self.pipe.write_json, - obj, - ) # ignore result - - def check_future(fut): - if fut.exception(): - if self.debug: - print(f"Write json future error: {fut!s}", file=sys.stderr) - if not future.done(): - print("Setting future based on pipe error", file=sys.stderr) - future.set_exception(fut.exception()) - print("Exception set", file=sys.stderr) - self.close() - - res.add_done_callback(check_future) - return future - else: - self.pipe.write_json(obj) - return key From 5bde5fa4d9e3f40d6607cd1b7f54917b4cfd3ec0 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Mon, 6 Jan 2025 12:06:42 -0500 Subject: [PATCH 19/88] Refactor sync --- choreographer/_brokers/_sync.py | 8 ++ choreographer/_browser_base.py | 21 ---- choreographer/_browser_sync.py | 93 --------------- choreographer/_tab.py | 9 -- choreographer/browser_sync.py | 176 ++++++++++++++++++++++++++++ choreographer/protocol.py | 117 ++++++++++++++++++ choreographer/protocol/__init__.py | 23 ---- choreographer/protocol/_protocol.py | 119 ------------------- choreographer/protocol/_session.py | 58 --------- choreographer/protocol/_target.py | 102 ---------------- 10 files changed, 301 insertions(+), 425 deletions(-) delete mode 100644 choreographer/_browser_base.py delete mode 100644 choreographer/_browser_sync.py delete mode 100644 choreographer/_tab.py create mode 100644 choreographer/browser_sync.py create mode 100644 choreographer/protocol.py delete mode 100644 choreographer/protocol/__init__.py delete mode 100644 choreographer/protocol/_protocol.py delete mode 100644 choreographer/protocol/_session.py delete mode 100644 choreographer/protocol/_target.py diff --git a/choreographer/_brokers/_sync.py b/choreographer/_brokers/_sync.py index 9b8629cf..5389d8bf 100644 --- a/choreographer/_brokers/_sync.py +++ b/choreographer/_brokers/_sync.py @@ -1,6 +1,8 @@ import json from threading import Thread +from choreographer import protocol + from ._channels import ChannelClosedError @@ -21,3 +23,9 @@ def run_print(): print("ChannelClosedError caught", **kwargs) Thread(target=run_print).start() + + def send_json(self, obj): + protocol.verify_params(obj) + key = protocol.calculate_message_key(obj) + self.channel.write_json(obj) + return key diff --git a/choreographer/_browser_base.py b/choreographer/_browser_base.py deleted file mode 100644 index 90f523bc..00000000 --- a/choreographer/_browser_base.py +++ /dev/null @@ -1,21 +0,0 @@ -from ._tab import Tab - - -class BrowserBase: - def __init__(self): - self.tabs = {} - - def _add_tab(self, tab): - if not isinstance(tab, Tab): - raise TypeError("tab must be an object of (sub)class Tab") - self.tabs[tab.target_id] = tab - - def _remove_tab(self, target_id): - if isinstance(target_id, Tab): - target_id = target_id.target_id - del self.tabs[target_id] - - def get_tab(self): - if self.tabs.values(): - return next(iter(self.tabs.values())) - return None diff --git a/choreographer/_browser_sync.py b/choreographer/_browser_sync.py deleted file mode 100644 index a8f73651..00000000 --- a/choreographer/_browser_sync.py +++ /dev/null @@ -1,93 +0,0 @@ -import subprocess - -import logistro - -from ._brokers import BrokerSync -from ._browser_base import BrowserBase -from ._browsers import BrowserClosedError, BrowserFailedError, Chromium -from ._channels import ChannelClosedError, Pipe -from ._sys_utils import kill -from .protocol._target import Session, Target - -logger = logistro.getLogger(__name__) - -# TODO: protocol a commin' - - -class BrowserSync(BrowserBase, Target): - """`BrowserSync` is the sync implementation of `Browser`.""" - - def __init__(self, path=None, *, browser_cls=Chromium, channel_cls=Pipe, **kwargs): - self.tabs = {} - # Compose Resources - self.channel = channel_cls() - self.broker = BrokerSync(self, self.channel) - self.browser_impl = browser_cls(self.channel, path, **kwargs) - - # Explicit supers are better IMO - super(BrowserBase, self).__init__() - # we don't init ourselves as a target until we open? - - def open(self): - self.subprocess = subprocess.Popen( # noqa: S603 - self.browser_impl.get_cli(), - # stderr= TODO make a pipe with logistro - env=self.browser_impl.get_env(), - **self.browser_impl.get_popen_args(), - ) - super(Target, self).__init__("0", self) - self._add_session(Session(self, "")) - # start a watchdock - # open can only be run once? - # or depends on lock - - def __enter__(self): - self.open() - return self - - def _is_closed(self, wait=0): - if wait == 0: - return self.subprocess.poll() is None - else: - try: - self.subprocess.wait(wait) - except subprocess.TimeoutExpired: - return False - return True - - def _close(self): - if self._is_closed(): - return - - try: - self.send_command("Browser.close") - except (BrowserClosedError, BrowserFailedError): - return - except ChannelClosedError: - pass - - self.channel.close() - if self._is_closed(): - return - - # try kiling - kill(self.subprocess) - if self._is_closed(wait=4): - return - else: - raise RuntimeError("Couldn't close or kill browser subprocess") - - def close(self): - try: - self._close() - except ProcessLookupError: - pass - self.channel.close() - self.browser_impl.clean() - - def __exit__(self, exc_type, exc_value, exc_traceback): - self.close() - - # wrap our broker for convenience - def start_output_thread(self, **kwargs): - self.broker.run_output_thread(**kwargs) diff --git a/choreographer/_tab.py b/choreographer/_tab.py deleted file mode 100644 index 078922e0..00000000 --- a/choreographer/_tab.py +++ /dev/null @@ -1,9 +0,0 @@ -from .protocol._target import Target - - -class Tab(Target): - def __init__(self, target_id, browser): - super().__init__(target_id, browser) - - async def close(self): - return await self.browser.close_tab(target_id=self.target_id) diff --git a/choreographer/browser_sync.py b/choreographer/browser_sync.py new file mode 100644 index 00000000..fa846c54 --- /dev/null +++ b/choreographer/browser_sync.py @@ -0,0 +1,176 @@ +import subprocess + +import logistro + +from ._brokers import BrokerSync +from ._browsers import BrowserClosedError, BrowserFailedError, Chromium +from ._channels import ChannelClosedError, Pipe +from ._sys_utils import kill + +logger = logistro.getLogger(__name__) + + +class SessionSync: + def __init__(self, browser, session_id): + if not isinstance(session_id, str): + raise TypeError("session_id must be a string") + # Resources + self.browser = browser + + # State + self.session_id = session_id + self.message_id = 0 + + def send_command(self, command, params=None): + current_id = self.message_id + self.message_id += 1 + json_command = { + "id": current_id, + "method": command, + } + + if self.session_id: + json_command["sessionId"] = self.session_id + if params: + json_command["params"] = params + + return self.browser.broker.send_json(json_command) + + +class TargetSync: + _session_type = SessionSync + + def __init__(self, target_id, browser): + if not isinstance(target_id, str): + raise TypeError("target_id must be string") + # Resources + self.browser = browser + + # States + self.sessions = {} + self.target_id = target_id + + def _add_session(self, session): + if not isinstance(session, self._session_type): + raise TypeError("session must be a session type class") + self.sessions[session.session_id] = session + self.browser.all_sessions[session.session_id] = session + + def _remove_session(self, session_id): + if isinstance(session_id, self._session_type): + session_id = session_id.session_id + _ = self.sessions.pop(session_id, None) + _ = self.browser.all_sessions.pop(session_id, None) + + def _get_first_session(self): + if not self.sessions.values(): + raise RuntimeError( + "Cannot use this method without at least one valid session", + ) + return next(iter(self.sessions.values())) + + def send_command(self, command, params=None): + if not self.sessions.values(): + raise RuntimeError("Cannot send_command without at least one valid session") + return self._get_first_session().send_command(command, params) + + +class TabSync(TargetSync): + def __init__(self, target_id, browser): + super().__init__(target_id, browser) + + +class BrowserSync(TargetSync): + """`BrowserSync` is the sync implementation of `Browser`.""" + + _tab_type = TabSync + _session_type = SessionSync + _target_type = TargetSync + + def __init__(self, path=None, *, browser_cls=Chromium, channel_cls=Pipe, **kwargs): + self.tabs = {} + # Compose Resources + self.channel = channel_cls() + self.broker = BrokerSync(self, self.channel) + self.browser_impl = browser_cls(self.channel, path, **kwargs) + + # we do need something to indicate we're open TODO yeah an open lock + + def open(self): + self.subprocess = subprocess.Popen( # noqa: S603 + self.browser_impl.get_cli(), + # stderr= TODO make a pipe with logistro + env=self.browser_impl.get_env(), + **self.browser_impl.get_popen_args(), + ) + super(TargetSync, self).__init__("0", self) + self._add_session(SessionSync(self, "")) + # start a watchdock + # open can only be run once? + # or depends on lock + + def __enter__(self): + self.open() + return self + + def _is_closed(self, wait=0): + if wait == 0: + return self.subprocess.poll() is None + else: + try: + self.subprocess.wait(wait) + except subprocess.TimeoutExpired: + return False + return True + + def _close(self): + if self._is_closed(): + return + + try: + self.send_command("Browser.close") + except (BrowserClosedError, BrowserFailedError): + return + except ChannelClosedError: + pass + + self.channel.close() + if self._is_closed(): + return + + # try kiling + kill(self.subprocess) + if self._is_closed(wait=4): + return + else: + raise RuntimeError("Couldn't close or kill browser subprocess") + + def close(self): + try: + self._close() + except ProcessLookupError: + pass + self.channel.close() + self.browser_impl.clean() + + def __exit__(self, exc_type, exc_value, exc_traceback): + self.close() + + # wrap our broker for convenience + def start_output_thread(self, **kwargs): + self.broker.run_output_thread(**kwargs) + + def _add_tab(self, tab): + if not isinstance(tab, self.tab_type): + raise TypeError("tab must be an object of (sub)class Tab") + self.tabs[tab.target_id] = tab + + def _remove_tab(self, target_id): + if isinstance(target_id, self.tab_type): + target_id = target_id.target_id + del self.tabs[target_id] + + def get_tab(self): + if self.tabs.values(): + return next(iter(self.tabs.values())) + return None diff --git a/choreographer/protocol.py b/choreographer/protocol.py new file mode 100644 index 00000000..d8b1d952 --- /dev/null +++ b/choreographer/protocol.py @@ -0,0 +1,117 @@ +"""protocol.py includes helpers and constants for the Chrome Devtools Protocol.""" + +from enum import Enum + + +class Ecode(Enum): + """Ecodes are a list of possible error codes chrome returns.""" + + TARGET_NOT_FOUND = -32602 + + +class DevtoolsProtocolError(Exception): + """.""" + + def __init__(self, response): + """.""" + super().__init__(response) + self.code = response["error"]["code"] + self.message = response["error"]["message"] + + +class MessageTypeError(TypeError): + """.""" + + def __init__(self, key, value, expected_type): + """.""" + value = type(value) if not isinstance(value, type) else value + super().__init__( + f"Message with key {key} must have type {expected_type}, not {value}.", + ) + + +class MissingKeyError(ValueError): + """.""" + + def __init__(self, key, obj): + """.""" + super().__init__( + f"Message missing required key/s {key}. Message received: {obj}", + ) + + +class ExperimentalFeatureWarning(UserWarning): + """.""" + + +def verify_params(obj): + n_keys = 0 + + required_keys = {"id": int, "method": str} + for key, type_key in required_keys.items(): + if key not in obj: + raise MissingKeyError(key, obj) + if not isinstance(obj[key], type_key): + raise MessageTypeError(key, type(obj[key]), type_key) + n_keys += 2 + + if "params" in obj: + n_keys += 1 + if "sessionId" in obj: + n_keys += 1 + + if len(obj.keys()) != n_keys: + raise RuntimeError( + "Message objects must have id and method keys, " + "and may have params and sessionId keys.", + ) + + +def calculate_message_key(response): + session_id = response.get("sessionId", "") + message_id = response.get("id", None) + if message_id is None: + return None + return (session_id, message_id) + + +def match_message_key(response, key): + session_id, message_id = key + if ("session_id" not in response and session_id == "") or ( # is browser session + "session_id" in response and response["session_id"] == session_id # is session + ): + pass + else: + return False + + if "id" in response and str(response["id"]) == str(message_id): + pass + else: + return False + return True + + +def is_event(response): + required_keys = {"method", "params"} + return required_keys <= response.keys() and "id" not in response + + +def get_target_id_from_result(response): + if "result" in response and "targetId" in response["result"]: + return response["result"]["targetId"] + else: + return None + + +def get_session_id_from_result(response): + if "result" in response and "sessionId" in response["result"]: + return response["result"]["sessionId"] + else: + return None + + +def get_error_from_result(response): + if "error" in response: + return response["error"] + else: + return None diff --git a/choreographer/protocol/__init__.py b/choreographer/protocol/__init__.py deleted file mode 100644 index 5e1860b1..00000000 --- a/choreographer/protocol/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -"""choreographer.protocol provides classes and tools for Chrome Devtools Protocol.""" - -from ._protocol import ( - DevtoolsProtocolError, - Ecode, - ExperimentalFeatureWarning, - MessageTypeError, - MissingKeyError, - Protocol, -) -from ._session import Session -from ._target import Target - -__all__ = [ - "DevtoolsProtocolError", - "Ecode", - "ExperimentalFeatureWarning", - "MessageTypeError", - "MissingKeyError", - "Protocol", - "Session", - "Target", -] diff --git a/choreographer/protocol/_protocol.py b/choreographer/protocol/_protocol.py deleted file mode 100644 index 925347d5..00000000 --- a/choreographer/protocol/_protocol.py +++ /dev/null @@ -1,119 +0,0 @@ -from enum import Enum - - -class Ecode(Enum): - TARGET_NOT_FOUND = -32602 - - -class DevtoolsProtocolError(Exception): - """.""" - - def __init__(self, response): - """.""" - super().__init__(response) - self.code = response["error"]["code"] - self.message = response["error"]["message"] - - -class MessageTypeError(TypeError): - """.""" - - def __init__(self, key, value, expected_type): - """.""" - value = type(value) if not isinstance(value, type) else value - super().__init__( - f"Message with key {key} must have type {expected_type}, not {value}.", - ) - - -class MissingKeyError(ValueError): - """.""" - - def __init__(self, key, obj): - """.""" - super().__init__( - f"Message missing required key/s {key}. Message received: {obj}", - ) - - -class ExperimentalFeatureWarning(UserWarning): - """.""" - - -class Protocol: - def __init__(self, *, debug=False): - # Stored Resources - - # Configuration - self.debug = debug - - # State - self.sessions = {} - - def calculate_key(self, response): - session_id = response.get("sessionId", "") - message_id = response.get("id", None) - if message_id is None: - return None - return (session_id, message_id) - - def verify_json(self, obj): - n_keys = 0 - required_keys = {"id": int, "method": str} - for key, type_key in required_keys.items(): - if key not in obj: - raise MissingKeyError(key, obj) - if not isinstance(obj[key], type_key): - raise MessageTypeError(key, type(obj[key]), type_key) - n_keys += 2 - - if "params" in obj: - n_keys += 1 - if "sessionId" in obj: - n_keys += 1 - - if len(obj.keys()) != n_keys: - raise RuntimeError( - "Message objects must have id and method keys, " - "and may have params and sessionId keys.", - ) - - def match_key(self, response, key): - session_id, message_id = key - if ("session_id" not in response and session_id == "") or ( - "session_id" in response and response["session_id"] == session_id - ): - pass - else: - return False - - if "id" in response and str(response["id"]) == str(message_id): - pass - else: - return False - return True - - def has_id(self, response): - return "id" in response - - def get_target_id(self, response): - if "result" in response and "targetId" in response["result"]: - return response["result"]["targetId"] - else: - return None - - def get_session_id(self, response): - if "result" in response and "sessionId" in response["result"]: - return response["result"]["sessionId"] - else: - return None - - def get_error(self, response): - if "error" in response: - return response["error"] - else: - return None - - def is_event(self, response): - required_keys = {"method", "params"} - return required_keys <= response.keys() and "id" not in response diff --git a/choreographer/protocol/_session.py b/choreographer/protocol/_session.py deleted file mode 100644 index 76656919..00000000 --- a/choreographer/protocol/_session.py +++ /dev/null @@ -1,58 +0,0 @@ -class Session: - # points to browser, bad - def __init__(self, browser, session_id): - if not isinstance(session_id, str): - raise TypeError("session_id must be a string") - # Resources - self.browser = browser - - # State - self.session_id = session_id - self.message_id = 0 - self.subscriptions = {} - self.subscriptions_futures = {} - - def send_command(self, command, params=None): - current_id = self.message_id - self.message_id += 1 - json_command = { - "id": current_id, - "method": command, - } - - if self.session_id: - json_command["sessionId"] = self.session_id - if params: - json_command["params"] = params - - return self.browser.write_json(json_command) - - def subscribe(self, string, callback, *, repeating=True): - if not self.browser.loop: - raise ValueError("You may use this method with a loop in Browser") - if string in self.subscriptions: - raise ValueError( - "You are already subscribed to this string, " - "duplicate subscriptions are not allowed.", - ) - else: - self.subscriptions[string] = (callback, repeating) - - def unsubscribe(self, string): - if not self.browser.loop: - raise ValueError("You may use this method with a loop in Browser") - if string not in self.subscriptions: - raise ValueError( - "Cannot unsubscribe as string is not present in subscriptions", - ) - del self.subscriptions[string] - - def subscribe_once(self, string): - if not self.browser.loop: - raise ValueError("You may use this method with a loop in Browser") - future = self.browser.loop.create_future() - if string not in self.subscriptions_futures: - self.subscriptions_futures[string] = [future] - else: - self.subscriptions_futures[string].append(future) - return future diff --git a/choreographer/protocol/_target.py b/choreographer/protocol/_target.py deleted file mode 100644 index 837e355d..00000000 --- a/choreographer/protocol/_target.py +++ /dev/null @@ -1,102 +0,0 @@ -import sys -from collections import OrderedDict - -from ._protocol import DevtoolsProtocolError -from ._session import Session - - -class Target: - # points to browser, bad - def __init__(self, target_id, browser): - if not isinstance(target_id, str): - raise TypeError("target_id must be string") - # Resources - self.browser = browser - - # States - self.sessions = OrderedDict() - self.target_id = target_id - - # sync - def _add_session(self, session): - if not isinstance(session, Session): - raise TypeError("session must be an object of class Session") - self.sessions[session.session_id] = session - self.browser.protocol.sessions[session.session_id] = session - - # sync - def _remove_session(self, session_id): - if isinstance(session_id, Session): - session_id = session_id.session_id - _ = self.sessions.pop(session_id, None) - _ = self.browser.protocol.sessions.pop(session_id, None) - - # async only - async def create_session(self): - if not self.browser.loop: - raise RuntimeError( - "There is no eventloop, or was not passed " - "to browser. Cannot use async methods", - ) - response = await self.browser.send_command( - "Target.attachToTarget", - params={"targetId": self.target_id, "flatten": True}, - ) - if "error" in response: - raise RuntimeError("Could not create session") from DevtoolsProtocolError( - response, - ) - session_id = response["result"]["sessionId"] - new_session = Session(self.browser, session_id) - self._add_session(new_session) - return new_session - - # async only - async def close_session(self, session_id): - if not self.browser.loop: - raise RuntimeError( - "There is no eventloop, or was not passed to " - "browser. Cannot use async methods", - ) - if isinstance(session_id, Session): - session_id = session_id.session_id - response = await self.browser.send_command( - command="Target.detachFromTarget", - params={"sessionId": session_id}, - ) - self._remove_session(session_id) - if "error" in response: - raise RuntimeError("Could not close session") from DevtoolsProtocolError( - response, - ) - print(f"The session {session_id} has been closed", file=sys.stderr) - return response - - # internal - def _get_first_session(self): - if not self.sessions.values(): - raise RuntimeError( - "Cannot use this method without at least one valid session", - ) - return next(iter(self.sessions.values())) - - # wrapper - def send_command(self, command, params=None): - if not self.sessions.values(): - raise RuntimeError("Cannot send_command without at least one valid session") - return self._get_first_session().send_command(command, params) - - # async only - def subscribe(self, string, callback, *, repeating=True): - session = self._get_first_session() - session.subscribe(string, callback, repeating=repeating) - - # async only - def unsubscribe(self, string): - session = self._get_first_session() - session.unsubscribe(string) - - # async only - def subscribe_once(self, string): - session = self._get_first_session() - return session.subscribe_once(string) From 479c4090553687b461397ee524be0af28d207772 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Mon, 6 Jan 2025 12:08:17 -0500 Subject: [PATCH 20/88] Fix imports --- choreographer/__init__.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/choreographer/__init__.py b/choreographer/__init__.py index b61e52ae..ceb49442 100644 --- a/choreographer/__init__.py +++ b/choreographer/__init__.py @@ -8,18 +8,17 @@ from choreographer import protocol -from ._browser import Browser, BrowserClosedError -from ._channels.pipe import BlockWarning, PipeClosedError +from ._browser_sync import BrowserClosedError, BrowserSync, TabSync +from ._channels import BlockWarning, ChannelClosedError from ._cli_utils import get_browser, get_browser_sync from ._sys_utils import TempDirectory, TempDirWarning, browser_which, get_browser_path -from ._tab import Tab __all__ = [ "BlockWarning", - "Browser", "BrowserClosedError", - "PipeClosedError", - "Tab", + "BrowserSync", + "ChannelClosedError", + "TabSync", "TempDirWarning", "TempDirectory", "browser_which", From 3fbee4b97f1759c32790842be66b1e992475e523 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Mon, 6 Jan 2025 12:09:40 -0500 Subject: [PATCH 21/88] Fix up metadata --- .github/workflows/publish_testpypi.yml | 2 +- .github/workflows/test.yml | 2 +- pyproject.toml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish_testpypi.yml b/.github/workflows/publish_testpypi.yml index 93872465..7d3fd429 100644 --- a/.github/workflows/publish_testpypi.yml +++ b/.github/workflows/publish_testpypi.yml @@ -28,7 +28,7 @@ jobs: # don't modify sync file! messes up version! - run: uv sync --all-extras --frozen # does order matter? - run: uv build - - run: uv run --no-sync choreo_get_browser -v --i ${{ matrix.chrome_v }} + - run: uv run --no-sync choreo_get_chrome -v --i ${{ matrix.chrome_v }} - name: Reinstall from wheel run: > uv pip install dist/choreographer-$(uv diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eb209913..2a136d02 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,7 +26,7 @@ jobs: - name: Install choreographer run: uv sync --all-extras - name: Install google-chrome-for-testing - run: uv run choreo_get_browser + run: uv run choreo_get_chrome - name: Diagnose run: uv run choreo_diagnose --no-run timeout-minutes: 1 diff --git a/pyproject.toml b/pyproject.toml index 4858f517..7d73e7aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,8 +52,8 @@ dev = [ #] [project.scripts] -choreo_diagnose = "choreographer._cli_utils_no_qa:diagnose" -choreo_get_browser = "choreographer._cli_utils:get_browser_cli" +choreo_diagnose = "choreographer._cli_utils:diagnose" +choreo_get_chrome = "choreographer._cli_utils:get_chrome_cli" [tool.ruff.lint] select = ["ALL"] From 1a1503f72c515c7cdef83f892e52e97557a92dca Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Mon, 6 Jan 2025 12:41:54 -0500 Subject: [PATCH 22/88] Fix up namespaces --- choreographer/__init__.py | 60 ++++++++++++++++++++++------- choreographer/_browsers/chromium.py | 9 ++--- choreographer/browser_sync.py | 3 +- uv.lock | 2 +- 4 files changed, 52 insertions(+), 22 deletions(-) diff --git a/choreographer/__init__.py b/choreographer/__init__.py index ceb49442..e91e9295 100644 --- a/choreographer/__init__.py +++ b/choreographer/__init__.py @@ -6,24 +6,56 @@ as a building block. """ -from choreographer import protocol +from ._browser_sync import ( # noqa: F401 unused import + BrowserSync, + SessionSync, + TabSync, + TargetSync, +) +from ._browsers import ( # noqa: F401 unused import + BrowserClosedError, + BrowserFailedError, + Chromium, +) +from ._channels import BlockWarning, ChannelClosedError # noqa: F401 unused import +from ._cli_utils import get_browser, get_browser_sync # noqa: F401 unused import +from ._sys_utils import ( # noqa: F401 unused import + TmpDirectory, + TmpDirWarning, + browser_which, + get_browser_path, +) -from ._browser_sync import BrowserClosedError, BrowserSync, TabSync -from ._channels import BlockWarning, ChannelClosedError -from ._cli_utils import get_browser, get_browser_sync -from ._sys_utils import TempDirectory, TempDirWarning, browser_which, get_browser_path +_sync_api = [ + "BrowserSync", + "SessionSync", + "TabSync", + "TargetSync", +] -__all__ = [ - "BlockWarning", +_browser_impls = [ + "Chromium", +] + +_errors = [ "BrowserClosedError", - "BrowserSync", + "BrowserFailedError", "ChannelClosedError", - "TabSync", - "TempDirWarning", - "TempDirectory", - "browser_which", + "BlockWarning", + "TmpDirWarning", +] + +_utils = [ "get_browser", - "get_browser_path", "get_browser_sync", - "protocol", + "TmpDirectory", + "browser_which", + "get_browser_path", +] + +__all__ = [ # noqa: PLE0604 non-string in all + *_sync_api, + *_browser_impls, + *_errors, + *_utils, ] diff --git a/choreographer/_browsers/chromium.py b/choreographer/_browsers/chromium.py index 809d0852..90dbcd09 100644 --- a/choreographer/_browsers/chromium.py +++ b/choreographer/_browsers/chromium.py @@ -8,14 +8,15 @@ if platform.system() == "Windows": import msvcrt -from choreographer._channels.pipe import Pipe, WebSocket +from choreographer._channels import Pipe from choreographer._sys_utils import get_browser_path class Chromium: def __init__(self, channel): - self._comm = channel - # extra information from pipe + self._channel = channel + if not isinstance(channel, Pipe): + raise NotImplementedError("Websocket style channels not implemented yet") # where do we get user data dir def get_cli(self, temp_dir, **kwargs): @@ -70,8 +71,6 @@ def get_cli(self, temp_dir, **kwargs): cli += [ f"--remote-debugging-io-pipes={r_handle!s},{w_handle!s}", ] - elif isinstance(self._channel, WebSocket): - raise NotImplementedError("Websocket style channels not implemented yet") def get_env(): diff --git a/choreographer/browser_sync.py b/choreographer/browser_sync.py index fa846c54..1fb72150 100644 --- a/choreographer/browser_sync.py +++ b/choreographer/browser_sync.py @@ -76,8 +76,7 @@ def send_command(self, command, params=None): class TabSync(TargetSync): - def __init__(self, target_id, browser): - super().__init__(target_id, browser) + pass class BrowserSync(TargetSync): diff --git a/uv.lock b/uv.lock index 03e1d761..f3d79308 100644 --- a/uv.lock +++ b/uv.lock @@ -16,7 +16,7 @@ wheels = [ [[package]] name = "choreographer" -version = "1.0.0a0.post51+git.2f98811d.dirty" +version = "1.0.0a0.post77+git.3fbee4b9" source = { editable = "." } dependencies = [ { name = "logistro" }, From 7a3ae8a4fe463e1052cf35c907c0a9845a1ac63d Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Mon, 6 Jan 2025 12:47:47 -0500 Subject: [PATCH 23/88] Fix up import errors --- choreographer/__init__.py | 18 +++++++++--------- choreographer/_brokers/_sync.py | 3 +-- choreographer/_channels/_wire.py | 3 ++- choreographer/_channels/pipe.py | 4 ++-- choreographer/_sys_utils/__init__.py | 2 +- uv.lock | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/choreographer/__init__.py b/choreographer/__init__.py index e91e9295..be89aed0 100644 --- a/choreographer/__init__.py +++ b/choreographer/__init__.py @@ -6,25 +6,25 @@ as a building block. """ -from ._browser_sync import ( # noqa: F401 unused import - BrowserSync, - SessionSync, - TabSync, - TargetSync, -) from ._browsers import ( # noqa: F401 unused import BrowserClosedError, BrowserFailedError, Chromium, ) from ._channels import BlockWarning, ChannelClosedError # noqa: F401 unused import -from ._cli_utils import get_browser, get_browser_sync # noqa: F401 unused import +from ._cli_utils import get_chrome, get_chrome_sync # noqa: F401 unused import from ._sys_utils import ( # noqa: F401 unused import TmpDirectory, TmpDirWarning, browser_which, get_browser_path, ) +from .browser_sync import ( # noqa: F401 unused import + BrowserSync, + SessionSync, + TabSync, + TargetSync, +) _sync_api = [ "BrowserSync", @@ -46,8 +46,8 @@ ] _utils = [ - "get_browser", - "get_browser_sync", + "get_chrome", + "get_chrome_sync", "TmpDirectory", "browser_which", "get_browser_path", diff --git a/choreographer/_brokers/_sync.py b/choreographer/_brokers/_sync.py index 5389d8bf..994017e2 100644 --- a/choreographer/_brokers/_sync.py +++ b/choreographer/_brokers/_sync.py @@ -2,8 +2,7 @@ from threading import Thread from choreographer import protocol - -from ._channels import ChannelClosedError +from choreographer._channels import ChannelClosedError class BrokerSync: diff --git a/choreographer/_channels/_wire.py b/choreographer/_channels/_wire.py index 3bf5ffda..c42ce417 100644 --- a/choreographer/_channels/_wire.py +++ b/choreographer/_channels/_wire.py @@ -1,5 +1,6 @@ import simplejson -from channel._errors import JSONError + +from ._errors import JSONError class MultiEncoder(simplejson.JSONEncoder): diff --git a/choreographer/_channels/pipe.py b/choreographer/_channels/pipe.py index 00e6ed1b..32219546 100644 --- a/choreographer/_channels/pipe.py +++ b/choreographer/_channels/pipe.py @@ -4,8 +4,8 @@ import warnings from threading import Lock -import channel._wire as wire -from channel._errors import BlockWarning, ChannelClosedError, JSONError +from . import _wire as wire +from ._errors import BlockWarning, ChannelClosedError, JSONError _with_block = bool(sys.version_info[:3] >= (3, 12) or platform.system() != "Windows") diff --git a/choreographer/_sys_utils/__init__.py b/choreographer/_sys_utils/__init__.py index 6f1f0414..b74c99c5 100644 --- a/choreographer/_sys_utils/__init__.py +++ b/choreographer/_sys_utils/__init__.py @@ -1,4 +1,4 @@ -from ._kill.py import kill +from ._kill import kill from ._tmpfile import TmpDirectory, TmpDirWarning from ._which import browser_which, get_browser_path diff --git a/uv.lock b/uv.lock index f3d79308..ec966575 100644 --- a/uv.lock +++ b/uv.lock @@ -16,7 +16,7 @@ wheels = [ [[package]] name = "choreographer" -version = "1.0.0a0.post77+git.3fbee4b9" +version = "1.0.0a0.post77+git.3fbee4b9.dirty" source = { editable = "." } dependencies = [ { name = "logistro" }, From c4e79dca1381cdec63959514a999e9da9cf20e94 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Mon, 6 Jan 2025 14:36:00 -0500 Subject: [PATCH 24/88] Redo chromium.py changes? --- choreographer/_browsers/chromium.py | 93 +++++++++++++++++++++-------- choreographer/_sys_utils/_which.py | 4 +- 2 files changed, 71 insertions(+), 26 deletions(-) diff --git a/choreographer/_browsers/chromium.py b/choreographer/_browsers/chromium.py index 90dbcd09..54bad7fe 100644 --- a/choreographer/_browsers/chromium.py +++ b/choreographer/_browsers/chromium.py @@ -2,6 +2,7 @@ import os import platform +import subprocess import sys from pathlib import Path @@ -9,38 +10,79 @@ import msvcrt from choreographer._channels import Pipe -from choreographer._sys_utils import get_browser_path +from choreographer._sys_utils import TmpDirectory, get_browser_path +from ._chrome_constants import chrome_names, typical_chrome_paths -class Chromium: - def __init__(self, channel): - self._channel = channel - if not isinstance(channel, Pipe): - raise NotImplementedError("Websocket style channels not implemented yet") +chromium_wrapper_path = Path(__file__).resolve().parent / "chromium_wrapper.py" + + +def _is_exe(path): + try: + return os.access(path, os.X_OK) + except: # noqa: E722 bare except ok, weird errors, best effort. + return False - # where do we get user data dir - def get_cli(self, temp_dir, **kwargs): - gpu_enabled = kwargs.pop("with_gpu", False) - headless = kwargs.pop("headless", True) - sandbox = kwargs.pop("with_sandbox", False) + +class Chromium: + def __init__(self, channel, **kwargs): + self.gpu_enabled = kwargs.pop("with_gpu", False) + self.headless = kwargs.pop("headless", True) + self.sandbox_enabled = kwargs.pop("with_sandbox", False) + self.path = kwargs.pop("path", None) + self._tmp_dir_path = kwargs.pop("tmp_dir", None) + self.skip_local = bool( + "ubuntu" in platform.version().lower() and self.enable_sandbox, + ) if kwargs: raise RuntimeError( "Chromium.get_cli() received " f"invalid args: {kwargs.keys()}", ) - path = get_browser_path() - if not path: - raise RuntimeError("Browser not found.") - chromium_wrapper_path = Path(__file__).resolve().parent / "chromium_wrapper.py" + if not self.path: + self.path = get_browser_path(chrome_names, skip_local=self.skip_local) + if not self.path: + # do typical chrome paths + for candidate in typical_chrome_paths: + if _is_exe(candidate): + self.path = candidate + break + if not self.path: + raise RuntimeError( + "Browser not found. You can use get_chrome(), " + "please see documentation.", + ) + self._channel = channel + if not isinstance(channel, Pipe): + raise NotImplementedError("Websocket style channels not implemented yet") + + self.tmp_dir = TmpDirectory( + path=self._tmp_dir_path, + sneak="snap" in self.path, + ) + + def get_popen_args(self): + args = {} + # need to check pipe + if platform.system() == "Windows": + args["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP + args["close_fds"] = False + else: + args["close_fds"] = True + if isinstance(self.channel, Pipe): + args["stdin"] = self.channel.from_choreo_to_external + args["stdout"] = self.channel.from_external_to_choreo + + def get_cli(self): if platform.system() != "Windows": cli = [ sys.executable, chromium_wrapper_path, - path, + self.path, ] else: cli = [ - path, + self.path, ] cli.extend( @@ -48,30 +90,33 @@ def get_cli(self, temp_dir, **kwargs): "--disable-breakpad", "--allow-file-access-from-files", "--enable-logging=stderr", - f"--user-data-dir={temp_dir}", + f"--user-data-dir={self.tmp_dir.name}", "--no-first-run", "--enable-unsafe-swiftshader", ], ) - if not gpu_enabled: + if not self.gpu_enabled: cli.append("--disable-gpu") - if headless: + if self.headless: cli.append("--headless") - if not sandbox: + if not self.sandbox_enabled: cli.append("--no-sandbox") if isinstance(self._channel, Pipe): cli.append("--remote-debugging-pipe") if platform.system() == "Windows": - _inheritable = True w_handle = msvcrt.get_osfhandle(self._channel.from_choreo_to_external) r_handle = msvcrt.get_osfhandle(self._channel.from_external_to_choreo) + _inheritable = True os.set_handle_inheritable(w_handle, _inheritable) os.set_handle_inheritable(r_handle, _inheritable) cli += [ f"--remote-debugging-io-pipes={r_handle!s},{w_handle!s}", ] + return cli + def get_env(): + return os.environ().copy() -def get_env(): - return os.environ().copy() + def clean(): + raise ValueError("Look at tempdir") diff --git a/choreographer/_sys_utils/_which.py b/choreographer/_sys_utils/_which.py index b6ea886f..578723ae 100644 --- a/choreographer/_sys_utils/_which.py +++ b/choreographer/_sys_utils/_which.py @@ -64,5 +64,5 @@ def browser_which(executable_names, *, skip_local=False): return None -def get_browser_path(**kwargs): - return os.environ.get("BROWSER_PATH", browser_which(**kwargs)) +def get_browser_path(*args, **kwargs): + return os.environ.get("BROWSER_PATH", browser_which(*args, **kwargs)) From afaf7ad5d87faf00a4b02c19bcdd485faab44b90 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Mon, 6 Jan 2025 14:36:34 -0500 Subject: [PATCH 25/88] Remember to return --- choreographer/_browsers/chromium.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/choreographer/_browsers/chromium.py b/choreographer/_browsers/chromium.py index 54bad7fe..ad61041d 100644 --- a/choreographer/_browsers/chromium.py +++ b/choreographer/_browsers/chromium.py @@ -54,7 +54,7 @@ def __init__(self, channel, **kwargs): ) self._channel = channel if not isinstance(channel, Pipe): - raise NotImplementedError("Websocket style channels not implemented yet") + raise NotImplementedError("Websocket style channels not implemented yet.") self.tmp_dir = TmpDirectory( path=self._tmp_dir_path, @@ -72,6 +72,7 @@ def get_popen_args(self): if isinstance(self.channel, Pipe): args["stdin"] = self.channel.from_choreo_to_external args["stdout"] = self.channel.from_external_to_choreo + return args def get_cli(self): if platform.system() != "Windows": From a59220062e0f14818835f7b147aba1194ead196b Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Mon, 6 Jan 2025 14:40:08 -0500 Subject: [PATCH 26/88] Move path to non-kwargs arg chromium --- choreographer/_browsers/chromium.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/choreographer/_browsers/chromium.py b/choreographer/_browsers/chromium.py index ad61041d..ac1ac9f5 100644 --- a/choreographer/_browsers/chromium.py +++ b/choreographer/_browsers/chromium.py @@ -25,19 +25,19 @@ def _is_exe(path): class Chromium: - def __init__(self, channel, **kwargs): + def __init__(self, channel, path=None, **kwargs): + self.path = path self.gpu_enabled = kwargs.pop("with_gpu", False) self.headless = kwargs.pop("headless", True) self.sandbox_enabled = kwargs.pop("with_sandbox", False) - self.path = kwargs.pop("path", None) self._tmp_dir_path = kwargs.pop("tmp_dir", None) - self.skip_local = bool( - "ubuntu" in platform.version().lower() and self.enable_sandbox, - ) if kwargs: raise RuntimeError( "Chromium.get_cli() received " f"invalid args: {kwargs.keys()}", ) + self.skip_local = bool( + "ubuntu" in platform.version().lower() and self.enable_sandbox, + ) if not self.path: self.path = get_browser_path(chrome_names, skip_local=self.skip_local) From 7ae4faedfe1136ccff746ba621ad62a75be149b9 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Mon, 6 Jan 2025 15:17:12 -0500 Subject: [PATCH 27/88] Fix some bad syntax --- choreographer/_browsers/_chrome_constants.py | 3 ++- choreographer/_browsers/chromium.py | 9 +++++++-- choreographer/_sys_utils/_which.py | 4 ++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/choreographer/_browsers/_chrome_constants.py b/choreographer/_browsers/_chrome_constants.py index 7dfae43f..60799d19 100644 --- a/choreographer/_browsers/_chrome_constants.py +++ b/choreographer/_browsers/_chrome_constants.py @@ -11,7 +11,8 @@ "Chrome.app", "Google Chrome", "Google Chrome.app", -].extend(chromium_names) +] +chrome_names.extend(chromium_names) typical_chrome_paths = None if platform.system() == "Windows": diff --git a/choreographer/_browsers/chromium.py b/choreographer/_browsers/chromium.py index ac1ac9f5..f95ecd03 100644 --- a/choreographer/_browsers/chromium.py +++ b/choreographer/_browsers/chromium.py @@ -6,6 +6,8 @@ import sys from pathlib import Path +import logistro # noqa: F401 might use + if platform.system() == "Windows": import msvcrt @@ -40,7 +42,10 @@ def __init__(self, channel, path=None, **kwargs): ) if not self.path: - self.path = get_browser_path(chrome_names, skip_local=self.skip_local) + self.path = get_browser_path( + executable_names=chrome_names, + skip_local=self.skip_local, + ) if not self.path: # do typical chrome paths for candidate in typical_chrome_paths: @@ -91,7 +96,7 @@ def get_cli(self): "--disable-breakpad", "--allow-file-access-from-files", "--enable-logging=stderr", - f"--user-data-dir={self.tmp_dir.name}", + f"--user-data-dir={self.tmp_dir.path}", "--no-first-run", "--enable-unsafe-swiftshader", ], diff --git a/choreographer/_sys_utils/_which.py b/choreographer/_sys_utils/_which.py index 578723ae..168a54c2 100644 --- a/choreographer/_sys_utils/_which.py +++ b/choreographer/_sys_utils/_which.py @@ -37,7 +37,7 @@ def browser_which(executable_names, *, skip_local=False): path = None if isinstance(executable_names, str): - executable_name = [executable_names] + executable_names = [executable_names] local_chrome = get_chrome_download_path() if ( @@ -50,7 +50,7 @@ def browser_which(executable_names, *, skip_local=False): if platform.system() == "Windows": os.environ["NoDefaultCurrentDirectoryInExePath"] = "0" # noqa: SIM112 var name set by windows - for exe in executable_name: + for exe in executable_names: if platform.system() == "Windows" and exe == "chrome": path = _which_from_windows_reg() if path and _is_exe(path): From 3b68fc814952519566c12982e3264f16d9ca5bc6 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Mon, 6 Jan 2025 16:22:53 -0500 Subject: [PATCH 28/88] Fix syntax --- choreographer/_browsers/chromium.py | 20 +++++++++++--------- choreographer/browser_sync.py | 7 +++---- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/choreographer/_browsers/chromium.py b/choreographer/_browsers/chromium.py index f95ecd03..9db9e841 100644 --- a/choreographer/_browsers/chromium.py +++ b/choreographer/_browsers/chromium.py @@ -16,7 +16,9 @@ from ._chrome_constants import chrome_names, typical_chrome_paths -chromium_wrapper_path = Path(__file__).resolve().parent / "chromium_wrapper.py" +chromium_wrapper_path = ( + Path(__file__).resolve().parent / "_unix_pipe_chromium_wrapper.py" +) def _is_exe(path): @@ -29,9 +31,9 @@ def _is_exe(path): class Chromium: def __init__(self, channel, path=None, **kwargs): self.path = path - self.gpu_enabled = kwargs.pop("with_gpu", False) + self.gpu_enabled = kwargs.pop("enable_gpu", False) self.headless = kwargs.pop("headless", True) - self.sandbox_enabled = kwargs.pop("with_sandbox", False) + self.sandbox_enabled = kwargs.pop("enable_sandbox", False) self._tmp_dir_path = kwargs.pop("tmp_dir", None) if kwargs: raise RuntimeError( @@ -74,9 +76,9 @@ def get_popen_args(self): args["close_fds"] = False else: args["close_fds"] = True - if isinstance(self.channel, Pipe): - args["stdin"] = self.channel.from_choreo_to_external - args["stdout"] = self.channel.from_external_to_choreo + if isinstance(self._channel, Pipe): + args["stdin"] = self._channel.from_choreo_to_external + args["stdout"] = self._channel.from_external_to_choreo return args def get_cli(self): @@ -121,8 +123,8 @@ def get_cli(self): ] return cli - def get_env(): - return os.environ().copy() + def get_env(self): + return os.environ.copy() - def clean(): + def clean(self): raise ValueError("Look at tempdir") diff --git a/choreographer/browser_sync.py b/choreographer/browser_sync.py index 1fb72150..191bebb0 100644 --- a/choreographer/browser_sync.py +++ b/choreographer/browser_sync.py @@ -88,6 +88,8 @@ class BrowserSync(TargetSync): def __init__(self, path=None, *, browser_cls=Chromium, channel_cls=Pipe, **kwargs): self.tabs = {} + self.targets = {} + self.all_sessions = {} # Compose Resources self.channel = channel_cls() self.broker = BrokerSync(self, self.channel) @@ -102,11 +104,8 @@ def open(self): env=self.browser_impl.get_env(), **self.browser_impl.get_popen_args(), ) - super(TargetSync, self).__init__("0", self) + super().__init__("0", self) self._add_session(SessionSync(self, "")) - # start a watchdock - # open can only be run once? - # or depends on lock def __enter__(self): self.open() From ac96c0b8f844c96e41a6747f15fafc1aa9706855 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Mon, 6 Jan 2025 16:23:05 -0500 Subject: [PATCH 29/88] Add process logger --- choreographer/_browsers/chromium.py | 9 +++++++++ choreographer/browser_sync.py | 13 +++++++++++-- pyproject.toml | 3 +++ uv.lock | 19 ++++++++++++------- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/choreographer/_browsers/chromium.py b/choreographer/_browsers/chromium.py index 9db9e841..955235d2 100644 --- a/choreographer/_browsers/chromium.py +++ b/choreographer/_browsers/chromium.py @@ -2,6 +2,7 @@ import os import platform +import re import subprocess import sys from pathlib import Path @@ -28,7 +29,15 @@ def _is_exe(path): return False +_logs_parser_regex = re.compile(r"\d*:\d*:\d*\/\d*\.\d*:") + + class Chromium: + @classmethod + def logger_parser(cls, record, _old): + record.msg = _logs_parser_regex.sub("", record.msg) + return True + def __init__(self, channel, path=None, **kwargs): self.path = path self.gpu_enabled = kwargs.pop("enable_gpu", False) diff --git a/choreographer/browser_sync.py b/choreographer/browser_sync.py index 191bebb0..d70a7462 100644 --- a/choreographer/browser_sync.py +++ b/choreographer/browser_sync.py @@ -1,3 +1,4 @@ +import os import subprocess import logistro @@ -94,13 +95,20 @@ def __init__(self, path=None, *, browser_cls=Chromium, channel_cls=Pipe, **kwarg self.channel = channel_cls() self.broker = BrokerSync(self, self.channel) self.browser_impl = browser_cls(self.channel, path, **kwargs) - + if hasattr(browser_cls, "logger_parser"): + parser = browser_cls.logger_parser + else: + parser = None + self.logger_pipe, _ = logistro.getPipeLogger( + __name__ + "-subprocess", + parser=parser, + ) # we do need something to indicate we're open TODO yeah an open lock def open(self): self.subprocess = subprocess.Popen( # noqa: S603 self.browser_impl.get_cli(), - # stderr= TODO make a pipe with logistro + stderr=self.logger_pipe, env=self.browser_impl.get_env(), **self.browser_impl.get_popen_args(), ) @@ -148,6 +156,7 @@ def close(self): self._close() except ProcessLookupError: pass + os.close(self.logger_pipe) self.channel.close() self.browser_impl.clean() diff --git a/pyproject.toml b/pyproject.toml index 7d73e7aa..e4371881 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,9 @@ dev = [ # "mkdocs-material>=9.5.49", #] +[tool.uv.sources] +logistro = { path = "../logistro", editable = true } + [project.scripts] choreo_diagnose = "choreographer._cli_utils:diagnose" choreo_get_chrome = "choreographer._cli_utils:get_chrome_cli" diff --git a/uv.lock b/uv.lock index ec966575..059c2b36 100644 --- a/uv.lock +++ b/uv.lock @@ -16,7 +16,7 @@ wheels = [ [[package]] name = "choreographer" -version = "1.0.0a0.post77+git.3fbee4b9.dirty" +version = "1.0.0a0.post83+git.7ae4faed.dirty" source = { editable = "." } dependencies = [ { name = "logistro" }, @@ -36,7 +36,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "logistro", specifier = ">=1.0.2" }, + { name = "logistro", editable = "../logistro" }, { name = "simplejson" }, ] @@ -88,11 +88,16 @@ wheels = [ [[package]] name = "logistro" -version = "1.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c4/37/616339aa4c47c8316caa74c6be0e42a7db2e5f11bb4df7b1589e2347c8d2/logistro-1.0.2.tar.gz", hash = "sha256:0d437c0f4fd9abef6eca0a28d85c171fc14f4f17aa1ce0f2f91d2e93f82a5c18", size = 5973 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/fb/a0d753951886ace90e2a92f55f119450e0dedabe81733c9f7655c9009d1c/logistro-1.0.2-py3-none-any.whl", hash = "sha256:f3435161b12a7f6a463aa23885c076ed2d2876255db9b5af3e302bdba6e2ed02", size = 5343 }, +version = "1.0.2.post1+git.4c99c29e" +source = { editable = "../logistro" } + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "poethepoet", specifier = ">=0.31.1" }, + { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-xdist" }, ] [[package]] From c7b2bf81611f6ff6b177fc61d2c5daed4afc7855 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Mon, 6 Jan 2025 17:00:49 -0500 Subject: [PATCH 30/88] Improve logging --- choreographer/_channels/_wire.py | 3 ++- choreographer/browser_sync.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/choreographer/_channels/_wire.py b/choreographer/_channels/_wire.py index c42ce417..29fa1b3d 100644 --- a/choreographer/_channels/_wire.py +++ b/choreographer/_channels/_wire.py @@ -1,8 +1,9 @@ +import logistro import simplejson from ._errors import JSONError - +logger = logistro.getLogger(__name__) class MultiEncoder(simplejson.JSONEncoder): """Special json encoder for numpy types.""" diff --git a/choreographer/browser_sync.py b/choreographer/browser_sync.py index d70a7462..a6daed44 100644 --- a/choreographer/browser_sync.py +++ b/choreographer/browser_sync.py @@ -100,7 +100,7 @@ def __init__(self, path=None, *, browser_cls=Chromium, channel_cls=Pipe, **kwarg else: parser = None self.logger_pipe, _ = logistro.getPipeLogger( - __name__ + "-subprocess", + "browser_proc", parser=parser, ) # we do need something to indicate we're open TODO yeah an open lock From c84ac06bc3a84ce6d8732dac574fbfe06866d4da Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Mon, 6 Jan 2025 17:02:47 -0500 Subject: [PATCH 31/88] Improve logging --- choreographer/_channels/_wire.py | 39 ++++++++++++++++++-------------- pyproject.toml | 1 + uv.lock | 2 +- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/choreographer/_channels/_wire.py b/choreographer/_channels/_wire.py index 29fa1b3d..6bec31bd 100644 --- a/choreographer/_channels/_wire.py +++ b/choreographer/_channels/_wire.py @@ -4,6 +4,8 @@ from ._errors import JSONError logger = logistro.getLogger(__name__) + + class MultiEncoder(simplejson.JSONEncoder): """Special json encoder for numpy types.""" @@ -18,20 +20,23 @@ def default(self, obj): return obj.isoformat() return simplejson.JSONEncoder.default(self, obj) - def serialize(self, obj): - try: - message = simplejson.dumps( - obj, - ensure_ascii=False, - ignore_nan=True, - cls=MultiEncoder, - ) - except simplejson.errors.JSONDecodeError as e: - raise JSONError from e - return message.encode("utf-8") - - def deserialize(self, message): - try: - return simplejson.loads(message) - except simplejson.errors.JSONDecodeError as e: - raise JSONError from e + +def serialize(obj): + try: + message = simplejson.dumps( + obj, + ensure_ascii=False, + ignore_nan=True, + cls=MultiEncoder, + ) + except simplejson.errors.JSONDecodeError as e: + raise JSONError from e + logger.debug2(f"Serialized: {message}") + return message.encode("utf-8") + + +def deserialize(message): + try: + return simplejson.loads(message) + except simplejson.errors.JSONDecodeError as e: + raise JSONError from e diff --git a/pyproject.toml b/pyproject.toml index e4371881..20feb7f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,6 +77,7 @@ ignore = [ "SIM105", # Too opionated (try-except-pass) "T201", # no print, remove after logistro TODO "PT003", # scope="function" implied but I like readability + "G004", # fstrings in my logs ] [tool.ruff.lint.per-file-ignores] diff --git a/uv.lock b/uv.lock index 059c2b36..65bda71b 100644 --- a/uv.lock +++ b/uv.lock @@ -16,7 +16,7 @@ wheels = [ [[package]] name = "choreographer" -version = "1.0.0a0.post83+git.7ae4faed.dirty" +version = "1.0.0a0.post85+git.ac96c0b8.dirty" source = { editable = "." } dependencies = [ { name = "logistro" }, From 670fea2cbcd85d9317292bcf2c5d66605a392c6d Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Mon, 6 Jan 2025 17:51:00 -0500 Subject: [PATCH 32/88] Fix download path in CLI --- choreographer/_browsers/chromium.py | 5 ++-- choreographer/_cli_utils/_cli_utils.py | 6 ++--- choreographer/_cli_utils/_cli_utils_no_qa.py | 28 +++++++++++++------- uv.lock | 2 +- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/choreographer/_browsers/chromium.py b/choreographer/_browsers/chromium.py index 955235d2..c4aa11e7 100644 --- a/choreographer/_browsers/chromium.py +++ b/choreographer/_browsers/chromium.py @@ -74,7 +74,7 @@ def __init__(self, channel, path=None, **kwargs): self.tmp_dir = TmpDirectory( path=self._tmp_dir_path, - sneak="snap" in self.path, + sneak="snap" in str(self.path), ) def get_popen_args(self): @@ -136,4 +136,5 @@ def get_env(self): return os.environ.copy() def clean(self): - raise ValueError("Look at tempdir") + return + # raise ValueError("Look at tempdir") diff --git a/choreographer/_cli_utils/_cli_utils.py b/choreographer/_cli_utils/_cli_utils.py index c825333f..46e55c21 100644 --- a/choreographer/_cli_utils/_cli_utils.py +++ b/choreographer/_cli_utils/_cli_utils.py @@ -66,7 +66,7 @@ def _extract_member(self, member, targetpath, pwd): def get_chrome_sync( arch=_chrome_platform_detected, i=-1, - path=_default_exe_path, + path=_default_download_path, *, verbose=False, ): @@ -114,7 +114,7 @@ def get_chrome_sync( async def get_chrome( arch=_chrome_platform_detected, i=-1, - path=_default_exe_path, + path=_default_download_path, ): return await asyncio.to_thread(get_chrome_sync, arch=arch, i=i, path=path) @@ -140,7 +140,7 @@ def get_chrome_cli(): action="store_true", ) parser.set_defaults(i=-1) - parser.set_defaults(path=_default_exe_path) + parser.set_defaults(path=_default_download_path) parser.set_defaults(arch=_chrome_platform_detected) parser.set_defaults(verbose=False) parsed = parser.parse_args() diff --git a/choreographer/_cli_utils/_cli_utils_no_qa.py b/choreographer/_cli_utils/_cli_utils_no_qa.py index 131658a7..9c0ce83b 100644 --- a/choreographer/_cli_utils/_cli_utils_no_qa.py +++ b/choreographer/_cli_utils/_cli_utils_no_qa.py @@ -1,5 +1,6 @@ import argparse -import asyncio + +# import asyncio import platform import subprocess import sys @@ -22,7 +23,13 @@ def diagnose(): - from choreographer import Browser, browser_which + import logistro + + logistro.getLogger().setLevel("DEBUG") + + # from choreographer import BrowserSync, Browser, browser_which + from choreographer import BrowserSync, browser_which + from choreographer._browsers._chrome_constants import chrome_names parser = argparse.ArgumentParser(description="tool to help debug problems") parser.add_argument("--no-run", dest="run", action="store_false") @@ -40,7 +47,7 @@ def diagnose(): print(platform.version()) print(platform.uname()) print("BROWSER:".center(50, "*")) - print(browser_which(debug=True)) + print(browser_which(chrome_names)) print("VERSION INFO:".center(50, "*")) try: print("PIP:".center(25, "*")) @@ -68,22 +75,23 @@ def diagnose(): if run: try: print("Sync Test Headless".center(50, "*")) - browser = Browser(debug=True, debug_browser=True, headless=headless) + browser = BrowserSync(headless=headless) + browser.open() time.sleep(3) browser.close() except BaseException as e: fail.append(("Sync test headless", e)) finally: print("Done with sync test headless".center(50, "*")) - - async def test_headless(): - browser = await Browser(debug=True, debug_browser=True, headless=headless) - await asyncio.sleep(3) - await browser.close() + # ruff: noqa: ERA001 + # async def test_headless(): + # browser = await Browser(debug=True, debug_browser=True, headless=headless) + # await asyncio.sleep(3) + # await browser.close() try: print("Async Test Headless".center(50, "*")) - asyncio.run(test_headless()) + # asyncio.run(test_headless()) except BaseException as e: fail.append(("Async test headless", e)) finally: diff --git a/uv.lock b/uv.lock index 65bda71b..8eaf5cf4 100644 --- a/uv.lock +++ b/uv.lock @@ -16,7 +16,7 @@ wheels = [ [[package]] name = "choreographer" -version = "1.0.0a0.post85+git.ac96c0b8.dirty" +version = "1.0.0a0.post87+git.c84ac06b.dirty" source = { editable = "." } dependencies = [ { name = "logistro" }, From eef038c7a2f7e85db981c6930e2f38a05ef73dd3 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Mon, 6 Jan 2025 18:32:46 -0500 Subject: [PATCH 33/88] Add lock to BrowserSync --- choreographer/_cli_utils/_cli_utils.py | 1 + choreographer/browser_sync.py | 33 ++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/choreographer/_cli_utils/_cli_utils.py b/choreographer/_cli_utils/_cli_utils.py index 46e55c21..9e7cbbd1 100644 --- a/choreographer/_cli_utils/_cli_utils.py +++ b/choreographer/_cli_utils/_cli_utils.py @@ -9,6 +9,7 @@ import zipfile from pathlib import Path +# SOON TODO this isn't the right download path, look at uv, use sysconfig _default_download_path = Path(__file__).resolve().parent / "browser_exe" _chrome_for_testing_url = "https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json" diff --git a/choreographer/browser_sync.py b/choreographer/browser_sync.py index a6daed44..dd1b723c 100644 --- a/choreographer/browser_sync.py +++ b/choreographer/browser_sync.py @@ -1,5 +1,6 @@ import os import subprocess +from threading import Lock import logistro @@ -87,7 +88,25 @@ class BrowserSync(TargetSync): _session_type = SessionSync _target_type = TargetSync + def _make_lock(self): + self._open_lock = Lock() + + def _lock_open(self): + # if open, acquire will return False, we want it to return True + return self._open_lock.acquire(blocking=False) + + def _release_lock(self): + try: + if self._open_lock.locked(): + self._open_lock.release() + return True + else: + return False + except RuntimeError: + return False + def __init__(self, path=None, *, browser_cls=Chromium, channel_cls=Pipe, **kwargs): + self._make_lock() self.tabs = {} self.targets = {} self.all_sessions = {} @@ -106,6 +125,8 @@ def __init__(self, path=None, *, browser_cls=Chromium, channel_cls=Pipe, **kwarg # we do need something to indicate we're open TODO yeah an open lock def open(self): + if not self._lock_open(): + raise RuntimeError("Can't re-open the browser") self.subprocess = subprocess.Popen( # noqa: S603 self.browser_impl.get_cli(), stderr=self.logger_pipe, @@ -113,7 +134,7 @@ def open(self): **self.browser_impl.get_popen_args(), ) super().__init__("0", self) - self._add_session(SessionSync(self, "")) + self._add_session(self._session_type(self, "")) def __enter__(self): self.open() @@ -152,6 +173,8 @@ def _close(self): raise RuntimeError("Couldn't close or kill browser subprocess") def close(self): + if not self._release_lock(): + return try: self._close() except ProcessLookupError: @@ -163,10 +186,6 @@ def close(self): def __exit__(self, exc_type, exc_value, exc_traceback): self.close() - # wrap our broker for convenience - def start_output_thread(self, **kwargs): - self.broker.run_output_thread(**kwargs) - def _add_tab(self, tab): if not isinstance(tab, self.tab_type): raise TypeError("tab must be an object of (sub)class Tab") @@ -181,3 +200,7 @@ def get_tab(self): if self.tabs.values(): return next(iter(self.tabs.values())) return None + + # wrap our broker for convenience + def start_output_thread(self, **kwargs): + self.broker.run_output_thread(**kwargs) From 9f0d0380f1b7824773ee8628a4c54666cb1d93ad Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Mon, 6 Jan 2025 22:54:23 -0500 Subject: [PATCH 34/88] Make sure Chromium cleans its tmp dir --- choreographer/_browsers/chromium.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/choreographer/_browsers/chromium.py b/choreographer/_browsers/chromium.py index c4aa11e7..9dc9478e 100644 --- a/choreographer/_browsers/chromium.py +++ b/choreographer/_browsers/chromium.py @@ -136,5 +136,8 @@ def get_env(self): return os.environ.copy() def clean(self): - return - # raise ValueError("Look at tempdir") + """Clean up any leftovers form browser, like tmp files.""" + self.tmp_dir.clean() + + def __del__(self): + self.clean() From 247944145dbb99c4f4f37dcda9b6c95b7c2223dc Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Mon, 6 Jan 2025 23:09:06 -0500 Subject: [PATCH 35/88] Add logs and docs --- choreographer/__init__.py | 2 + choreographer/_brokers/_sync.py | 10 ++- choreographer/_browsers/_errors.py | 4 +- choreographer/_browsers/chromium.py | 47 +++++++++++++- choreographer/_channels/_errors.py | 6 +- choreographer/_channels/pipe.py | 69 +++++---------------- choreographer/_cli_utils/_cli_utils.py | 11 ++++ choreographer/_sys_utils/_tmpfile.py | 76 ++++++++++------------- choreographer/_sys_utils/_which.py | 14 +++++ choreographer/browser_sync.py | 78 ++++++++++++++++++++++- choreographer/protocol.py | 86 +++++++++++++++++++++++--- 11 files changed, 290 insertions(+), 113 deletions(-) diff --git a/choreographer/__init__.py b/choreographer/__init__.py index be89aed0..d7ed3548 100644 --- a/choreographer/__init__.py +++ b/choreographer/__init__.py @@ -4,6 +4,8 @@ choreographer is natively async, so while there are two main entrypoints: classes `Browser` and `BrowserSync`, the sync version is very limited, functioning as a building block. + +See the main README for a quickstart. """ from ._browsers import ( # noqa: F401 unused import diff --git a/choreographer/_brokers/_sync.py b/choreographer/_brokers/_sync.py index 994017e2..c19b5c19 100644 --- a/choreographer/_brokers/_sync.py +++ b/choreographer/_brokers/_sync.py @@ -1,9 +1,13 @@ import json from threading import Thread +import logistro + from choreographer import protocol from choreographer._channels import ChannelClosedError +logger = logistro.getLogger(__name__) + class BrokerSync: def __init__(self, browser, channel): @@ -19,8 +23,9 @@ def run_print(): for response in responses: print(json.dumps(response, indent=4), **kwargs) except ChannelClosedError: - print("ChannelClosedError caught", **kwargs) + print("ChannelClosedError caught.", **kwargs) + logger.info("Starting thread to dump output to stdout.") Thread(target=run_print).start() def send_json(self, obj): @@ -28,3 +33,6 @@ def send_json(self, obj): key = protocol.calculate_message_key(obj) self.channel.write_json(obj) return key + + def clean(self): + pass diff --git a/choreographer/_browsers/_errors.py b/choreographer/_browsers/_errors.py index b5b34c27..5a983f8a 100644 --- a/choreographer/_browsers/_errors.py +++ b/choreographer/_browsers/_errors.py @@ -1,6 +1,6 @@ class BrowserFailedError(RuntimeError): - pass + """An error for when the browser fails to launch.""" class BrowserClosedError(RuntimeError): - pass + """An error for when the browser is closed accidently (during access).""" diff --git a/choreographer/_browsers/chromium.py b/choreographer/_browsers/chromium.py index 9dc9478e..c4207882 100644 --- a/choreographer/_browsers/chromium.py +++ b/choreographer/_browsers/chromium.py @@ -7,7 +7,7 @@ import sys from pathlib import Path -import logistro # noqa: F401 might use +import logistro if platform.system() == "Windows": import msvcrt @@ -21,6 +21,8 @@ Path(__file__).resolve().parent / "_unix_pipe_chromium_wrapper.py" ) +logger = logistro.getLogger(__name__) + def _is_exe(path): try: @@ -33,12 +35,45 @@ def _is_exe(path): class Chromium: + """ + Chromium represents an implementation of the chromium browser. + + It also includes chromium-like browsers (chrome, edge, and brave). + """ + @classmethod def logger_parser(cls, record, _old): + """ + Parse (per logistro) and extract data from browser stderr for logging. + + Args: + record: the `logging.LogRecord` object to read/modify + _old: data that was already stripped out. + + """ record.msg = _logs_parser_regex.sub("", record.msg) + # we just eliminate their stamp, we dont' extract it return True def __init__(self, channel, path=None, **kwargs): + """ + Construct a chromium browser implementation. + + Args: + channel: the choreographer.Channel we'll be using (WebSockets? Pipe?) + path: path to the browser + kwargs: + gpu_enabled (default False): Turn on GPU? Doesn't work in all envs. + headless (default True): Actually launch a browser? + sandbox_enabled (default False): Enable sandbox- + a persnickety thing depending on environment, OS, user, etc + tmp_dir (default None): Manually set the temporary directory + + Raises: + RuntimeError: Too many kwargs, or browser not found. + NotImplementedError: Pipe is the only channel type it'll accept right now. + + """ self.path = path self.gpu_enabled = kwargs.pop("enable_gpu", False) self.headless = kwargs.pop("headless", True) @@ -51,6 +86,8 @@ def __init__(self, channel, path=None, **kwargs): self.skip_local = bool( "ubuntu" in platform.version().lower() and self.enable_sandbox, ) + if self.skip_local: + logger.warning("Ubuntu + Sandbox won't work unless chrome from snap") if not self.path: self.path = get_browser_path( @@ -68,6 +105,7 @@ def __init__(self, channel, path=None, **kwargs): "Browser not found. You can use get_chrome(), " "please see documentation.", ) + logger.debug(f"Found path: {self.path}") self._channel = channel if not isinstance(channel, Pipe): raise NotImplementedError("Websocket style channels not implemented yet.") @@ -76,8 +114,10 @@ def __init__(self, channel, path=None, **kwargs): path=self._tmp_dir_path, sneak="snap" in str(self.path), ) + logger.info(f"Temporary directory at: {self.tmp_dir.name}") def get_popen_args(self): + """Return the args needed to runc chromium with subprocess.Popen().""" args = {} # need to check pipe if platform.system() == "Windows": @@ -88,9 +128,11 @@ def get_popen_args(self): if isinstance(self._channel, Pipe): args["stdin"] = self._channel.from_choreo_to_external args["stdout"] = self._channel.from_external_to_choreo + logger.debug(f"Returning args: {args}") return args def get_cli(self): + """Return the CLI command for chromium.""" if platform.system() != "Windows": cli = [ sys.executable, @@ -130,9 +172,12 @@ def get_cli(self): cli += [ f"--remote-debugging-io-pipes={r_handle!s},{w_handle!s}", ] + logger.debug(f"Returning cli: {cli}") return cli def get_env(self): + """Return the env needed for chromium.""" + logger.debug("Returning env: same env, no modification.") return os.environ.copy() def clean(self): diff --git a/choreographer/_channels/_errors.py b/choreographer/_channels/_errors.py index e2f878b8..55f17a92 100644 --- a/choreographer/_channels/_errors.py +++ b/choreographer/_channels/_errors.py @@ -1,10 +1,10 @@ class BlockWarning(UserWarning): - pass + """A warning for when block modification operatins used on incompatible OS.""" class ChannelClosedError(IOError): - pass + """An error to throw when the channel has closed from either end or error.""" class JSONError(RuntimeError): - pass + """Another JSONError.""" diff --git a/choreographer/_channels/pipe.py b/choreographer/_channels/pipe.py index 32219546..7a0a7024 100644 --- a/choreographer/_channels/pipe.py +++ b/choreographer/_channels/pipe.py @@ -4,15 +4,19 @@ import warnings from threading import Lock +import logistro + from . import _wire as wire from ._errors import BlockWarning, ChannelClosedError, JSONError _with_block = bool(sys.version_info[:3] >= (3, 12) or platform.system() != "Windows") +logger = logistro.getLogger(__name__) + # if we're a pipe we expect these public attributes class Pipe: - def __init__(self, *, debug=False): + def __init__(self): # This is where pipe listens (from browser) # So pass the write to browser self._read_from_browser, self._write_from_browser = list(os.pipe()) @@ -31,30 +35,20 @@ def __init__(self, *, debug=False): # These won't be used on windows directly, they'll be t-formed to # windows-style handles. But let another layer handle that. - self.debug = debug # should be private - # this is just a convenience to prevent multiple shutdowns self.shutdown_lock = Lock() # should be private - def write_json(self, obj, debug=None): + def write_json(self, obj): if self.shutdown_lock.locked(): raise ChannelClosedError - if not debug: - debug = self.debug - if debug: - print(f"write_json: {obj}", file=sys.stderr) encoded_message = wire.serialize(obj) + b"\0" - if debug: - print(f"write_json: {encoded_message}", file=sys.stderr) try: os.write(self._write_to_browser, encoded_message) except OSError as e: self.close() raise ChannelClosedError from e - if debug: - print("wrote_json.", file=sys.stderr) - def read_jsons(self, *, blocking=True, debug=None): # noqa: PLR0912, C901 branches, complexity + def read_jsons(self, *, blocking=True): # noqa: PLR0912, C901 branches, complexity if self.shutdown_lock.locked(): raise ChannelClosedError if not _with_block and not blocking: @@ -62,13 +56,6 @@ def read_jsons(self, *, blocking=True, debug=None): # noqa: PLR0912, C901 branc "Windows python version < 3.12 does not support non-blocking", BlockWarning, ) - if not debug: - debug = self.debug - if debug: - print( - f"read_jsons ({'blocking' if blocking else 'not blocking'}):", - file=sys.stderr, - ) jsons = [] try: if _with_block: @@ -84,8 +71,6 @@ def read_jsons(self, *, blocking=True, debug=None): # noqa: PLR0912, C901 branc ) # 10MB buffer, nbd, doesn't matter w/ this if not raw_buffer or raw_buffer == b"{bye}\n": # we seem to need {bye} even if chrome closes NOTE - if debug: - print("read_jsons pipe was closed, raising", file=sys.stderr) raise ChannelClosedError while raw_buffer[-1] != 0: # still not great, return what you have @@ -93,64 +78,40 @@ def read_jsons(self, *, blocking=True, debug=None): # noqa: PLR0912, C901 branc os.set_blocking(self._read_from_browser, True) raw_buffer += os.read(self._read_from_browser, 10000) except BlockingIOError: - if debug: - print("read_jsons: BlockingIOError caught.", file=sys.stderr) return jsons except OSError as e: self.close() - if debug: - print(f"caught OSError in read() {e!s}", file=sys.stderr) if not raw_buffer or raw_buffer == b"{bye}\n": raise ChannelClosedError from e # this could be hard to test as it is a real OS corner case decoded_buffer = raw_buffer.decode("utf-8") - if debug: - print(decoded_buffer, file=sys.stderr) for raw_message in decoded_buffer.split("\0"): if raw_message: try: jsons.append(wire.deserialize(raw_message)) - except JSONError as e: - if debug: - print( - f"Problem with {raw_message} in json: {e}", - file=sys.stderr, - ) - if debug: - # This debug is kinda late but the jsons package - # helps with decoding, since JSON optionally - # allows escaping unicode characters, which chrome does (oof) - print(f"read_jsons: {jsons[-1]}", file=sys.stderr) + except JSONError: + logger.exception("JSONError decoding message.") return jsons def _unblock_fd(self, fd): try: if _with_block: os.set_blocking(fd, False) - except BaseException as e: # noqa: BLE001 OS errors are not consistent, catch blind - # also, best effort. - if self.debug: - print(f"Expected error unblocking {fd!s}: {e!s}", file=sys.stderr) + except BaseException: # noqa: BLE001, S110 OS errors are not consistent, catch blind + pass + pass def _close_fd(self, fd): try: os.close(fd) - except BaseException as e: # noqa: BLE001 OS errors are not consistent, catch blind - # also, best effort. - if self.debug: - print(f"Expected error closing {fd!s}: {e!s}", file=sys.stderr) + except BaseException: # noqa: BLE001, S110 OS errors are not consistent, catch blind + pass + pass def _fake_bye(self): self._unblock_fd(self._write_from_browser) try: os.write(self._write_from_browser, b"{bye}\n") - except BaseException as e: # noqa: BLE001 OS errors are not consistent, catch blind - # also, best effort. - if self.debug: - print( - f"Caught expected error in self-wrte bye: {e!s}", - file=sys.stderr, - ) + except BaseException: # noqa: BLE001, S110 OS errors are not consistent, catch blind + pass + pass def close(self): if self.shutdown_lock.acquire(blocking=False): diff --git a/choreographer/_cli_utils/_cli_utils.py b/choreographer/_cli_utils/_cli_utils.py index 9e7cbbd1..8535853c 100644 --- a/choreographer/_cli_utils/_cli_utils.py +++ b/choreographer/_cli_utils/_cli_utils.py @@ -71,6 +71,7 @@ def get_chrome_sync( *, verbose=False, ): + """Download chrome synchronously: see `get_chrome()`.""" path = Path(path) browser_list = json.loads( urllib.request.urlopen( # noqa: S310 audit url for schemes @@ -117,6 +118,16 @@ async def get_chrome( i=-1, path=_default_download_path, ): + """ + Download google chrome from google-chrome-for-testing server. + + Args: + arch: the target platform/os, as understood by google's json directory. + i: the chrome version: -1 being the latest version, 0 being the oldest + still in the testing directory. + path: where to download it too (the folder). + + """ return await asyncio.to_thread(get_chrome_sync, arch=arch, i=i, path=path) diff --git a/choreographer/_sys_utils/_tmpfile.py b/choreographer/_sys_utils/_tmpfile.py index 871f6405..78d46420 100644 --- a/choreographer/_sys_utils/_tmpfile.py +++ b/choreographer/_sys_utils/_tmpfile.py @@ -9,18 +9,35 @@ from pathlib import Path from threading import Thread +import logistro + +logger = logistro.getLogger(__name__) + class TmpDirWarning(UserWarning): - pass + """A warning if for whatever reason we can't eliminate the tmp dir.""" -# Python's built-in temporary directory functions are lacking -# In short, they don't handle removal well, and there's -# lots of API changes over recent versions. -# Here we have our own class to deal with it. class TmpDirectory: + """ + The python stdlib TemporaryDirectory wrapper for easier use. + + Python's TemporaryDirectory suffered a couple API changes that mean + you can't call it the same way for similar versions. This wrapper is + also much more aggressive about deleting the directory when it's done, + not necessarily relying on OS functions. + """ + def __init__(self, path=None, *, sneak=False): - self.debug = True # temporary! TODO + """ + Construct a wrapped TemporaryDirectory (TmpDirectory). + + Args: + path: manually specify the directory to use + sneak: (default False) avoid using /tmp + Ubuntu's snap will sandbox /tmp + + """ self._with_onexc = bool(sys.version_info[:3] >= (3, 12)) args = {} @@ -49,17 +66,10 @@ def __init__(self, path=None, *, sneak=False): self.path = Path(self.temp_dir.name) self.exists = True - if self.debug: - print(f"TEMP DIR PATH: {self.path}", file=sys.stderr) - def delete_manually(self, *, check_only=False): # noqa: C901, PLR0912 + def _delete_manually(self, *, check_only=False): # noqa: C901, PLR0912 if not self.path.exists(): self.exists = False - if self.debug: - print( - "No retry delete manual necessary, path doesn't exist", - file=sys.stderr, - ) return 0, 0, [] n_dirs = 0 n_files = 0 @@ -70,24 +80,16 @@ def delete_manually(self, *, check_only=False): # noqa: C901, PLR0912 if not check_only: for f in files: fp = Path(root) / f - if self.debug: - print(f"Removing file: {fp}", file=sys.stderr) try: fp.chmod(stat.S_IWUSR) fp.unlink() - if self.debug: - print("Success", file=sys.stderr) except BaseException as e: # noqa: BLE001 yes catch and report errors.append((fp, e)) for d in dirs: fp = Path(root) / d - if self.debug: - print(f"Removing dir: {fp}", file=sys.stderr) try: fp.chmod(stat.S_IWUSR) fp.rmdir() - if self.debug: - print("Success", file=sys.stderr) except BaseException as e: # noqa: BLE001 yes catch and report errors.append((fp, e)) @@ -116,18 +118,15 @@ def delete_manually(self, *, check_only=False): # noqa: C901, PLR0912 return n_dirs, n_files, errors - def clean(self): # noqa: C901 + def clean(self): + """Try several different ways to eliminate the temporary directory.""" try: # no faith in this python implementation, always fails with windows # very unstable recently as well, lots new arguments in tempfile package self.temp_dir.cleanup() self.exists = False - except BaseException as e: # noqa: BLE001 yes catch and report - if self.debug: - print( - f"First tempdir deletion failed: TmpDirWarning: {e!s}", - file=sys.stderr, - ) + except BaseException: + logger.exception("TemporaryDirectory.cleanup() failed.") def remove_readonly(func, path, _excinfo): try: @@ -145,24 +144,17 @@ def remove_readonly(func, path, _excinfo): del self.temp_dir except FileNotFoundError: pass # it worked! - except BaseException as e: # noqa: BLE001 yes catch like this and report and try - if self.debug: - print( - f"Second tmpdir deletion failed (shutil.rmtree): {e!s}", - file=sys.stderr, - ) - self.delete_manually(check_only=True) + except BaseException: + self._delete_manually(check_only=True) if not self.exists: return + logger.exception("shutil.rmtree() failed to delete temporary file.") def extra_clean(): time.sleep(3) - self.delete_manually() + self._delete_manually() t = Thread(target=extra_clean) t.run() - if self.debug: - print( - f"Tmpfile still exists?: {self.path.exists()!s}", - file=sys.stderr, - ) + if self.path.exists(): + logger.warning("Temporary dictory couldn't be removed manually.") diff --git a/choreographer/_sys_utils/_which.py b/choreographer/_sys_utils/_which.py index 168a54c2..827bf593 100644 --- a/choreographer/_sys_utils/_which.py +++ b/choreographer/_sys_utils/_which.py @@ -34,6 +34,14 @@ def _which_from_windows_reg(): def browser_which(executable_names, *, skip_local=False): + """ + Look for and return first name found in PATH. + + Args: + executable_names: the list of names to look for + skip_local: (default False) don't look for a choreo download of anything. + + """ path = None if isinstance(executable_names, str): @@ -65,4 +73,10 @@ def browser_which(executable_names, *, skip_local=False): def get_browser_path(*args, **kwargs): + """ + Call `browser_which()` but check for user override first. + + Accepts the same arguments as `browser_which`. + + """ return os.environ.get("BROWSER_PATH", browser_which(*args, **kwargs)) diff --git a/choreographer/browser_sync.py b/choreographer/browser_sync.py index dd1b723c..19e2fdd9 100644 --- a/choreographer/browser_sync.py +++ b/choreographer/browser_sync.py @@ -1,3 +1,5 @@ +"""Provides the sync api: `BrowserSync`, `TabSync`, `TargetSync` and `SessionSync`.""" + import os import subprocess from threading import Lock @@ -13,7 +15,20 @@ class SessionSync: + """A session is a single conversation with a single target.""" + def __init__(self, browser, session_id): + """ + Construct a session from the browser as an object. + + A session is like an open conversation with a target. + All commands are sent on sessions. + + Args: + browser: a reference to the main browser + session_id: the id given by the browser + + """ if not isinstance(session_id, str): raise TypeError("session_id must be a string") # Resources @@ -21,9 +36,20 @@ def __init__(self, browser, session_id): # State self.session_id = session_id + logger.debug(f"New session: {session_id}") self.message_id = 0 def send_command(self, command, params=None): + """ + Send a devtools command on the session. + + https://chromedevtools.github.io/devtools-protocol/ + + Args: + command: devtools command to send + params: the parameters to send + + """ current_id = self.message_id self.message_id += 1 json_command = { @@ -35,14 +61,20 @@ def send_command(self, command, params=None): json_command["sessionId"] = self.session_id if params: json_command["params"] = params - + logger.debug( + f"Sending {command} with {params} on session {self.session_id}", + ) return self.browser.broker.send_json(json_command) class TargetSync: + """A target like a browser, tab, or others. It sends commands. It has sessions.""" + _session_type = SessionSync + """Like generic typing<>. This is the session type associated with TargetSync.""" def __init__(self, target_id, browser): + """Create a target after one ahs been created by the browser.""" if not isinstance(target_id, str): raise TypeError("target_id must be string") # Resources @@ -51,6 +83,7 @@ def __init__(self, target_id, browser): # States self.sessions = {} self.target_id = target_id + logger.info(f"Created new target {target_id}.") def _add_session(self, session): if not isinstance(session, self._session_type): @@ -72,13 +105,27 @@ def _get_first_session(self): return next(iter(self.sessions.values())) def send_command(self, command, params=None): + """ + Send a command to the first session in a target. + + https://chromedevtools.github.io/devtools-protocol/ + + Args: + command: devtools command to send + params: the parameters to send + + """ if not self.sessions.values(): raise RuntimeError("Cannot send_command without at least one valid session") - return self._get_first_session().send_command(command, params) + session = self._get_first_session() + logger.debug( + f"Sending {command} with {params} on session {session.session_id}", + ) + return session.send_command(command, params) class TabSync(TargetSync): - pass + """A wrapper for TargetSync, so user can use TabSync, not TargetSync.""" class BrowserSync(TargetSync): @@ -106,6 +153,18 @@ def _release_lock(self): return False def __init__(self, path=None, *, browser_cls=Chromium, channel_cls=Pipe, **kwargs): + """ + Construct a new browser instance. + + Args: + path: The path to the browser executable. + browser_cls: The type of browser (default: `Chromium`). + channel_cls: The type of channel to browser (default: `Pipe`). + kwargs: The arguments that the browser_cls takes. For example, + headless=True/False, enable_gpu=True/False, etc. + + """ + logger.debug("Attempting to open new browser.") self._make_lock() self.tabs = {} self.targets = {} @@ -125,6 +184,7 @@ def __init__(self, path=None, *, browser_cls=Chromium, channel_cls=Pipe, **kwarg # we do need something to indicate we're open TODO yeah an open lock def open(self): + """Open the browser.""" if not self._lock_open(): raise RuntimeError("Can't re-open the browser") self.subprocess = subprocess.Popen( # noqa: S603 @@ -137,6 +197,7 @@ def open(self): self._add_session(self._session_type(self, "")) def __enter__(self): + """Open browser as context to launch on entry and close on exit.""" self.open() return self @@ -173,17 +234,26 @@ def _close(self): raise RuntimeError("Couldn't close or kill browser subprocess") def close(self): + """Close the browser.""" + self.broker.clean() + logger.info("Broker cleaned up.") if not self._release_lock(): return try: + logger.info("Trying to close browser.") self._close() + logger.info("browser._close() called successfully.") except ProcessLookupError: pass os.close(self.logger_pipe) + logger.info("Logging pipe closed.") self.channel.close() + logger.info("Browser channel closed.") self.browser_impl.clean() + logger.info("Browser implementation cleaned up.") def __exit__(self, exc_type, exc_value, exc_traceback): + """Close the browser.""" self.close() def _add_tab(self, tab): @@ -197,10 +267,12 @@ def _remove_tab(self, target_id): del self.tabs[target_id] def get_tab(self): + """Get the first tab if there is one. Useful for default tabs.""" if self.tabs.values(): return next(iter(self.tabs.values())) return None # wrap our broker for convenience def start_output_thread(self, **kwargs): + """Start a separate thread that dumps all messages received to stdout.""" self.broker.run_output_thread(**kwargs) diff --git a/choreographer/protocol.py b/choreographer/protocol.py index d8b1d952..505ab5c3 100644 --- a/choreographer/protocol.py +++ b/choreographer/protocol.py @@ -10,20 +10,34 @@ class Ecode(Enum): class DevtoolsProtocolError(Exception): - """.""" + """Raise a general error reported by the devtools protocol.""" def __init__(self, response): - """.""" + """ + Construct a new DevtoolsProtocolError. + + Args: + response: the json response that contains the error + + """ super().__init__(response) self.code = response["error"]["code"] self.message = response["error"]["message"] class MessageTypeError(TypeError): - """.""" + """An error for poorly formatted devtools protocol message.""" def __init__(self, key, value, expected_type): - """.""" + """ + Construct a message about a poorly formed protocol message. + + Args: + key: the key that has the badly typed value + value: the type of the value that is incorrect + expected_type: the type that was expected + + """ value = type(value) if not isinstance(value, type) else value super().__init__( f"Message with key {key} must have type {expected_type}, not {value}.", @@ -31,20 +45,39 @@ def __init__(self, key, value, expected_type): class MissingKeyError(ValueError): - """.""" + """An error for poorly formatted devtools protocol message.""" def __init__(self, key, obj): - """.""" + """ + Construct a MissingKeyError specifying which key was missing. + + Args: + key: the missing key + obj: the message without the key + + """ super().__init__( f"Message missing required key/s {key}. Message received: {obj}", ) class ExperimentalFeatureWarning(UserWarning): - """.""" + """An warning to report that a feature may or may not work.""" def verify_params(obj): + """ + Verify the message obj hast he proper keys and values. + + Args: + obj: the object to check. + + Raises: + MissingKeyError: if a key is missing. + MessageTypeError: if a value type is incorrect. + RuntimeError: if there are strange keys. + + """ n_keys = 0 required_keys = {"id": int, "method": str} @@ -68,6 +101,15 @@ def verify_params(obj): def calculate_message_key(response): + """ + Given a response from the browser, calculate the key corresponding to the command. + + Every message is uniquely identified by its sessionId and id (counter). + + Args: + response: the message for which to calculate the key. + + """ session_id = response.get("sessionId", "") message_id = response.get("id", None) if message_id is None: @@ -76,6 +118,14 @@ def calculate_message_key(response): def match_message_key(response, key): + """ + Report True if a response matches with a certain key (sessionId, id). + + Args: + response: the object response from the browser + key: the (sessionId, id) key tubple we're looking for + + """ session_id, message_id = key if ("session_id" not in response and session_id == "") or ( # is browser session "session_id" in response and response["session_id"] == session_id # is session @@ -92,11 +142,19 @@ def match_message_key(response, key): def is_event(response): + """Return true if the browser response is an event notification.""" required_keys = {"method", "params"} return required_keys <= response.keys() and "id" not in response def get_target_id_from_result(response): + """ + Extract target id from a browser response. + + Args: + response: the browser response to extract the targetId from. + + """ if "result" in response and "targetId" in response["result"]: return response["result"]["targetId"] else: @@ -104,6 +162,13 @@ def get_target_id_from_result(response): def get_session_id_from_result(response): + """ + Extract session id from a browser response. + + Args: + response: the browser response to extract the sessionId from. + + """ if "result" in response and "sessionId" in response["result"]: return response["result"]["sessionId"] else: @@ -111,6 +176,13 @@ def get_session_id_from_result(response): def get_error_from_result(response): + """ + Extract error from a browser response. + + Args: + response: the browser response to extract the error from. + + """ if "error" in response: return response["error"] else: From f55268e9ba2ccdc844c8ba27ef9277ed8fd3db97 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Mon, 6 Jan 2025 23:49:31 -0500 Subject: [PATCH 36/88] Commit for mkdocs - no push --- choreographer/__init__.py | 18 +- mkdocs.yml | 39 +++ pyproject.toml | 16 +- uv.lock | 671 +++++++++++++++++++++++++++----------- 4 files changed, 525 insertions(+), 219 deletions(-) create mode 100644 mkdocs.yml diff --git a/choreographer/__init__.py b/choreographer/__init__.py index d7ed3548..5a1cda38 100644 --- a/choreographer/__init__.py +++ b/choreographer/__init__.py @@ -28,36 +28,20 @@ TargetSync, ) -_sync_api = [ +__all__ = [ "BrowserSync", "SessionSync", "TabSync", "TargetSync", -] - -_browser_impls = [ "Chromium", -] - -_errors = [ "BrowserClosedError", "BrowserFailedError", "ChannelClosedError", "BlockWarning", "TmpDirWarning", -] - -_utils = [ "get_chrome", "get_chrome_sync", "TmpDirectory", "browser_which", "get_browser_path", ] - -__all__ = [ # noqa: PLE0604 non-string in all - *_sync_api, - *_browser_impls, - *_errors, - *_utils, -] diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..59b3cd42 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,39 @@ +--- +### Site metadata ### + +site_name: choreographer +repo_name: github + + +### Build settings ### + +docs_dir: 'docs/' +nav: + - Readme: >- + { + "dest": "README.md", + "src": "../README.md", + "replace": {"src='docs/": "src='"} + } + - Reference: >- + { + "api": "choreographer", + "test": "exports", + "tree": "imports" + } + # CLI tools? + +theme: + name: material +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences +plugins: + - quimeta + - quicopy + - quiapi diff --git a/pyproject.toml b/pyproject.toml index 20feb7f7..85ea879a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ enabled = true name = "choreographer" description = "Devtools Protocol implementation for chrome." readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.11" # TODO change back dynamic = ["version"] authors = [ {name = "Andrew Pikul", email="ajpikul@gmail.com"}, @@ -28,7 +28,7 @@ dependencies = [ [project.urls] Homepage = "https://github.com/plotly/choreographer" -Repository = "https://github.com/geopozo/logistro" +Repository = "https://github.com/plotly/choreographer" [dependency-groups] dev = [ @@ -45,14 +45,14 @@ dev = [ # this group we need to require higher python # and only resolve if explicitly asked for -#docs = [ -# "mkquixote @ git+ssh://git@github.com/geopozo/mkquixote", -# "mkdocs>=1.6.1", -# "mkdocs-material>=9.5.49", -#] +docs = [ + "mkquixote @ git+ssh://git@github.com/geopozo/mkquixote", + "mkdocs>=1.6.1", + "mkdocs-material>=9.5.49", +] [tool.uv.sources] -logistro = { path = "../logistro", editable = true } + mkquixote = { path = "../mkquixote", editable = true } [project.scripts] choreo_diagnose = "choreographer._cli_utils:diagnose" diff --git a/uv.lock b/uv.lock index 8eaf5cf4..bd3eb1b8 100644 --- a/uv.lock +++ b/uv.lock @@ -1,9 +1,5 @@ version = 1 -requires-python = ">=3.9" -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version < '3.10'", -] +requires-python = ">=3.11" [[package]] name = "async-timeout" @@ -14,9 +10,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, ] +[[package]] +name = "babel" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, +] + +[[package]] +name = "certifi" +version = "2024.12.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, + { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, + { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, + { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, + { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, + { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, + { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, + { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, + { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, + { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, + { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, + { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, + { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, + { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, + { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, + { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, + { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, + { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, + { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, + { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, + { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, + { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, + { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, + { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, + { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, + { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +] + [[package]] name = "choreographer" -version = "1.0.0a0.post87+git.c84ac06b.dirty" +version = "1.0.0a0.post91+git.24794414.dirty" source = { editable = "." } dependencies = [ { name = "logistro" }, @@ -26,17 +88,21 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "async-timeout" }, - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numpy" }, { name = "poethepoet" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-xdist" }, ] +docs = [ + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "mkquixote" }, +] [package.metadata] requires-dist = [ - { name = "logistro", editable = "../logistro" }, + { name = "logistro", specifier = ">=1.0.2" }, { name = "simplejson" }, ] @@ -49,23 +115,31 @@ dev = [ { name = "pytest-asyncio" }, { name = "pytest-xdist" }, ] +docs = [ + { name = "mkdocs", specifier = ">=1.6.1" }, + { name = "mkdocs-material", specifier = ">=9.5.49" }, + { name = "mkquixote", editable = "../mkquixote" }, +] [[package]] -name = "colorama" -version = "0.4.6" +name = "click" +version = "8.1.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, ] [[package]] -name = "exceptiongroup" -version = "1.2.2" +name = "colorama" +version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] [[package]] @@ -77,6 +151,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612 }, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034 }, +] + +[[package]] +name = "griffe" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/9b/0bc9d53ed6628aae43223dd3c081637da54f66ed17a8c1d9fd36ee5da244/griffe-1.5.4.tar.gz", hash = "sha256:073e78ad3e10c8378c2f798bd4ef87b92d8411e9916e157fd366a17cc4fd4e52", size = 389376 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/29/d0f156c076ec71eb485e70cbcde4872e3c045cda965a48d1d938aa3d9f76/griffe-1.5.4-py3-none-any.whl", hash = "sha256:ed33af890586a5bebc842fcb919fc694b3dc1bc55b7d9e0228de41ce566b4a1d", size = 128102 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + [[package]] name = "iniconfig" version = "2.0.0" @@ -86,12 +193,180 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] +[[package]] +name = "jinja2" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 }, +] + [[package]] name = "logistro" -version = "1.0.2.post1+git.4c99c29e" -source = { editable = "../logistro" } +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/37/616339aa4c47c8316caa74c6be0e42a7db2e5f11bb4df7b1589e2347c8d2/logistro-1.0.2.tar.gz", hash = "sha256:0d437c0f4fd9abef6eca0a28d85c171fc14f4f17aa1ce0f2f91d2e93f82a5c18", size = 5973 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/fb/a0d753951886ace90e2a92f55f119450e0dedabe81733c9f7655c9009d1c/logistro-1.0.2-py3-none-any.whl", hash = "sha256:f3435161b12a7f6a463aa23885c076ed2d2876255db9b5af3e302bdba6e2ed02", size = 5343 }, +] + +[[package]] +name = "markdown" +version = "3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354 }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451 }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521 }, +] + +[[package]] +name = "mkdocs-material" +version = "9.5.49" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/14/8daeeecee2e25bd84239a843fdcb92b20db88ebbcb26e0d32f414ca54a22/mkdocs_material-9.5.49.tar.gz", hash = "sha256:3671bb282b4f53a1c72e08adbe04d2481a98f85fed392530051f80ff94a9621d", size = 3949559 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/2d/2dd23a36b48421db54f118bb6f6f733dbe2d5c78fe7867375e48649fd3df/mkdocs_material-9.5.49-py3-none-any.whl", hash = "sha256:c3c2d8176b18198435d3a3e119011922f3e11424074645c24019c2dcf08a360e", size = 8684098 }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728 }, +] + +[[package]] +name = "mkquixote" +version = "0.0.1" +source = { editable = "../mkquixote" } +dependencies = [ + { name = "griffe" }, + { name = "jinja2" }, + { name = "mkdocs" }, + { name = "mkdocs-material" }, +] [package.metadata] +requires-dist = [ + { name = "griffe", specifier = ">=1.5.1" }, + { name = "jinja2", specifier = ">=3.1.4" }, + { name = "mkdocs", specifier = ">=1.6.1" }, + { name = "mkdocs-material", specifier = ">=9.5.49" }, +] [package.metadata.requires-dev] dev = [ @@ -104,21 +379,8 @@ dev = [ name = "numpy" version = "2.0.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015 } wheels = [ - { url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece", size = 21165245 }, - { url = "https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04", size = 13738540 }, - { url = "https://files.pythonhosted.org/packages/ec/31/cc46e13bf07644efc7a4bf68df2df5fb2a1a88d0cd0da9ddc84dc0033e51/numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66", size = 5300623 }, - { url = "https://files.pythonhosted.org/packages/6e/16/7bfcebf27bb4f9d7ec67332ffebee4d1bf085c84246552d52dbb548600e7/numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b", size = 6901774 }, - { url = "https://files.pythonhosted.org/packages/f9/a3/561c531c0e8bf082c5bef509d00d56f82e0ea7e1e3e3a7fc8fa78742a6e5/numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd", size = 13907081 }, - { url = "https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318", size = 19523451 }, - { url = "https://files.pythonhosted.org/packages/25/7f/0b209498009ad6453e4efc2c65bcdf0ae08a182b2b7877d7ab38a92dc542/numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8", size = 19927572 }, - { url = "https://files.pythonhosted.org/packages/3e/df/2619393b1e1b565cd2d4c4403bdd979621e2c4dea1f8532754b2598ed63b/numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326", size = 14400722 }, - { url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170 }, - { url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558 }, { url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137 }, { url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552 }, { url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957 }, @@ -139,85 +401,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701 }, { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313 }, { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179 }, - { url = "https://files.pythonhosted.org/packages/43/c1/41c8f6df3162b0c6ffd4437d729115704bd43363de0090c7f913cfbc2d89/numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c", size = 21169942 }, - { url = "https://files.pythonhosted.org/packages/39/bc/fd298f308dcd232b56a4031fd6ddf11c43f9917fbc937e53762f7b5a3bb1/numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd", size = 13711512 }, - { url = "https://files.pythonhosted.org/packages/96/ff/06d1aa3eeb1c614eda245c1ba4fb88c483bee6520d361641331872ac4b82/numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b", size = 5306976 }, - { url = "https://files.pythonhosted.org/packages/2d/98/121996dcfb10a6087a05e54453e28e58694a7db62c5a5a29cee14c6e047b/numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729", size = 6906494 }, - { url = "https://files.pythonhosted.org/packages/15/31/9dffc70da6b9bbf7968f6551967fc21156207366272c2a40b4ed6008dc9b/numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1", size = 13912596 }, - { url = "https://files.pythonhosted.org/packages/b9/14/78635daab4b07c0930c919d451b8bf8c164774e6a3413aed04a6d95758ce/numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd", size = 19526099 }, - { url = "https://files.pythonhosted.org/packages/26/4c/0eeca4614003077f68bfe7aac8b7496f04221865b3a5e7cb230c9d055afd/numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d", size = 19932823 }, - { url = "https://files.pythonhosted.org/packages/f1/46/ea25b98b13dccaebddf1a803f8c748680d972e00507cd9bc6dcdb5aa2ac1/numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d", size = 14404424 }, - { url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809 }, - { url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314 }, - { url = "https://files.pythonhosted.org/packages/8f/3b/df5a870ac6a3be3a86856ce195ef42eec7ae50d2a202be1f5a4b3b340e14/numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8", size = 21025288 }, - { url = "https://files.pythonhosted.org/packages/2c/97/51af92f18d6f6f2d9ad8b482a99fb74e142d71372da5d834b3a2747a446e/numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4", size = 6762793 }, - { url = "https://files.pythonhosted.org/packages/12/46/de1fbd0c1b5ccaa7f9a005b66761533e2f6a3e560096682683a223631fe9/numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c", size = 19334885 }, - { url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784 }, -] - -[[package]] -name = "numpy" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/fdbf6a7871703df6160b5cf3dd774074b086d278172285c52c2758b76305/numpy-2.2.1.tar.gz", hash = "sha256:45681fd7128c8ad1c379f0ca0776a8b0c6583d2f69889ddac01559dfe4390918", size = 20227662 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/c4/5588367dc9f91e1a813beb77de46ea8cab13f778e1b3a0e661ab031aba44/numpy-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5edb4e4caf751c1518e6a26a83501fda79bff41cc59dac48d70e6d65d4ec4440", size = 21213214 }, - { url = "https://files.pythonhosted.org/packages/d8/8b/32dd9f08419023a4cf856c5ad0b4eba9b830da85eafdef841a104c4fc05a/numpy-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa3017c40d513ccac9621a2364f939d39e550c542eb2a894b4c8da92b38896ab", size = 14352248 }, - { url = "https://files.pythonhosted.org/packages/84/2d/0e895d02940ba6e12389f0ab5cac5afcf8dc2dc0ade4e8cad33288a721bd/numpy-2.2.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:61048b4a49b1c93fe13426e04e04fdf5a03f456616f6e98c7576144677598675", size = 5391007 }, - { url = "https://files.pythonhosted.org/packages/11/b9/7f1e64a0d46d9c2af6d17966f641fb12d5b8ea3003f31b2308f3e3b9a6aa/numpy-2.2.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:7671dc19c7019103ca44e8d94917eba8534c76133523ca8406822efdd19c9308", size = 6926174 }, - { url = "https://files.pythonhosted.org/packages/2e/8c/043fa4418bc9364e364ab7aba8ff6ef5f6b9171ade22de8fbcf0e2fa4165/numpy-2.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4250888bcb96617e00bfa28ac24850a83c9f3a16db471eca2ee1f1714df0f957", size = 14330914 }, - { url = "https://files.pythonhosted.org/packages/f7/b6/d8110985501ca8912dfc1c3bbef99d66e62d487f72e46b2337494df77364/numpy-2.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7746f235c47abc72b102d3bce9977714c2444bdfaea7888d241b4c4bb6a78bf", size = 16379607 }, - { url = "https://files.pythonhosted.org/packages/e2/57/bdca9fb8bdaa810c3a4ff2eb3231379b77f618a7c0d24be9f7070db50775/numpy-2.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:059e6a747ae84fce488c3ee397cee7e5f905fd1bda5fb18c66bc41807ff119b2", size = 15541760 }, - { url = "https://files.pythonhosted.org/packages/97/55/3b9147b3cbc3b6b1abc2a411dec5337a46c873deca0dd0bf5bef9d0579cc/numpy-2.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f62aa6ee4eb43b024b0e5a01cf65a0bb078ef8c395e8713c6e8a12a697144528", size = 18168476 }, - { url = "https://files.pythonhosted.org/packages/00/e7/7c2cde16c9b87a8e14fdd262ca7849c4681cf48c8a774505f7e6f5e3b643/numpy-2.2.1-cp310-cp310-win32.whl", hash = "sha256:48fd472630715e1c1c89bf1feab55c29098cb403cc184b4859f9c86d4fcb6a95", size = 6570985 }, - { url = "https://files.pythonhosted.org/packages/a1/a8/554b0e99fc4ac11ec481254781a10da180d0559c2ebf2c324232317349ee/numpy-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:b541032178a718c165a49638d28272b771053f628382d5e9d1c93df23ff58dbf", size = 12913384 }, - { url = "https://files.pythonhosted.org/packages/59/14/645887347124e101d983e1daf95b48dc3e136bf8525cb4257bf9eab1b768/numpy-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40f9e544c1c56ba8f1cf7686a8c9b5bb249e665d40d626a23899ba6d5d9e1484", size = 21217379 }, - { url = "https://files.pythonhosted.org/packages/9f/fd/2279000cf29f58ccfd3778cbf4670dfe3f7ce772df5e198c5abe9e88b7d7/numpy-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9b57eaa3b0cd8db52049ed0330747b0364e899e8a606a624813452b8203d5f7", size = 14388520 }, - { url = "https://files.pythonhosted.org/packages/58/b0/034eb5d5ba12d66ab658ff3455a31f20add0b78df8203c6a7451bd1bee21/numpy-2.2.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bc8a37ad5b22c08e2dbd27df2b3ef7e5c0864235805b1e718a235bcb200cf1cb", size = 5389286 }, - { url = "https://files.pythonhosted.org/packages/5d/69/6f3cccde92e82e7835fdb475c2bf439761cbf8a1daa7c07338e1e132dfec/numpy-2.2.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:9036d6365d13b6cbe8f27a0eaf73ddcc070cae584e5ff94bb45e3e9d729feab5", size = 6930345 }, - { url = "https://files.pythonhosted.org/packages/d1/72/1cd38e91ab563e67f584293fcc6aca855c9ae46dba42e6b5ff4600022899/numpy-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51faf345324db860b515d3f364eaa93d0e0551a88d6218a7d61286554d190d73", size = 14335748 }, - { url = "https://files.pythonhosted.org/packages/f2/d4/f999444e86986f3533e7151c272bd8186c55dda554284def18557e013a2a/numpy-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38efc1e56b73cc9b182fe55e56e63b044dd26a72128fd2fbd502f75555d92591", size = 16391057 }, - { url = "https://files.pythonhosted.org/packages/99/7b/85cef6a3ae1b19542b7afd97d0b296526b6ef9e3c43ea0c4d9c4404fb2d0/numpy-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:31b89fa67a8042e96715c68e071a1200c4e172f93b0fbe01a14c0ff3ff820fc8", size = 15556943 }, - { url = "https://files.pythonhosted.org/packages/69/7e/b83cc884c3508e91af78760f6b17ab46ad649831b1fa35acb3eb26d9e6d2/numpy-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4c86e2a209199ead7ee0af65e1d9992d1dce7e1f63c4b9a616500f93820658d0", size = 18180785 }, - { url = "https://files.pythonhosted.org/packages/b2/9f/eb4a9a38867de059dcd4b6e18d47c3867fbd3795d4c9557bb49278f94087/numpy-2.2.1-cp311-cp311-win32.whl", hash = "sha256:b34d87e8a3090ea626003f87f9392b3929a7bbf4104a05b6667348b6bd4bf1cd", size = 6568983 }, - { url = "https://files.pythonhosted.org/packages/6d/1e/be3b9f3073da2f8c7fa361fcdc231b548266b0781029fdbaf75eeab997fd/numpy-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:360137f8fb1b753c5cde3ac388597ad680eccbbbb3865ab65efea062c4a1fd16", size = 12917260 }, - { url = "https://files.pythonhosted.org/packages/62/12/b928871c570d4a87ab13d2cc19f8817f17e340d5481621930e76b80ffb7d/numpy-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:694f9e921a0c8f252980e85bce61ebbd07ed2b7d4fa72d0e4246f2f8aa6642ab", size = 20909861 }, - { url = "https://files.pythonhosted.org/packages/3d/c3/59df91ae1d8ad7c5e03efd63fd785dec62d96b0fe56d1f9ab600b55009af/numpy-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3683a8d166f2692664262fd4900f207791d005fb088d7fdb973cc8d663626faa", size = 14095776 }, - { url = "https://files.pythonhosted.org/packages/af/4e/8ed5868efc8e601fb69419644a280e9c482b75691466b73bfaab7d86922c/numpy-2.2.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:780077d95eafc2ccc3ced969db22377b3864e5b9a0ea5eb347cc93b3ea900315", size = 5126239 }, - { url = "https://files.pythonhosted.org/packages/1a/74/dd0bbe650d7bc0014b051f092f2de65e34a8155aabb1287698919d124d7f/numpy-2.2.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:55ba24ebe208344aa7a00e4482f65742969a039c2acfcb910bc6fcd776eb4355", size = 6659296 }, - { url = "https://files.pythonhosted.org/packages/7f/11/4ebd7a3f4a655764dc98481f97bd0a662fb340d1001be6050606be13e162/numpy-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b1d07b53b78bf84a96898c1bc139ad7f10fda7423f5fd158fd0f47ec5e01ac7", size = 14047121 }, - { url = "https://files.pythonhosted.org/packages/7f/a7/c1f1d978166eb6b98ad009503e4d93a8c1962d0eb14a885c352ee0276a54/numpy-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5062dc1a4e32a10dc2b8b13cedd58988261416e811c1dc4dbdea4f57eea61b0d", size = 16096599 }, - { url = "https://files.pythonhosted.org/packages/3d/6d/0e22afd5fcbb4d8d0091f3f46bf4e8906399c458d4293da23292c0ba5022/numpy-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fce4f615f8ca31b2e61aa0eb5865a21e14f5629515c9151850aa936c02a1ee51", size = 15243932 }, - { url = "https://files.pythonhosted.org/packages/03/39/e4e5832820131ba424092b9610d996b37e5557180f8e2d6aebb05c31ae54/numpy-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:67d4cda6fa6ffa073b08c8372aa5fa767ceb10c9a0587c707505a6d426f4e046", size = 17861032 }, - { url = "https://files.pythonhosted.org/packages/5f/8a/3794313acbf5e70df2d5c7d2aba8718676f8d054a05abe59e48417fb2981/numpy-2.2.1-cp312-cp312-win32.whl", hash = "sha256:32cb94448be47c500d2c7a95f93e2f21a01f1fd05dd2beea1ccd049bb6001cd2", size = 6274018 }, - { url = "https://files.pythonhosted.org/packages/17/c1/c31d3637f2641e25c7a19adf2ae822fdaf4ddd198b05d79a92a9ce7cb63e/numpy-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:ba5511d8f31c033a5fcbda22dd5c813630af98c70b2661f2d2c654ae3cdfcfc8", size = 12613843 }, - { url = "https://files.pythonhosted.org/packages/20/d6/91a26e671c396e0c10e327b763485ee295f5a5a7a48c553f18417e5a0ed5/numpy-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f1d09e520217618e76396377c81fba6f290d5f926f50c35f3a5f72b01a0da780", size = 20896464 }, - { url = "https://files.pythonhosted.org/packages/8c/40/5792ccccd91d45e87d9e00033abc4f6ca8a828467b193f711139ff1f1cd9/numpy-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ecc47cd7f6ea0336042be87d9e7da378e5c7e9b3c8ad0f7c966f714fc10d821", size = 14111350 }, - { url = "https://files.pythonhosted.org/packages/c0/2a/fb0a27f846cb857cef0c4c92bef89f133a3a1abb4e16bba1c4dace2e9b49/numpy-2.2.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f419290bc8968a46c4933158c91a0012b7a99bb2e465d5ef5293879742f8797e", size = 5111629 }, - { url = "https://files.pythonhosted.org/packages/eb/e5/8e81bb9d84db88b047baf4e8b681a3e48d6390bc4d4e4453eca428ecbb49/numpy-2.2.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b6c390bfaef8c45a260554888966618328d30e72173697e5cabe6b285fb2348", size = 6645865 }, - { url = "https://files.pythonhosted.org/packages/7a/1a/a90ceb191dd2f9e2897c69dde93ccc2d57dd21ce2acbd7b0333e8eea4e8d/numpy-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:526fc406ab991a340744aad7e25251dd47a6720a685fa3331e5c59fef5282a59", size = 14043508 }, - { url = "https://files.pythonhosted.org/packages/f1/5a/e572284c86a59dec0871a49cd4e5351e20b9c751399d5f1d79628c0542cb/numpy-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f74e6fdeb9a265624ec3a3918430205dff1df7e95a230779746a6af78bc615af", size = 16094100 }, - { url = "https://files.pythonhosted.org/packages/0c/2c/a79d24f364788386d85899dd280a94f30b0950be4b4a545f4fa4ed1d4ca7/numpy-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:53c09385ff0b72ba79d8715683c1168c12e0b6e84fb0372e97553d1ea91efe51", size = 15239691 }, - { url = "https://files.pythonhosted.org/packages/cf/79/1e20fd1c9ce5a932111f964b544facc5bb9bde7865f5b42f00b4a6a9192b/numpy-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f3eac17d9ec51be534685ba877b6ab5edc3ab7ec95c8f163e5d7b39859524716", size = 17856571 }, - { url = "https://files.pythonhosted.org/packages/be/5b/cc155e107f75d694f562bdc84a26cc930569f3dfdfbccb3420b626065777/numpy-2.2.1-cp313-cp313-win32.whl", hash = "sha256:9ad014faa93dbb52c80d8f4d3dcf855865c876c9660cb9bd7553843dd03a4b1e", size = 6270841 }, - { url = "https://files.pythonhosted.org/packages/44/be/0e5cd009d2162e4138d79a5afb3b5d2341f0fe4777ab6e675aa3d4a42e21/numpy-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:164a829b6aacf79ca47ba4814b130c4020b202522a93d7bff2202bfb33b61c60", size = 12606618 }, - { url = "https://files.pythonhosted.org/packages/a8/87/04ddf02dd86fb17c7485a5f87b605c4437966d53de1e3745d450343a6f56/numpy-2.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4dfda918a13cc4f81e9118dea249e192ab167a0bb1966272d5503e39234d694e", size = 20921004 }, - { url = "https://files.pythonhosted.org/packages/6e/3e/d0e9e32ab14005425d180ef950badf31b862f3839c5b927796648b11f88a/numpy-2.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:733585f9f4b62e9b3528dd1070ec4f52b8acf64215b60a845fa13ebd73cd0712", size = 14119910 }, - { url = "https://files.pythonhosted.org/packages/b5/5b/aa2d1905b04a8fb681e08742bb79a7bddfc160c7ce8e1ff6d5c821be0236/numpy-2.2.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:89b16a18e7bba224ce5114db863e7029803c179979e1af6ad6a6b11f70545008", size = 5153612 }, - { url = "https://files.pythonhosted.org/packages/ce/35/6831808028df0648d9b43c5df7e1051129aa0d562525bacb70019c5f5030/numpy-2.2.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:676f4eebf6b2d430300f1f4f4c2461685f8269f94c89698d832cdf9277f30b84", size = 6668401 }, - { url = "https://files.pythonhosted.org/packages/b1/38/10ef509ad63a5946cc042f98d838daebfe7eaf45b9daaf13df2086b15ff9/numpy-2.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f5cdf9f493b35f7e41e8368e7d7b4bbafaf9660cba53fb21d2cd174ec09631", size = 14014198 }, - { url = "https://files.pythonhosted.org/packages/df/f8/c80968ae01df23e249ee0a4487fae55a4c0fe2f838dfe9cc907aa8aea0fa/numpy-2.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1ad395cf254c4fbb5b2132fee391f361a6e8c1adbd28f2cd8e79308a615fe9d", size = 16076211 }, - { url = "https://files.pythonhosted.org/packages/09/69/05c169376016a0b614b432967ac46ff14269eaffab80040ec03ae1ae8e2c/numpy-2.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:08ef779aed40dbc52729d6ffe7dd51df85796a702afbf68a4f4e41fafdc8bda5", size = 15220266 }, - { url = "https://files.pythonhosted.org/packages/f1/ff/94a4ce67ea909f41cf7ea712aebbe832dc67decad22944a1020bb398a5ee/numpy-2.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:26c9c4382b19fcfbbed3238a14abf7ff223890ea1936b8890f058e7ba35e8d71", size = 17852844 }, - { url = "https://files.pythonhosted.org/packages/46/72/8a5dbce4020dfc595592333ef2fbb0a187d084ca243b67766d29d03e0096/numpy-2.2.1-cp313-cp313t-win32.whl", hash = "sha256:93cf4e045bae74c90ca833cba583c14b62cb4ba2cba0abd2b141ab52548247e2", size = 6326007 }, - { url = "https://files.pythonhosted.org/packages/7b/9c/4fce9cf39dde2562584e4cfd351a0140240f82c0e3569ce25a250f47037d/numpy-2.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bff7d8ec20f5f42607599f9994770fa65d76edca264a87b5e4ea5629bce12268", size = 12693107 }, - { url = "https://files.pythonhosted.org/packages/f1/65/d36a76b811ffe0a4515e290cb05cb0e22171b1b0f0db6bee9141cf023545/numpy-2.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7ba9cc93a91d86365a5d270dee221fdc04fb68d7478e6bf6af650de78a8339e3", size = 21044672 }, - { url = "https://files.pythonhosted.org/packages/aa/3f/b644199f165063154df486d95198d814578f13dd4d8c1651e075bf1cb8af/numpy-2.2.1-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:3d03883435a19794e41f147612a77a8f56d4e52822337844fff3d4040a142964", size = 6789873 }, - { url = "https://files.pythonhosted.org/packages/d7/df/2adb0bb98a3cbe8a6c3c6d1019aede1f1d8b83927ced228a46cc56c7a206/numpy-2.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4511d9e6071452b944207c8ce46ad2f897307910b402ea5fa975da32e0102800", size = 16194933 }, - { url = "https://files.pythonhosted.org/packages/13/3e/1959d5219a9e6d200638d924cedda6a606392f7186a4ed56478252e70d55/numpy-2.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5c5cc0cbabe9452038ed984d05ac87910f89370b9242371bd9079cb4af61811e", size = 12820057 }, ] [[package]] @@ -229,6 +412,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746 }, +] + [[package]] name = "pastel" version = "0.2.1" @@ -238,6 +430,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955 }, ] +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + [[package]] name = "pluggy" version = "1.5.0" @@ -254,24 +464,43 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pastel" }, { name = "pyyaml" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8a/f6/1692a42cf426494d89dbc693ba55ebd653bd2e84bbb6b3da4127b87956df/poethepoet-0.32.0.tar.gz", hash = "sha256:a700be02e932e1a8907ae630928fc769ea9a77986189ba6867e6e3fd8f60e5b7", size = 62962 } wheels = [ { url = "https://files.pythonhosted.org/packages/27/12/2994011e33d37772228439fe215fc022ff180b161ab7bd8ea5ac92717556/poethepoet-0.32.0-py3-none-any.whl", hash = "sha256:fba84c72d923feac228d1ea7734c5a54701f2e71fad42845f027c0fbf998a073", size = 81717 }, ] +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/87/4998d1aac5afea5b081238a609d9814f4c33cd5c7123503276d1105fb6a9/pymdown_extensions-10.13.tar.gz", hash = "sha256:e0b351494dc0d8d14a1f52b39b1499a00ef1566b4ba23dc74f1eba75c736f5dd", size = 843302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/7f/46c7122186759350cf523c71d29712be534f769f073a1d980ce8f095072c/pymdown_extensions-10.13-py3-none-any.whl", hash = "sha256:80bc33d715eec68e683e04298946d47d78c7739e79d808203df278ee8ef89428", size = 264108 }, +] + [[package]] name = "pytest" version = "8.3.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } wheels = [ @@ -303,21 +532,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/82/1d96bf03ee4c0fdc3c0cbe61470070e659ca78dc0086fb88b66c185e2449/pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7", size = 46108 }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + [[package]] name = "pyyaml" version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, @@ -345,15 +577,86 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, - { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 }, - { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 }, - { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 }, - { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614 }, - { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360 }, - { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006 }, - { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577 }, - { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593 }, - { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911 }, +] + +[[package]] +name = "regex" +version = "2024.11.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 }, + { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 }, + { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 }, + { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 }, + { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 }, + { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 }, + { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 }, + { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 }, + { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 }, + { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 }, + { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 }, + { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 }, + { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 }, + { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, + { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, + { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, + { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, + { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, + { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, + { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, + { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, + { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, + { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, + { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, + { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, + { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, + { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, + { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, + { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 }, + { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 }, + { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 }, + { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 }, + { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 }, + { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 }, + { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 }, + { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 }, + { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 }, + { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 }, + { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 }, + { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 }, + { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 }, + { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 }, + { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, ] [[package]] @@ -362,19 +665,6 @@ version = "3.19.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3d/29/085111f19717f865eceaf0d4397bf3e76b08d60428b076b64e2a1903706d/simplejson-3.19.3.tar.gz", hash = "sha256:8e086896c36210ab6050f2f9f095a5f1e03c83fa0e7f296d6cba425411364680", size = 85237 } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/24/260ad03435ce8ef2436031951134659c7161776ec3a78094b35b9375ceea/simplejson-3.19.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:50d8b742d74c449c4dcac570d08ce0f21f6a149d2d9cf7652dbf2ba9a1bc729a", size = 93660 }, - { url = "https://files.pythonhosted.org/packages/63/a1/dee207f357bcd6b106f2ca5129ee916c24993ba08b7dfbf9a37c22442ea9/simplejson-3.19.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd011fc3c1d88b779645495fdb8189fb318a26981eebcce14109460e062f209b", size = 75546 }, - { url = "https://files.pythonhosted.org/packages/80/7b/45ef1da43f54d209ce2ef59b7356cda13f810186c381f38ae23a4d2b1337/simplejson-3.19.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:637c4d4b81825c1f4d651e56210bd35b5604034b192b02d2d8f17f7ce8c18f42", size = 75602 }, - { url = "https://files.pythonhosted.org/packages/7f/4b/9a132382982f8127bc7ce5212a5585d83c174707c9dd698d0cb6a0d41882/simplejson-3.19.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f56eb03bc9e432bb81adc8ecff2486d39feb371abb442964ffb44f6db23b332", size = 138632 }, - { url = "https://files.pythonhosted.org/packages/76/37/012f5ad2f38afa28f8a6ad9da01dc0b64492ffbaf2a3f2f8a0e1fddf9c1d/simplejson-3.19.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef59a53be400c1fad2c914b8d74c9d42384fed5174f9321dd021b7017fd40270", size = 146740 }, - { url = "https://files.pythonhosted.org/packages/69/b3/89640bd676e26ea2315b5aaf80712a6fbbb4338e4caf872d91448502a19b/simplejson-3.19.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72e8abbc86fcac83629a030888b45fed3a404d54161118be52cb491cd6975d3e", size = 134440 }, - { url = "https://files.pythonhosted.org/packages/61/20/0035a288deaff05397d6cc0145b33f3dd2429b99cdc880de4c5eca41ca72/simplejson-3.19.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8efb03ca77bd7725dfacc9254df00d73e6f43013cf39bd37ef1a8ed0ebb5165", size = 137949 }, - { url = "https://files.pythonhosted.org/packages/5d/de/5b03fafe3003e32d179588953d38183af6c3747e95c7dcc668c4f9eb886a/simplejson-3.19.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:add8850db04b98507a8b62d248a326ecc8561e6d24336d1ca5c605bbfaab4cad", size = 139992 }, - { url = "https://files.pythonhosted.org/packages/d1/ce/e493116ff49fd215f7baa25195b8f684c91e65c153e2a57e04dc3f3a466b/simplejson-3.19.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fc3dc9fb413fc34c396f52f4c87de18d0bd5023804afa8ab5cc224deeb6a9900", size = 140320 }, - { url = "https://files.pythonhosted.org/packages/86/f3/a18b98a7a27548829f672754dd3940fb637a27981399838128d3e560087f/simplejson-3.19.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4dfa420bb9225dd33b6efdabde7c6a671b51150b9b1d9c4e5cd74d3b420b3fe1", size = 148625 }, - { url = "https://files.pythonhosted.org/packages/0f/55/d3da33ee3e708133da079b9d537693d7fef281e6f0d27921cc7e5b3ec523/simplejson-3.19.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7b5c472099b39b274dcde27f1113db8d818c9aa3ba8f78cbb8ad04a4c1ac2118", size = 141287 }, - { url = "https://files.pythonhosted.org/packages/17/e8/56184ab4d66bb64a6ff569f069b3796dfd943f9b961268fe0d403526fc17/simplejson-3.19.3-cp310-cp310-win32.whl", hash = "sha256:817abad79241ed4a507b3caf4d3f2be5079f39d35d4c550a061988986bffd2ec", size = 74143 }, - { url = "https://files.pythonhosted.org/packages/be/8f/a0089eff060f10a925f08b0a0f50854321484f1ac54b1895bbf4c9213dfe/simplejson-3.19.3-cp310-cp310-win_amd64.whl", hash = "sha256:dd5b9b1783e14803e362a558680d88939e830db2466f3fa22df5c9319f8eea94", size = 75643 }, { url = "https://files.pythonhosted.org/packages/8c/bb/9ee3959e6929d228cf669b3f13f0edd43c5261b6cd69598640748b19ca35/simplejson-3.19.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e88abff510dcff903a18d11c2a75f9964e768d99c8d147839913886144b2065e", size = 91930 }, { url = "https://files.pythonhosted.org/packages/ac/ae/a06523928af3a6783e2638cd4f6035c3e32de1c1063d563d9060c8d2f1ad/simplejson-3.19.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:934a50a614fb831614db5dbfba35127ee277624dda4d15895c957d2f5d48610c", size = 74787 }, { url = "https://files.pythonhosted.org/packages/c3/58/fea732e48a7540035fe46d39e6fd77679f5810311d31da8661ce7a18210a/simplejson-3.19.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:212fce86a22188b0c7f53533b0f693ea9605c1a0f02c84c475a30616f55a744d", size = 74612 }, @@ -414,57 +704,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/e1/59cc6a371b60f89e3498d9f4c8109f6b7359094d453f5fe80b2677b777b0/simplejson-3.19.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:256e09d0f94d9c3d177d9e95fd27a68c875a4baa2046633df387b86b652f5747", size = 154344 }, { url = "https://files.pythonhosted.org/packages/79/45/1b36044670016f5cb25ebd92497427d2d1711ecb454d00f71eb9a00b77cc/simplejson-3.19.3-cp313-cp313-win32.whl", hash = "sha256:2c78293470313aefa9cfc5e3f75ca0635721fb016fb1121c1c5b0cb8cc74712a", size = 74002 }, { url = "https://files.pythonhosted.org/packages/e2/58/b06226e6b0612f2b1fa13d5273551da259f894566b1eef32249ddfdcce44/simplejson-3.19.3-cp313-cp313-win_amd64.whl", hash = "sha256:3bbcdc438dc1683b35f7a8dc100960c721f922f9ede8127f63bed7dfded4c64c", size = 75599 }, - { url = "https://files.pythonhosted.org/packages/9a/3d/e7f1caf7fa8c004c30e2c0595a22646a178344a7f53924c11c3d263a8623/simplejson-3.19.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b5587feda2b65a79da985ae6d116daf6428bf7489992badc29fc96d16cd27b05", size = 93646 }, - { url = "https://files.pythonhosted.org/packages/01/40/ff5cae1b4ff35c7822456ad7d098371d697479d418194064b8aff8142d70/simplejson-3.19.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e0d2b00ecbcd1a3c5ea1abc8bb99a26508f758c1759fd01c3be482a3655a176f", size = 75544 }, - { url = "https://files.pythonhosted.org/packages/56/a8/dbe799f3620a08337ff5f3be27df7b5ba5beb1ee06acaf75f3cb46f8d650/simplejson-3.19.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:32a3ada8f3ea41db35e6d37b86dade03760f804628ec22e4fe775b703d567426", size = 75593 }, - { url = "https://files.pythonhosted.org/packages/d5/53/6ed299b9201ea914bb6a178a7e65413ed1969981533f50bfbe8a215be98f/simplejson-3.19.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f455672f4738b0f47183c5896e3606cd65c9ddee3805a4d18e8c96aa3f47c84", size = 138077 }, - { url = "https://files.pythonhosted.org/packages/1c/73/14306559157a6faedb4ecae28ad907b64b5359be5c9ec79233546acb96a4/simplejson-3.19.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b737a5fefedb8333fa50b8db3dcc9b1d18fd6c598f89fa7debff8b46bf4e511", size = 146307 }, - { url = "https://files.pythonhosted.org/packages/5b/1a/7994abb33e53ec972dd5e6dbb337b9070d3ad96017c4cff9d5dc83678ad4/simplejson-3.19.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb47ee773ce67476a960e2db4a0a906680c54f662521550828c0cc57d0099426", size = 133922 }, - { url = "https://files.pythonhosted.org/packages/08/15/8b4e1a8c7729b37797d0eab1381f517f928bd323d17efa7f4414c3565e1f/simplejson-3.19.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eed8cd98a7b24861da9d3d937f5fbfb6657350c547528a117297fe49e3960667", size = 137367 }, - { url = "https://files.pythonhosted.org/packages/59/9a/f5b786fe611395564d3e84f58f668242a7a2e674b4fac71b4e6b21d6d2b7/simplejson-3.19.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:619756f1dd634b5bdf57d9a3914300526c3b348188a765e45b8b08eabef0c94e", size = 139513 }, - { url = "https://files.pythonhosted.org/packages/4d/87/c310daf5e2f10306de3720f075f8ed74cbe83396879b8c55e832393233a5/simplejson-3.19.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:dd7230d061e755d60a4d5445bae854afe33444cdb182f3815cff26ac9fb29a15", size = 139749 }, - { url = "https://files.pythonhosted.org/packages/fd/89/690880e1639b421a919d36fadf1fc364a38c3bc4f208dc11627426cdbe98/simplejson-3.19.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:101a3c8392028cd704a93c7cba8926594e775ca3c91e0bee82144e34190903f1", size = 148103 }, - { url = "https://files.pythonhosted.org/packages/a3/31/ef13eda5b5a0d8d9555b70151ee2956f63b845e1fac4ff904339dfb4dd89/simplejson-3.19.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e557712fc79f251673aeb3fad3501d7d4da3a27eff0857af2e1d1afbbcf6685", size = 140740 }, - { url = "https://files.pythonhosted.org/packages/39/5f/26b0a036592e45a2cb4be2f53d8827257e169bd5c84744a1aac89b0ff56f/simplejson-3.19.3-cp39-cp39-win32.whl", hash = "sha256:0bc5544e3128891bf613b9f71813ee2ec9c11574806f74dd8bb84e5e95bf64a2", size = 74115 }, - { url = "https://files.pythonhosted.org/packages/32/06/a35e2e1d8850aff1cf1320d4887bd5f97921c8964a1e260983d38d5d6c17/simplejson-3.19.3-cp39-cp39-win_amd64.whl", hash = "sha256:06662392e4913dc8846d6a71a6d5de86db5fba244831abe1dd741d62a4136764", size = 75636 }, { url = "https://files.pythonhosted.org/packages/0d/e7/f9fafbd4f39793a20cc52e77bbd766f7384312526d402c382928dc7667f6/simplejson-3.19.3-py3-none-any.whl", hash = "sha256:49cc4c7b940d43bd12bf87ec63f28cbc4964fc4e12c031cc8cd01650f43eb94e", size = 57004 }, ] [[package]] -name = "tomli" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, - { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, - { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, - { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, - { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, - { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, - { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, - { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, - { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, - { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, - { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, - { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, - { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, - { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, - { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, - { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, - { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, - { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, - { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, - { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, - { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "urllib3" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393 }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392 }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019 }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471 }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449 }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054 }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480 }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451 }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057 }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, ] From 7a73a5998a97dd50372bfd8696baaf4bb4dd7051 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Wed, 8 Jan 2025 10:28:36 -0500 Subject: [PATCH 37/88] Lint test_serializer --- tests/test_serializer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 8bea73b0..4647f969 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -1,10 +1,10 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime import numpy as np from choreographer._pipe import Pipe -_timestamp = datetime(1970, 1, 1, tzinfo=timezone.utc) +_timestamp = datetime(1970, 1, 1, tzinfo=UTC) data = [1, 2.00, 3, float("nan"), float("inf"), float("-inf"), _timestamp] expected_message = b'[1, 2.0, 3, null, null, null, "1970-01-01T00:00:00+00:00"]\x00' @@ -16,10 +16,10 @@ def test_de_serialize(): message = pipe.serialize(data) assert message == expected_message obj = pipe.deserialize(message[:-1]) # split out \0 - for o, t in zip(obj, converted_type): + for o, t in zip(obj, converted_type, strict=False): assert isinstance(o, t) message_np = pipe.serialize(np.array(data)) assert message_np == expected_message obj_np = pipe.deserialize(message_np[:-1]) # split out \0 - for o, t in zip(obj_np, converted_type): + for o, t in zip(obj_np, converted_type, strict=False): assert isinstance(o, t) From b50b3d15384f80ab26f506dd65ee9f42b4947763 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Wed, 8 Jan 2025 15:12:38 -0500 Subject: [PATCH 38/88] Format docs --- choreographer/_sys_utils/_tmpfile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/choreographer/_sys_utils/_tmpfile.py b/choreographer/_sys_utils/_tmpfile.py index 78d46420..4184bf9e 100644 --- a/choreographer/_sys_utils/_tmpfile.py +++ b/choreographer/_sys_utils/_tmpfile.py @@ -20,9 +20,9 @@ class TmpDirWarning(UserWarning): class TmpDirectory: """ - The python stdlib TemporaryDirectory wrapper for easier use. + The python stdlib `TemporaryDirectory` wrapper for easier use. - Python's TemporaryDirectory suffered a couple API changes that mean + Python's `TemporaryDirectory` suffered a couple API changes that mean you can't call it the same way for similar versions. This wrapper is also much more aggressive about deleting the directory when it's done, not necessarily relying on OS functions. From d324401a3c949146a844cffd8cf883553237db44 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Thu, 9 Jan 2025 15:53:13 -0500 Subject: [PATCH 39/88] Format for docs --- choreographer/__init__.py | 24 ++++++++++++------------ mkdocs.yml | 13 +++++++------ uv.lock | 4 +++- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/choreographer/__init__.py b/choreographer/__init__.py index 5a1cda38..75ec56d0 100644 --- a/choreographer/__init__.py +++ b/choreographer/__init__.py @@ -8,20 +8,20 @@ See the main README for a quickstart. """ -from ._browsers import ( # noqa: F401 unused import +from ._browsers import ( BrowserClosedError, BrowserFailedError, Chromium, ) -from ._channels import BlockWarning, ChannelClosedError # noqa: F401 unused import -from ._cli_utils import get_chrome, get_chrome_sync # noqa: F401 unused import -from ._sys_utils import ( # noqa: F401 unused import +from ._channels import BlockWarning, ChannelClosedError +from ._cli_utils import get_chrome, get_chrome_sync +from ._sys_utils import ( TmpDirectory, TmpDirWarning, browser_which, get_browser_path, ) -from .browser_sync import ( # noqa: F401 unused import +from .browser_sync import ( BrowserSync, SessionSync, TabSync, @@ -29,19 +29,19 @@ ) __all__ = [ + "BlockWarning", + "BrowserClosedError", + "BrowserFailedError", "BrowserSync", + "ChannelClosedError", + "Chromium", "SessionSync", "TabSync", "TargetSync", - "Chromium", - "BrowserClosedError", - "BrowserFailedError", - "ChannelClosedError", - "BlockWarning", "TmpDirWarning", - "get_chrome", - "get_chrome_sync", "TmpDirectory", "browser_which", "get_browser_path", + "get_chrome", + "get_chrome_sync", ] diff --git a/mkdocs.yml b/mkdocs.yml index 59b3cd42..05d9e555 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -15,12 +15,13 @@ nav: "src": "../README.md", "replace": {"src='docs/": "src='"} } - - Reference: >- - { - "api": "choreographer", - "test": "exports", - "tree": "imports" - } + - Reference: + - >- + { + "api": "choreographer", + "test": ["exports"], + "tree": "imports" + } # CLI tools? theme: diff --git a/uv.lock b/uv.lock index bd3eb1b8..5becc4c1 100644 --- a/uv.lock +++ b/uv.lock @@ -78,7 +78,7 @@ wheels = [ [[package]] name = "choreographer" -version = "1.0.0a0.post91+git.24794414.dirty" +version = "1.0.0a0.post93+git.7a73a599.dirty" source = { editable = "." } dependencies = [ { name = "logistro" }, @@ -356,6 +356,7 @@ source = { editable = "../mkquixote" } dependencies = [ { name = "griffe" }, { name = "jinja2" }, + { name = "logistro" }, { name = "mkdocs" }, { name = "mkdocs-material" }, ] @@ -364,6 +365,7 @@ dependencies = [ requires-dist = [ { name = "griffe", specifier = ">=1.5.1" }, { name = "jinja2", specifier = ">=3.1.4" }, + { name = "logistro", specifier = ">=1.0.2" }, { name = "mkdocs", specifier = ">=1.6.1" }, { name = "mkdocs-material", specifier = ">=9.5.49" }, ] From 65afc139684f6a3573e924257f3dfab059a07672 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Fri, 10 Jan 2025 12:47:40 -0500 Subject: [PATCH 40/88] Fix up namespaces (plain renames) --- choreographer/__init__.py | 36 ++---- choreographer/_brokers/_sync.py | 5 +- choreographer/{ => _brokers}/protocol.py | 0 choreographer/browser_sync.py | 119 +----------------- .../{_browsers => browsers}/README.txt | 0 .../{_browsers => browsers}/__init__.py | 2 + .../_chrome_constants.py | 0 .../{_browsers => browsers}/_errors.py | 0 .../_unix_pipe_chromium_wrapper.py | 0 .../{_browsers => browsers}/chromium.py | 5 +- .../{_channels => channels}/README.txt | 0 .../{_channels => channels}/__init__.py | 2 + .../{_channels => channels}/_errors.py | 0 .../{_channels => channels}/_wire.py | 0 choreographer/{_channels => channels}/pipe.py | 23 ++++ choreographer/{_cli_utils => cli}/README.txt | 0 choreographer/{_cli_utils => cli}/__init__.py | 6 +- .../{_cli_utils => cli}/_cli_utils.py | 0 .../{_cli_utils => cli}/_cli_utils_no_qa.py | 5 +- choreographer/errors.py | 26 ++++ choreographer/protocol/__init__.py | 5 + choreographer/protocol/sync.py | 115 +++++++++++++++++ .../{_sys_utils => utils}/README.txt | 0 .../{_sys_utils => utils}/__init__.py | 7 +- choreographer/{_sys_utils => utils}/_kill.py | 0 .../{_sys_utils => utils}/_tmpfile.py | 0 choreographer/{_sys_utils => utils}/_which.py | 2 +- pyproject.toml | 4 +- uv.lock | 2 +- 29 files changed, 209 insertions(+), 155 deletions(-) rename choreographer/{ => _brokers}/protocol.py (100%) rename choreographer/{_browsers => browsers}/README.txt (100%) rename choreographer/{_browsers => browsers}/__init__.py (67%) rename choreographer/{_browsers => browsers}/_chrome_constants.py (100%) rename choreographer/{_browsers => browsers}/_errors.py (100%) rename choreographer/{_browsers => browsers}/_unix_pipe_chromium_wrapper.py (100%) rename choreographer/{_browsers => browsers}/chromium.py (97%) rename choreographer/{_channels => channels}/README.txt (100%) rename choreographer/{_channels => channels}/__init__.py (70%) rename choreographer/{_channels => channels}/_errors.py (100%) rename choreographer/{_channels => channels}/_wire.py (100%) rename choreographer/{_channels => channels}/pipe.py (89%) rename choreographer/{_cli_utils => cli}/README.txt (100%) rename choreographer/{_cli_utils => cli}/__init__.py (69%) rename choreographer/{_cli_utils => cli}/_cli_utils.py (100%) rename choreographer/{_cli_utils => cli}/_cli_utils_no_qa.py (95%) create mode 100644 choreographer/errors.py create mode 100644 choreographer/protocol/__init__.py create mode 100644 choreographer/protocol/sync.py rename choreographer/{_sys_utils => utils}/README.txt (100%) rename choreographer/{_sys_utils => utils}/__init__.py (53%) rename choreographer/{_sys_utils => utils}/_kill.py (100%) rename choreographer/{_sys_utils => utils}/_tmpfile.py (100%) rename choreographer/{_sys_utils => utils}/_which.py (97%) diff --git a/choreographer/__init__.py b/choreographer/__init__.py index 75ec56d0..eac2ecff 100644 --- a/choreographer/__init__.py +++ b/choreographer/__init__.py @@ -8,40 +8,26 @@ See the main README for a quickstart. """ -from ._browsers import ( - BrowserClosedError, - BrowserFailedError, - Chromium, -) -from ._channels import BlockWarning, ChannelClosedError -from ._cli_utils import get_chrome, get_chrome_sync -from ._sys_utils import ( - TmpDirectory, - TmpDirWarning, - browser_which, - get_browser_path, -) +from . import browsers, channel, cli, errors, protocol, util from .browser_sync import ( BrowserSync, - SessionSync, TabSync, - TargetSync, +) +from .cli import get_chrome, get_chrome_sync +from .utils import ( + get_browser_path, ) __all__ = [ - "BlockWarning", - "BrowserClosedError", - "BrowserFailedError", "BrowserSync", - "ChannelClosedError", - "Chromium", - "SessionSync", "TabSync", - "TargetSync", - "TmpDirWarning", - "TmpDirectory", - "browser_which", + "browsers", + "channel", + "cli", + "errors", "get_browser_path", "get_chrome", "get_chrome_sync", + "protocol", + "util", ] diff --git a/choreographer/_brokers/_sync.py b/choreographer/_brokers/_sync.py index c19b5c19..abc0a8fc 100644 --- a/choreographer/_brokers/_sync.py +++ b/choreographer/_brokers/_sync.py @@ -3,8 +3,9 @@ import logistro -from choreographer import protocol -from choreographer._channels import ChannelClosedError +from choreographer.channels import ChannelClosedError + +from . import protocol logger = logistro.getLogger(__name__) diff --git a/choreographer/protocol.py b/choreographer/_brokers/protocol.py similarity index 100% rename from choreographer/protocol.py rename to choreographer/_brokers/protocol.py diff --git a/choreographer/browser_sync.py b/choreographer/browser_sync.py index 19e2fdd9..b881c1c0 100644 --- a/choreographer/browser_sync.py +++ b/choreographer/browser_sync.py @@ -1,4 +1,4 @@ -"""Provides the sync api: `BrowserSync`, `TabSync`, `TargetSync` and `SessionSync`.""" +"""Provides the sync api: `BrowserSync`, `TabSync`.""" import os import subprocess @@ -7,123 +7,14 @@ import logistro from ._brokers import BrokerSync -from ._browsers import BrowserClosedError, BrowserFailedError, Chromium -from ._channels import ChannelClosedError, Pipe -from ._sys_utils import kill +from .browsers import BrowserClosedError, BrowserFailedError, Chromium +from .channels import ChannelClosedError, Pipe +from .protocol.sync import SessionSync, TargetSync +from .utils._kill import kill logger = logistro.getLogger(__name__) -class SessionSync: - """A session is a single conversation with a single target.""" - - def __init__(self, browser, session_id): - """ - Construct a session from the browser as an object. - - A session is like an open conversation with a target. - All commands are sent on sessions. - - Args: - browser: a reference to the main browser - session_id: the id given by the browser - - """ - if not isinstance(session_id, str): - raise TypeError("session_id must be a string") - # Resources - self.browser = browser - - # State - self.session_id = session_id - logger.debug(f"New session: {session_id}") - self.message_id = 0 - - def send_command(self, command, params=None): - """ - Send a devtools command on the session. - - https://chromedevtools.github.io/devtools-protocol/ - - Args: - command: devtools command to send - params: the parameters to send - - """ - current_id = self.message_id - self.message_id += 1 - json_command = { - "id": current_id, - "method": command, - } - - if self.session_id: - json_command["sessionId"] = self.session_id - if params: - json_command["params"] = params - logger.debug( - f"Sending {command} with {params} on session {self.session_id}", - ) - return self.browser.broker.send_json(json_command) - - -class TargetSync: - """A target like a browser, tab, or others. It sends commands. It has sessions.""" - - _session_type = SessionSync - """Like generic typing<>. This is the session type associated with TargetSync.""" - - def __init__(self, target_id, browser): - """Create a target after one ahs been created by the browser.""" - if not isinstance(target_id, str): - raise TypeError("target_id must be string") - # Resources - self.browser = browser - - # States - self.sessions = {} - self.target_id = target_id - logger.info(f"Created new target {target_id}.") - - def _add_session(self, session): - if not isinstance(session, self._session_type): - raise TypeError("session must be a session type class") - self.sessions[session.session_id] = session - self.browser.all_sessions[session.session_id] = session - - def _remove_session(self, session_id): - if isinstance(session_id, self._session_type): - session_id = session_id.session_id - _ = self.sessions.pop(session_id, None) - _ = self.browser.all_sessions.pop(session_id, None) - - def _get_first_session(self): - if not self.sessions.values(): - raise RuntimeError( - "Cannot use this method without at least one valid session", - ) - return next(iter(self.sessions.values())) - - def send_command(self, command, params=None): - """ - Send a command to the first session in a target. - - https://chromedevtools.github.io/devtools-protocol/ - - Args: - command: devtools command to send - params: the parameters to send - - """ - if not self.sessions.values(): - raise RuntimeError("Cannot send_command without at least one valid session") - session = self._get_first_session() - logger.debug( - f"Sending {command} with {params} on session {session.session_id}", - ) - return session.send_command(command, params) - - class TabSync(TargetSync): """A wrapper for TargetSync, so user can use TabSync, not TargetSync.""" diff --git a/choreographer/_browsers/README.txt b/choreographer/browsers/README.txt similarity index 100% rename from choreographer/_browsers/README.txt rename to choreographer/browsers/README.txt diff --git a/choreographer/_browsers/__init__.py b/choreographer/browsers/__init__.py similarity index 67% rename from choreographer/_browsers/__init__.py rename to choreographer/browsers/__init__.py index a20568f6..ed403756 100644 --- a/choreographer/_browsers/__init__.py +++ b/choreographer/browsers/__init__.py @@ -1,3 +1,5 @@ +"""browsers contains implementations of browsers that choreographer can open.""" + from ._errors import BrowserClosedError, BrowserFailedError from .chromium import Chromium diff --git a/choreographer/_browsers/_chrome_constants.py b/choreographer/browsers/_chrome_constants.py similarity index 100% rename from choreographer/_browsers/_chrome_constants.py rename to choreographer/browsers/_chrome_constants.py diff --git a/choreographer/_browsers/_errors.py b/choreographer/browsers/_errors.py similarity index 100% rename from choreographer/_browsers/_errors.py rename to choreographer/browsers/_errors.py diff --git a/choreographer/_browsers/_unix_pipe_chromium_wrapper.py b/choreographer/browsers/_unix_pipe_chromium_wrapper.py similarity index 100% rename from choreographer/_browsers/_unix_pipe_chromium_wrapper.py rename to choreographer/browsers/_unix_pipe_chromium_wrapper.py diff --git a/choreographer/_browsers/chromium.py b/choreographer/browsers/chromium.py similarity index 97% rename from choreographer/_browsers/chromium.py rename to choreographer/browsers/chromium.py index c4207882..ca80e883 100644 --- a/choreographer/_browsers/chromium.py +++ b/choreographer/browsers/chromium.py @@ -12,8 +12,8 @@ if platform.system() == "Windows": import msvcrt -from choreographer._channels import Pipe -from choreographer._sys_utils import TmpDirectory, get_browser_path +from choreographer.channels import Pipe +from choreographer.utils import TmpDirectory, get_browser_path from ._chrome_constants import chrome_names, typical_chrome_paths @@ -185,4 +185,5 @@ def clean(self): self.tmp_dir.clean() def __del__(self): + """Delete the temporary file and run clean().""" self.clean() diff --git a/choreographer/_channels/README.txt b/choreographer/channels/README.txt similarity index 100% rename from choreographer/_channels/README.txt rename to choreographer/channels/README.txt diff --git a/choreographer/_channels/__init__.py b/choreographer/channels/__init__.py similarity index 70% rename from choreographer/_channels/__init__.py rename to choreographer/channels/__init__.py index c905250e..f7cebb95 100644 --- a/choreographer/_channels/__init__.py +++ b/choreographer/channels/__init__.py @@ -1,3 +1,5 @@ +"""Channels are classes that choreo and the browser use to communicate.""" + from ._errors import BlockWarning, ChannelClosedError, JSONError from .pipe import Pipe diff --git a/choreographer/_channels/_errors.py b/choreographer/channels/_errors.py similarity index 100% rename from choreographer/_channels/_errors.py rename to choreographer/channels/_errors.py diff --git a/choreographer/_channels/_wire.py b/choreographer/channels/_wire.py similarity index 100% rename from choreographer/_channels/_wire.py rename to choreographer/channels/_wire.py diff --git a/choreographer/_channels/pipe.py b/choreographer/channels/pipe.py similarity index 89% rename from choreographer/_channels/pipe.py rename to choreographer/channels/pipe.py index 7a0a7024..4fcd5b04 100644 --- a/choreographer/_channels/pipe.py +++ b/choreographer/channels/pipe.py @@ -1,3 +1,5 @@ +"""Pipe is a channel based on operating system file pipes.""" + import os import platform import sys @@ -16,7 +18,10 @@ # if we're a pipe we expect these public attributes class Pipe: + """Pipe is the class defining an operating system pipe.""" + def __init__(self): + """Construct a pipe using os functions.""" # This is where pipe listens (from browser) # So pass the write to browser self._read_from_browser, self._write_from_browser = list(os.pipe()) @@ -39,6 +44,13 @@ def __init__(self): self.shutdown_lock = Lock() # should be private def write_json(self, obj): + """ + write_json sends jsons down the pipe. + + Args: + obj: any python object that serializes to json. + + """ if self.shutdown_lock.locked(): raise ChannelClosedError encoded_message = wire.serialize(obj) + b"\0" @@ -49,6 +61,16 @@ def write_json(self, obj): raise ChannelClosedError from e def read_jsons(self, *, blocking=True): # noqa: PLR0912, C901 branches, complexity + """ + read_jsons will read from the pipe and return one or more jsons in a list. + + Args: + blocking: The read option can be set to block or not. + + Returns: + A list of jsons. + + """ if self.shutdown_lock.locked(): raise ChannelClosedError if not _with_block and not blocking: @@ -114,6 +136,7 @@ def _fake_bye(self): pass def close(self): + """Close the pipe.""" if self.shutdown_lock.acquire(blocking=False): if platform.system() == "Windows": self._fake_bye() diff --git a/choreographer/_cli_utils/README.txt b/choreographer/cli/README.txt similarity index 100% rename from choreographer/_cli_utils/README.txt rename to choreographer/cli/README.txt diff --git a/choreographer/_cli_utils/__init__.py b/choreographer/cli/__init__.py similarity index 69% rename from choreographer/_cli_utils/__init__.py rename to choreographer/cli/__init__.py index f5ec7439..315b54b6 100644 --- a/choreographer/_cli_utils/__init__.py +++ b/choreographer/cli/__init__.py @@ -1,10 +1,12 @@ -from ._cli_utils import ( +"""cli provides some tools that are used on the commandline (and to download chrome).""" + +from ._cli_utils_no_qa import diagnose +from .cli import ( get_chrome, get_chrome_cli, get_chrome_download_path, get_chrome_sync, ) -from ._cli_utils_no_qa import diagnose __all__ = [ "diagnose", diff --git a/choreographer/_cli_utils/_cli_utils.py b/choreographer/cli/_cli_utils.py similarity index 100% rename from choreographer/_cli_utils/_cli_utils.py rename to choreographer/cli/_cli_utils.py diff --git a/choreographer/_cli_utils/_cli_utils_no_qa.py b/choreographer/cli/_cli_utils_no_qa.py similarity index 95% rename from choreographer/_cli_utils/_cli_utils_no_qa.py rename to choreographer/cli/_cli_utils_no_qa.py index 9c0ce83b..35fbb4fb 100644 --- a/choreographer/_cli_utils/_cli_utils_no_qa.py +++ b/choreographer/cli/_cli_utils_no_qa.py @@ -28,8 +28,9 @@ def diagnose(): logistro.getLogger().setLevel("DEBUG") # from choreographer import BrowserSync, Browser, browser_which - from choreographer import BrowserSync, browser_which - from choreographer._browsers._chrome_constants import chrome_names + from choreographer import BrowserSync + from choreographer.browsers._chrome_constants import chrome_names + from choreographer.utils._which import browser_which parser = argparse.ArgumentParser(description="tool to help debug problems") parser.add_argument("--no-run", dest="run", action="store_false") diff --git a/choreographer/errors.py b/choreographer/errors.py new file mode 100644 index 00000000..5690ce34 --- /dev/null +++ b/choreographer/errors.py @@ -0,0 +1,26 @@ +"""The errors available in choreographer.""" + +from ._brokers.protocol import ( + DevtoolsProtocolError, + ExperimentalFeatureWarning, + MessageTypeError, + MissingKeyError, +) +from ._tmpfile import TmpDirWarning +from .browsers import ( + BrowserClosedError, + BrowserFailedError, +) +from .channels import BlockWarning, ChannelClosedError + +__all__ = [ + "BlockWarning", + "BrowserClosedError", + "BrowserFailedError", + "ChannelClosedError", + "DevtoolsProtocolError", + "ExperimentalFeatureWarning", + "MessageTypeError", + "MissingKeyError", + "TmpDirWarning", +] diff --git a/choreographer/protocol/__init__.py b/choreographer/protocol/__init__.py new file mode 100644 index 00000000..b604f400 --- /dev/null +++ b/choreographer/protocol/__init__.py @@ -0,0 +1,5 @@ +"""Subpackage protocol provides various implementations of Session and Target.""" + +from . import sync + +__all__ = ["sync"] diff --git a/choreographer/protocol/sync.py b/choreographer/protocol/sync.py new file mode 100644 index 00000000..fed92054 --- /dev/null +++ b/choreographer/protocol/sync.py @@ -0,0 +1,115 @@ +"""Provide a lower-level sync interface to the Devtools Protocol.""" + +import logistro + +logger = logistro.getLogger(__name__) + + +class SessionSync: + """A session is a single conversation with a single target.""" + + def __init__(self, browser, session_id): + """ + Construct a session from the browser as an object. + + A session is like an open conversation with a target. + All commands are sent on sessions. + + Args: + browser: a reference to the main browser + session_id: the id given by the browser + + """ + if not isinstance(session_id, str): + raise TypeError("session_id must be a string") + # Resources + self.browser = browser + + # State + self.session_id = session_id + logger.debug(f"New session: {session_id}") + self.message_id = 0 + + def send_command(self, command, params=None): + """ + Send a devtools command on the session. + + https://chromedevtools.github.io/devtools-protocol/ + + Args: + command: devtools command to send + params: the parameters to send + + """ + current_id = self.message_id + self.message_id += 1 + json_command = { + "id": current_id, + "method": command, + } + + if self.session_id: + json_command["sessionId"] = self.session_id + if params: + json_command["params"] = params + logger.debug( + f"Sending {command} with {params} on session {self.session_id}", + ) + return self.browser.broker.send_json(json_command) + + +class TargetSync: + """A target like a browser, tab, or others. It sends commands. It has sessions.""" + + _session_type = SessionSync + """Like generic typing<>. This is the session type associated with TargetSync.""" + + def __init__(self, target_id, browser): + """Create a target after one ahs been created by the browser.""" + if not isinstance(target_id, str): + raise TypeError("target_id must be string") + # Resources + self.browser = browser + + # States + self.sessions = {} + self.target_id = target_id + logger.info(f"Created new target {target_id}.") + + def _add_session(self, session): + if not isinstance(session, self._session_type): + raise TypeError("session must be a session type class") + self.sessions[session.session_id] = session + self.browser.all_sessions[session.session_id] = session + + def _remove_session(self, session_id): + if isinstance(session_id, self._session_type): + session_id = session_id.session_id + _ = self.sessions.pop(session_id, None) + _ = self.browser.all_sessions.pop(session_id, None) + + def _get_first_session(self): + if not self.sessions.values(): + raise RuntimeError( + "Cannot use this method without at least one valid session", + ) + return next(iter(self.sessions.values())) + + def send_command(self, command, params=None): + """ + Send a command to the first session in a target. + + https://chromedevtools.github.io/devtools-protocol/ + + Args: + command: devtools command to send + params: the parameters to send + + """ + if not self.sessions.values(): + raise RuntimeError("Cannot send_command without at least one valid session") + session = self._get_first_session() + logger.debug( + f"Sending {command} with {params} on session {session.session_id}", + ) + return session.send_command(command, params) diff --git a/choreographer/_sys_utils/README.txt b/choreographer/utils/README.txt similarity index 100% rename from choreographer/_sys_utils/README.txt rename to choreographer/utils/README.txt diff --git a/choreographer/_sys_utils/__init__.py b/choreographer/utils/__init__.py similarity index 53% rename from choreographer/_sys_utils/__init__.py rename to choreographer/utils/__init__.py index b74c99c5..c1d98693 100644 --- a/choreographer/_sys_utils/__init__.py +++ b/choreographer/utils/__init__.py @@ -1,11 +1,10 @@ -from ._kill import kill +"""Utils contains functions and class that primarily help us with the OS.""" + from ._tmpfile import TmpDirectory, TmpDirWarning -from ._which import browser_which, get_browser_path +from ._which import get_browser_path __all__ = [ "TmpDirWarning", "TmpDirectory", - "browser_which", "get_browser_path", - "kill", ] diff --git a/choreographer/_sys_utils/_kill.py b/choreographer/utils/_kill.py similarity index 100% rename from choreographer/_sys_utils/_kill.py rename to choreographer/utils/_kill.py diff --git a/choreographer/_sys_utils/_tmpfile.py b/choreographer/utils/_tmpfile.py similarity index 100% rename from choreographer/_sys_utils/_tmpfile.py rename to choreographer/utils/_tmpfile.py diff --git a/choreographer/_sys_utils/_which.py b/choreographer/utils/_which.py similarity index 97% rename from choreographer/_sys_utils/_which.py rename to choreographer/utils/_which.py index 827bf593..e83311ef 100644 --- a/choreographer/_sys_utils/_which.py +++ b/choreographer/utils/_which.py @@ -2,7 +2,7 @@ import platform import shutil -from choreographer._cli_utils import get_chrome_download_path +from choreographer.cli import get_chrome_download_path def _is_exe(path): diff --git a/pyproject.toml b/pyproject.toml index 85ea879a..f2b1b36d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,8 +55,8 @@ docs = [ mkquixote = { path = "../mkquixote", editable = true } [project.scripts] -choreo_diagnose = "choreographer._cli_utils:diagnose" -choreo_get_chrome = "choreographer._cli_utils:get_chrome_cli" +choreo_diagnose = "choreographer.cli:diagnose" +choreo_get_chrome = "choreographer.cli:get_chrome_cli" [tool.ruff.lint] select = ["ALL"] diff --git a/uv.lock b/uv.lock index 5becc4c1..9184db65 100644 --- a/uv.lock +++ b/uv.lock @@ -78,7 +78,7 @@ wheels = [ [[package]] name = "choreographer" -version = "1.0.0a0.post93+git.7a73a599.dirty" +version = "1.0.0a0.post95+git.d324401a.dirty" source = { editable = "." } dependencies = [ { name = "logistro" }, From d4efa567896d02c36c922b184d7e5e632199c570 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Fri, 10 Jan 2025 12:56:19 -0500 Subject: [PATCH 41/88] Fix bad naming --- choreographer/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/choreographer/__init__.py b/choreographer/__init__.py index eac2ecff..4b14c448 100644 --- a/choreographer/__init__.py +++ b/choreographer/__init__.py @@ -8,7 +8,7 @@ See the main README for a quickstart. """ -from . import browsers, channel, cli, errors, protocol, util +from . import browsers, channel, cli, errors, protocol, utils from .browser_sync import ( BrowserSync, TabSync, @@ -29,5 +29,5 @@ "get_chrome", "get_chrome_sync", "protocol", - "util", + "utils", ] From 3d890e382b7f5ac033a84acb642cf2b61f1c10be Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Fri, 10 Jan 2025 13:03:31 -0500 Subject: [PATCH 42/88] Fix up naming a bit --- choreographer/__init__.py | 16 +--------------- choreographer/_brokers/_sync.py | 3 +-- choreographer/errors.py | 14 +++++++------- choreographer/{_brokers => }/protocol.py | 3 ++- 4 files changed, 11 insertions(+), 25 deletions(-) rename choreographer/{_brokers => }/protocol.py (98%) diff --git a/choreographer/__init__.py b/choreographer/__init__.py index 4b14c448..25eb9bee 100644 --- a/choreographer/__init__.py +++ b/choreographer/__init__.py @@ -3,31 +3,17 @@ choreographer is natively async, so while there are two main entrypoints: classes `Browser` and `BrowserSync`, the sync version is very limited, functioning -as a building block. +as a building block for more featureful implementations. See the main README for a quickstart. """ -from . import browsers, channel, cli, errors, protocol, utils from .browser_sync import ( BrowserSync, TabSync, ) -from .cli import get_chrome, get_chrome_sync -from .utils import ( - get_browser_path, -) __all__ = [ "BrowserSync", "TabSync", - "browsers", - "channel", - "cli", - "errors", - "get_browser_path", - "get_chrome", - "get_chrome_sync", - "protocol", - "utils", ] diff --git a/choreographer/_brokers/_sync.py b/choreographer/_brokers/_sync.py index abc0a8fc..46080df0 100644 --- a/choreographer/_brokers/_sync.py +++ b/choreographer/_brokers/_sync.py @@ -3,10 +3,9 @@ import logistro +from choreographer import protocol from choreographer.channels import ChannelClosedError -from . import protocol - logger = logistro.getLogger(__name__) diff --git a/choreographer/errors.py b/choreographer/errors.py index 5690ce34..7511c4a2 100644 --- a/choreographer/errors.py +++ b/choreographer/errors.py @@ -1,17 +1,17 @@ -"""The errors available in choreographer.""" +"""A compilation of the errors available in choreographer.""" -from ._brokers.protocol import ( - DevtoolsProtocolError, - ExperimentalFeatureWarning, - MessageTypeError, - MissingKeyError, -) from ._tmpfile import TmpDirWarning from .browsers import ( BrowserClosedError, BrowserFailedError, ) from .channels import BlockWarning, ChannelClosedError +from .protocol import ( + DevtoolsProtocolError, + ExperimentalFeatureWarning, + MessageTypeError, + MissingKeyError, +) __all__ = [ "BlockWarning", diff --git a/choreographer/_brokers/protocol.py b/choreographer/protocol.py similarity index 98% rename from choreographer/_brokers/protocol.py rename to choreographer/protocol.py index 505ab5c3..59e22dc6 100644 --- a/choreographer/_brokers/protocol.py +++ b/choreographer/protocol.py @@ -1,4 +1,4 @@ -"""protocol.py includes helpers and constants for the Chrome Devtools Protocol.""" +"""Includes helpers and constants for the Chrome Devtools Protocol.""" from enum import Enum @@ -7,6 +7,7 @@ class Ecode(Enum): """Ecodes are a list of possible error codes chrome returns.""" TARGET_NOT_FOUND = -32602 + """Self explanatory.""" class DevtoolsProtocolError(Exception): From 4a0738219010508b154f83f1cb4abc46246a6b7d Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Fri, 10 Jan 2025 13:17:57 -0500 Subject: [PATCH 43/88] Rename more --- choreographer/README.txt | 10 -- choreographer/_brokers/README.txt | 13 -- choreographer/browser_sync.py | 18 +-- choreographer/browsers/README.txt | 12 -- choreographer/browsers/__init__.py | 2 +- choreographer/browsers/chromium.py | 28 ++--- choreographer/channels/__init__.py | 7 +- choreographer/channels/pipe.py | 12 +- choreographer/cli/README.txt | 1 - choreographer/protocol.py | 190 ---------------------------- choreographer/protocol/__init__.py | 195 ++++++++++++++++++++++++++++- choreographer/protocol/sync.py | 10 +- 12 files changed, 235 insertions(+), 263 deletions(-) delete mode 100644 choreographer/README.txt delete mode 100644 choreographer/_brokers/README.txt delete mode 100644 choreographer/browsers/README.txt delete mode 100644 choreographer/cli/README.txt delete mode 100644 choreographer/protocol.py diff --git a/choreographer/README.txt b/choreographer/README.txt deleted file mode 100644 index d73abad9..00000000 --- a/choreographer/README.txt +++ /dev/null @@ -1,10 +0,0 @@ -These READMEs (in directories) are development notes, think of them as additions -to the package-level docstrings (the one at the top of each __init__.py). - - -The browsers are the main entrypoint for the user. They: - -1. Manage the actual browser process (start/stop/kill). -2. Integrate the different resources: (see _channels/, _brokers/, _browsers/). -3. Is a list of `Tab`s, `Session`s, and `Target`s. -4. Provides a basic API. diff --git a/choreographer/_brokers/README.txt b/choreographer/_brokers/README.txt deleted file mode 100644 index 9652ab6e..00000000 --- a/choreographer/_brokers/README.txt +++ /dev/null @@ -1,13 +0,0 @@ -Brokers will handle all events and promises, if there are any. - -For example, the _sync.py broker only gives us the option of -starting a thread to dump all events. Writing a broker without -a concurrent structure would be fruitless- it would block and break. - - -The _async.py broker is much more complex, and will allow users to subscribe -to events, will fulfill processes when receiving responses from the browser -addressed to the user's commands, and generally absorbs all browser communication. - -Brokers do not have a standard interface, they are usually 1-1 matched with a particular -`class Browser*` implementation. diff --git a/choreographer/browser_sync.py b/choreographer/browser_sync.py index b881c1c0..14c76c02 100644 --- a/choreographer/browser_sync.py +++ b/choreographer/browser_sync.py @@ -12,11 +12,11 @@ from .protocol.sync import SessionSync, TargetSync from .utils._kill import kill -logger = logistro.getLogger(__name__) +_logger = logistro.getLogger(__name__) class TabSync(TargetSync): - """A wrapper for TargetSync, so user can use TabSync, not TargetSync.""" + """A wrapper for `TargetSync`, so user can use `TabSync`, not `TargetSync`.""" class BrowserSync(TargetSync): @@ -55,7 +55,7 @@ def __init__(self, path=None, *, browser_cls=Chromium, channel_cls=Pipe, **kwarg headless=True/False, enable_gpu=True/False, etc. """ - logger.debug("Attempting to open new browser.") + _logger.debug("Attempting to open new browser.") self._make_lock() self.tabs = {} self.targets = {} @@ -127,21 +127,21 @@ def _close(self): def close(self): """Close the browser.""" self.broker.clean() - logger.info("Broker cleaned up.") + _logger.info("Broker cleaned up.") if not self._release_lock(): return try: - logger.info("Trying to close browser.") + _logger.info("Trying to close browser.") self._close() - logger.info("browser._close() called successfully.") + _logger.info("browser._close() called successfully.") except ProcessLookupError: pass os.close(self.logger_pipe) - logger.info("Logging pipe closed.") + _logger.info("Logging pipe closed.") self.channel.close() - logger.info("Browser channel closed.") + _logger.info("Browser channel closed.") self.browser_impl.clean() - logger.info("Browser implementation cleaned up.") + _logger.info("Browser implementation cleaned up.") def __exit__(self, exc_type, exc_value, exc_traceback): """Close the browser.""" diff --git a/choreographer/browsers/README.txt b/choreographer/browsers/README.txt deleted file mode 100644 index 0a42aa9a..00000000 --- a/choreographer/browsers/README.txt +++ /dev/null @@ -1,12 +0,0 @@ -Browser specific stuff is in here. - -This is not the users `Browser()`, which is abstract, this is -`Chromium()` or in the future `Firefox()`. It's an implementation -of a browser. - -The implementations provide `get_cli()`, `get_popen_args()`, `get_env()` which -`Browser()` uses to actually start the process. Browser will pass them all its -keyword arguments which it assumes are flags for starting browsers (they are -curated, not passed blindly). `Browser()` also gives each implementation a copy -of the channel being used, as the CLI arguments will change based on -information proovided by that. diff --git a/choreographer/browsers/__init__.py b/choreographer/browsers/__init__.py index ed403756..f0537931 100644 --- a/choreographer/browsers/__init__.py +++ b/choreographer/browsers/__init__.py @@ -1,4 +1,4 @@ -"""browsers contains implementations of browsers that choreographer can open.""" +"""Contains implementations of browsers that choreographer can open.""" from ._errors import BrowserClosedError, BrowserFailedError from .chromium import Chromium diff --git a/choreographer/browsers/chromium.py b/choreographer/browsers/chromium.py index ca80e883..41b0becd 100644 --- a/choreographer/browsers/chromium.py +++ b/choreographer/browsers/chromium.py @@ -1,4 +1,4 @@ -"""chromium.py provides a class proving tools for running chromium browsers.""" +"""Provides a class proving tools for running chromium browsers.""" import os import platform @@ -17,11 +17,11 @@ from ._chrome_constants import chrome_names, typical_chrome_paths -chromium_wrapper_path = ( +_chromium_wrapper_path = ( Path(__file__).resolve().parent / "_unix_pipe_chromium_wrapper.py" ) -logger = logistro.getLogger(__name__) +_logger = logistro.getLogger(__name__) def _is_exe(path): @@ -44,7 +44,7 @@ class Chromium: @classmethod def logger_parser(cls, record, _old): """ - Parse (per logistro) and extract data from browser stderr for logging. + Parse (via `logging.Filter.parse()`) data from browser stderr for logging. Args: record: the `logging.LogRecord` object to read/modify @@ -60,7 +60,7 @@ def __init__(self, channel, path=None, **kwargs): Construct a chromium browser implementation. Args: - channel: the choreographer.Channel we'll be using (WebSockets? Pipe?) + channel: the `choreographer.Channel` we'll be using (WebSockets? Pipe?) path: path to the browser kwargs: gpu_enabled (default False): Turn on GPU? Doesn't work in all envs. @@ -87,7 +87,7 @@ def __init__(self, channel, path=None, **kwargs): "ubuntu" in platform.version().lower() and self.enable_sandbox, ) if self.skip_local: - logger.warning("Ubuntu + Sandbox won't work unless chrome from snap") + _logger.warning("Ubuntu + Sandbox won't work unless chrome from snap") if not self.path: self.path = get_browser_path( @@ -105,7 +105,7 @@ def __init__(self, channel, path=None, **kwargs): "Browser not found. You can use get_chrome(), " "please see documentation.", ) - logger.debug(f"Found path: {self.path}") + _logger.debug(f"Found path: {self.path}") self._channel = channel if not isinstance(channel, Pipe): raise NotImplementedError("Websocket style channels not implemented yet.") @@ -114,10 +114,10 @@ def __init__(self, channel, path=None, **kwargs): path=self._tmp_dir_path, sneak="snap" in str(self.path), ) - logger.info(f"Temporary directory at: {self.tmp_dir.name}") + _logger.info(f"Temporary directory at: {self.tmp_dir.name}") def get_popen_args(self): - """Return the args needed to runc chromium with subprocess.Popen().""" + """Return the args needed to runc chromium with `subprocess.Popen()`.""" args = {} # need to check pipe if platform.system() == "Windows": @@ -128,7 +128,7 @@ def get_popen_args(self): if isinstance(self._channel, Pipe): args["stdin"] = self._channel.from_choreo_to_external args["stdout"] = self._channel.from_external_to_choreo - logger.debug(f"Returning args: {args}") + _logger.debug(f"Returning args: {args}") return args def get_cli(self): @@ -136,7 +136,7 @@ def get_cli(self): if platform.system() != "Windows": cli = [ sys.executable, - chromium_wrapper_path, + _chromium_wrapper_path, self.path, ] else: @@ -172,12 +172,12 @@ def get_cli(self): cli += [ f"--remote-debugging-io-pipes={r_handle!s},{w_handle!s}", ] - logger.debug(f"Returning cli: {cli}") + _logger.debug(f"Returning cli: {cli}") return cli def get_env(self): """Return the env needed for chromium.""" - logger.debug("Returning env: same env, no modification.") + _logger.debug("Returning env: same env, no modification.") return os.environ.copy() def clean(self): @@ -185,5 +185,5 @@ def clean(self): self.tmp_dir.clean() def __del__(self): - """Delete the temporary file and run clean().""" + """Delete the temporary file and run `clean()`.""" self.clean() diff --git a/choreographer/channels/__init__.py b/choreographer/channels/__init__.py index f7cebb95..6e0f0bbe 100644 --- a/choreographer/channels/__init__.py +++ b/choreographer/channels/__init__.py @@ -1,4 +1,9 @@ -"""Channels are classes that choreo and the browser use to communicate.""" +""" +Channels are classes that choreo and the browser use to communicate. + +This is a low-level part of the API. + +""" from ._errors import BlockWarning, ChannelClosedError, JSONError from .pipe import Pipe diff --git a/choreographer/channels/pipe.py b/choreographer/channels/pipe.py index 4fcd5b04..e5c425dd 100644 --- a/choreographer/channels/pipe.py +++ b/choreographer/channels/pipe.py @@ -1,4 +1,4 @@ -"""Pipe is a channel based on operating system file pipes.""" +"""Provides a channel based on operating system file pipes.""" import os import platform @@ -13,12 +13,12 @@ _with_block = bool(sys.version_info[:3] >= (3, 12) or platform.system() != "Windows") -logger = logistro.getLogger(__name__) +_logger = logistro.getLogger(__name__) # if we're a pipe we expect these public attributes class Pipe: - """Pipe is the class defining an operating system pipe.""" + """Defines an operating system pipe.""" def __init__(self): """Construct a pipe using os functions.""" @@ -45,7 +45,7 @@ def __init__(self): def write_json(self, obj): """ - write_json sends jsons down the pipe. + Send one json down the pipe. Args: obj: any python object that serializes to json. @@ -62,7 +62,7 @@ def write_json(self, obj): def read_jsons(self, *, blocking=True): # noqa: PLR0912, C901 branches, complexity """ - read_jsons will read from the pipe and return one or more jsons in a list. + Read from the pipe and return one or more jsons in a list. Args: blocking: The read option can be set to block or not. @@ -112,7 +112,7 @@ def read_jsons(self, *, blocking=True): # noqa: PLR0912, C901 branches, complex try: jsons.append(wire.deserialize(raw_message)) except JSONError: - logger.exception("JSONError decoding message.") + _logger.exception("JSONError decoding message.") return jsons def _unblock_fd(self, fd): diff --git a/choreographer/cli/README.txt b/choreographer/cli/README.txt deleted file mode 100644 index b500cbb4..00000000 --- a/choreographer/cli/README.txt +++ /dev/null @@ -1 +0,0 @@ -CLI tools provide various functions that are turned into scripts. diff --git a/choreographer/protocol.py b/choreographer/protocol.py deleted file mode 100644 index 59e22dc6..00000000 --- a/choreographer/protocol.py +++ /dev/null @@ -1,190 +0,0 @@ -"""Includes helpers and constants for the Chrome Devtools Protocol.""" - -from enum import Enum - - -class Ecode(Enum): - """Ecodes are a list of possible error codes chrome returns.""" - - TARGET_NOT_FOUND = -32602 - """Self explanatory.""" - - -class DevtoolsProtocolError(Exception): - """Raise a general error reported by the devtools protocol.""" - - def __init__(self, response): - """ - Construct a new DevtoolsProtocolError. - - Args: - response: the json response that contains the error - - """ - super().__init__(response) - self.code = response["error"]["code"] - self.message = response["error"]["message"] - - -class MessageTypeError(TypeError): - """An error for poorly formatted devtools protocol message.""" - - def __init__(self, key, value, expected_type): - """ - Construct a message about a poorly formed protocol message. - - Args: - key: the key that has the badly typed value - value: the type of the value that is incorrect - expected_type: the type that was expected - - """ - value = type(value) if not isinstance(value, type) else value - super().__init__( - f"Message with key {key} must have type {expected_type}, not {value}.", - ) - - -class MissingKeyError(ValueError): - """An error for poorly formatted devtools protocol message.""" - - def __init__(self, key, obj): - """ - Construct a MissingKeyError specifying which key was missing. - - Args: - key: the missing key - obj: the message without the key - - """ - super().__init__( - f"Message missing required key/s {key}. Message received: {obj}", - ) - - -class ExperimentalFeatureWarning(UserWarning): - """An warning to report that a feature may or may not work.""" - - -def verify_params(obj): - """ - Verify the message obj hast he proper keys and values. - - Args: - obj: the object to check. - - Raises: - MissingKeyError: if a key is missing. - MessageTypeError: if a value type is incorrect. - RuntimeError: if there are strange keys. - - """ - n_keys = 0 - - required_keys = {"id": int, "method": str} - for key, type_key in required_keys.items(): - if key not in obj: - raise MissingKeyError(key, obj) - if not isinstance(obj[key], type_key): - raise MessageTypeError(key, type(obj[key]), type_key) - n_keys += 2 - - if "params" in obj: - n_keys += 1 - if "sessionId" in obj: - n_keys += 1 - - if len(obj.keys()) != n_keys: - raise RuntimeError( - "Message objects must have id and method keys, " - "and may have params and sessionId keys.", - ) - - -def calculate_message_key(response): - """ - Given a response from the browser, calculate the key corresponding to the command. - - Every message is uniquely identified by its sessionId and id (counter). - - Args: - response: the message for which to calculate the key. - - """ - session_id = response.get("sessionId", "") - message_id = response.get("id", None) - if message_id is None: - return None - return (session_id, message_id) - - -def match_message_key(response, key): - """ - Report True if a response matches with a certain key (sessionId, id). - - Args: - response: the object response from the browser - key: the (sessionId, id) key tubple we're looking for - - """ - session_id, message_id = key - if ("session_id" not in response and session_id == "") or ( # is browser session - "session_id" in response and response["session_id"] == session_id # is session - ): - pass - else: - return False - - if "id" in response and str(response["id"]) == str(message_id): - pass - else: - return False - return True - - -def is_event(response): - """Return true if the browser response is an event notification.""" - required_keys = {"method", "params"} - return required_keys <= response.keys() and "id" not in response - - -def get_target_id_from_result(response): - """ - Extract target id from a browser response. - - Args: - response: the browser response to extract the targetId from. - - """ - if "result" in response and "targetId" in response["result"]: - return response["result"]["targetId"] - else: - return None - - -def get_session_id_from_result(response): - """ - Extract session id from a browser response. - - Args: - response: the browser response to extract the sessionId from. - - """ - if "result" in response and "sessionId" in response["result"]: - return response["result"]["sessionId"] - else: - return None - - -def get_error_from_result(response): - """ - Extract error from a browser response. - - Args: - response: the browser response to extract the error from. - - """ - if "error" in response: - return response["error"] - else: - return None diff --git a/choreographer/protocol/__init__.py b/choreographer/protocol/__init__.py index b604f400..9c769063 100644 --- a/choreographer/protocol/__init__.py +++ b/choreographer/protocol/__init__.py @@ -1,5 +1,198 @@ -"""Subpackage protocol provides various implementations of Session and Target.""" +""" +Provides various implementations of Session and Target. + +It includes helpers and constants for the Chrome Devtools Protocol. +""" + +from enum import Enum from . import sync __all__ = ["sync"] + + +class Ecode(Enum): + """Ecodes are a list of possible error codes chrome returns.""" + + TARGET_NOT_FOUND = -32602 + """Self explanatory.""" + + +class DevtoolsProtocolError(Exception): + """Raise a general error reported by the devtools protocol.""" + + def __init__(self, response): + """ + Construct a new DevtoolsProtocolError. + + Args: + response: the json response that contains the error + + """ + super().__init__(response) + self.code = response["error"]["code"] + self.message = response["error"]["message"] + + +class MessageTypeError(TypeError): + """An error for poorly formatted devtools protocol message.""" + + def __init__(self, key, value, expected_type): + """ + Construct a message about a poorly formed protocol message. + + Args: + key: the key that has the badly typed value + value: the type of the value that is incorrect + expected_type: the type that was expected + + """ + value = type(value) if not isinstance(value, type) else value + super().__init__( + f"Message with key {key} must have type {expected_type}, not {value}.", + ) + + +class MissingKeyError(ValueError): + """An error for poorly formatted devtools protocol message.""" + + def __init__(self, key, obj): + """ + Construct a MissingKeyError specifying which key was missing. + + Args: + key: the missing key + obj: the message without the key + + """ + super().__init__( + f"Message missing required key/s {key}. Message received: {obj}", + ) + + +class ExperimentalFeatureWarning(UserWarning): + """An warning to report that a feature may or may not work.""" + + +def verify_params(obj): + """ + Verify the message obj hast he proper keys and values. + + Args: + obj: the object to check. + + Raises: + MissingKeyError: if a key is missing. + MessageTypeError: if a value type is incorrect. + RuntimeError: if there are strange keys. + + """ + n_keys = 0 + + required_keys = {"id": int, "method": str} + for key, type_key in required_keys.items(): + if key not in obj: + raise MissingKeyError(key, obj) + if not isinstance(obj[key], type_key): + raise MessageTypeError(key, type(obj[key]), type_key) + n_keys += 2 + + if "params" in obj: + n_keys += 1 + if "sessionId" in obj: + n_keys += 1 + + if len(obj.keys()) != n_keys: + raise RuntimeError( + "Message objects must have id and method keys, " + "and may have params and sessionId keys.", + ) + + +def calculate_message_key(response): + """ + Given a response from the browser, calculate the key corresponding to the command. + + Every message is uniquely identified by its sessionId and id (counter). + + Args: + response: the message for which to calculate the key. + + """ + session_id = response.get("sessionId", "") + message_id = response.get("id", None) + if message_id is None: + return None + return (session_id, message_id) + + +def match_message_key(response, key): + """ + Report True if a response matches with a certain key (sessionId, id). + + Args: + response: the object response from the browser + key: the (sessionId, id) key tubple we're looking for + + """ + session_id, message_id = key + if ("session_id" not in response and session_id == "") or ( # is browser session + "session_id" in response and response["session_id"] == session_id # is session + ): + pass + else: + return False + + if "id" in response and str(response["id"]) == str(message_id): + pass + else: + return False + return True + + +def is_event(response): + """Return true if the browser response is an event notification.""" + required_keys = {"method", "params"} + return required_keys <= response.keys() and "id" not in response + + +def get_target_id_from_result(response): + """ + Extract target id from a browser response. + + Args: + response: the browser response to extract the targetId from. + + """ + if "result" in response and "targetId" in response["result"]: + return response["result"]["targetId"] + else: + return None + + +def get_session_id_from_result(response): + """ + Extract session id from a browser response. + + Args: + response: the browser response to extract the sessionId from. + + """ + if "result" in response and "sessionId" in response["result"]: + return response["result"]["sessionId"] + else: + return None + + +def get_error_from_result(response): + """ + Extract error from a browser response. + + Args: + response: the browser response to extract the error from. + + """ + if "error" in response: + return response["error"] + else: + return None diff --git a/choreographer/protocol/sync.py b/choreographer/protocol/sync.py index fed92054..059faab2 100644 --- a/choreographer/protocol/sync.py +++ b/choreographer/protocol/sync.py @@ -2,7 +2,7 @@ import logistro -logger = logistro.getLogger(__name__) +_logger = logistro.getLogger(__name__) class SessionSync: @@ -27,7 +27,7 @@ def __init__(self, browser, session_id): # State self.session_id = session_id - logger.debug(f"New session: {session_id}") + _logger.debug(f"New session: {session_id}") self.message_id = 0 def send_command(self, command, params=None): @@ -52,7 +52,7 @@ def send_command(self, command, params=None): json_command["sessionId"] = self.session_id if params: json_command["params"] = params - logger.debug( + _logger.debug( f"Sending {command} with {params} on session {self.session_id}", ) return self.browser.broker.send_json(json_command) @@ -74,7 +74,7 @@ def __init__(self, target_id, browser): # States self.sessions = {} self.target_id = target_id - logger.info(f"Created new target {target_id}.") + _logger.info(f"Created new target {target_id}.") def _add_session(self, session): if not isinstance(session, self._session_type): @@ -109,7 +109,7 @@ def send_command(self, command, params=None): if not self.sessions.values(): raise RuntimeError("Cannot send_command without at least one valid session") session = self._get_first_session() - logger.debug( + _logger.debug( f"Sending {command} with {params} on session {session.session_id}", ) return session.send_command(command, params) From d55f8b575bba638df677144cbf17f38fb516872e Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Fri, 10 Jan 2025 13:20:22 -0500 Subject: [PATCH 44/88] Keep renaming --- choreographer/utils/__init__.py | 2 +- choreographer/utils/_tmpfile.py | 2 +- choreographer/utils/_which.py | 10 ++++++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/choreographer/utils/__init__.py b/choreographer/utils/__init__.py index c1d98693..448a1e18 100644 --- a/choreographer/utils/__init__.py +++ b/choreographer/utils/__init__.py @@ -1,4 +1,4 @@ -"""Utils contains functions and class that primarily help us with the OS.""" +"""Contains functions and class that primarily help us with the OS.""" from ._tmpfile import TmpDirectory, TmpDirWarning from ._which import get_browser_path diff --git a/choreographer/utils/_tmpfile.py b/choreographer/utils/_tmpfile.py index 4184bf9e..c629ac37 100644 --- a/choreographer/utils/_tmpfile.py +++ b/choreographer/utils/_tmpfile.py @@ -30,7 +30,7 @@ class TmpDirectory: def __init__(self, path=None, *, sneak=False): """ - Construct a wrapped TemporaryDirectory (TmpDirectory). + Construct a wrapped `TemporaryDirectory`. Args: path: manually specify the directory to use diff --git a/choreographer/utils/_which.py b/choreographer/utils/_which.py index e83311ef..ccd1732d 100644 --- a/choreographer/utils/_which.py +++ b/choreographer/utils/_which.py @@ -72,11 +72,17 @@ def browser_which(executable_names, *, skip_local=False): return None -def get_browser_path(*args, **kwargs): +def get_browser_path(*args, **kwargs): # noqa: D417: don't pass args explicitly """ Call `browser_which()` but check for user override first. - Accepts the same arguments as `browser_which`. + It looks for the browser in path. + + Accepts the same arguments as `browser_which': + + Args: + executable_names: the list of names to look for + skip_local: (default False) don't look for a choreo download of anything. """ return os.environ.get("BROWSER_PATH", browser_which(*args, **kwargs)) From d192d42f3a8694d839d89f449ac7cb4a405acb5a Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Fri, 10 Jan 2025 13:56:00 -0500 Subject: [PATCH 45/88] Remove unnecessary tools from CLI --- choreographer/cli/__init__.py | 8 +------- choreographer/utils/_which.py | 2 +- mkdocs.yml | 12 +++++++++--- pyproject.toml | 4 ++-- uv.lock | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/choreographer/cli/__init__.py b/choreographer/cli/__init__.py index 315b54b6..0f9fc682 100644 --- a/choreographer/cli/__init__.py +++ b/choreographer/cli/__init__.py @@ -1,17 +1,11 @@ """cli provides some tools that are used on the commandline (and to download chrome).""" -from ._cli_utils_no_qa import diagnose -from .cli import ( +from ._cli_utils import ( get_chrome, - get_chrome_cli, - get_chrome_download_path, get_chrome_sync, ) __all__ = [ - "diagnose", "get_chrome", - "get_chrome_cli", - "get_chrome_download_path", "get_chrome_sync", ] diff --git a/choreographer/utils/_which.py b/choreographer/utils/_which.py index ccd1732d..dfa6c893 100644 --- a/choreographer/utils/_which.py +++ b/choreographer/utils/_which.py @@ -2,7 +2,7 @@ import platform import shutil -from choreographer.cli import get_chrome_download_path +from choreographer.cli._cli_utils import get_chrome_download_path def _is_exe(path): diff --git a/mkdocs.yml b/mkdocs.yml index 05d9e555..45efdd7b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -15,12 +15,18 @@ nav: "src": "../README.md", "replace": {"src='docs/": "src='"} } - - Reference: + - User API: >- + { + "api": "choreographer", + "test": ["exports", "_prefix_local"], + "tree": "none" + } + - Developer API: - >- { "api": "choreographer", - "test": ["exports"], - "tree": "imports" + "test": ["exports", "_prefix_local"], + "tree": "packages" } # CLI tools? diff --git a/pyproject.toml b/pyproject.toml index f2b1b36d..3159084d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,8 +55,8 @@ docs = [ mkquixote = { path = "../mkquixote", editable = true } [project.scripts] -choreo_diagnose = "choreographer.cli:diagnose" -choreo_get_chrome = "choreographer.cli:get_chrome_cli" +choreo_diagnose = "choreographer.cli._cli_utils_no_qa:diagnose" +choreo_get_chrome = "choreographer.cli._cli_utils:get_chrome_cli" [tool.ruff.lint] select = ["ALL"] diff --git a/uv.lock b/uv.lock index 9184db65..633a732a 100644 --- a/uv.lock +++ b/uv.lock @@ -78,7 +78,7 @@ wheels = [ [[package]] name = "choreographer" -version = "1.0.0a0.post95+git.d324401a.dirty" +version = "1.0.0a0.post101+git.bcac3b92.dirty" source = { editable = "." } dependencies = [ { name = "logistro" }, From c54755e2e36590092a9f41f908c1f71a53ba3f2a Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Fri, 10 Jan 2025 14:03:21 -0500 Subject: [PATCH 46/88] Add first build of documentation --- .pre-commit-config.yaml | 1 + site/404.html | 728 ++ .../api/choreographer.browser_sync/index.html | 1545 ++++ .../index.html | 1282 ++++ site/api/choreographer.browsers/index.html | 1104 +++ .../choreographer.channels.pipe/index.html | 1128 +++ site/api/choreographer.channels/index.html | 1013 +++ site/api/choreographer.cli/index.html | 909 +++ site/api/choreographer.errors/index.html | 1350 ++++ .../choreographer.protocol.sync/index.html | 1310 ++++ site/api/choreographer.protocol/index.html | 1301 ++++ site/api/choreographer.utils/index.html | 1166 +++ site/api/choreographer/index.html | 1224 +++ site/assets/images/favicon.png | Bin 0 -> 1870 bytes .../assets/javascripts/bundle.88dd0f4e.min.js | 15 + .../javascripts/bundle.88dd0f4e.min.js.map | 7 + .../javascripts/lunr/min/lunr.ar.min.js | 1 + .../javascripts/lunr/min/lunr.da.min.js | 18 + .../javascripts/lunr/min/lunr.de.min.js | 18 + .../javascripts/lunr/min/lunr.du.min.js | 18 + .../javascripts/lunr/min/lunr.el.min.js | 1 + .../javascripts/lunr/min/lunr.es.min.js | 18 + .../javascripts/lunr/min/lunr.fi.min.js | 18 + .../javascripts/lunr/min/lunr.fr.min.js | 18 + .../javascripts/lunr/min/lunr.he.min.js | 1 + .../javascripts/lunr/min/lunr.hi.min.js | 1 + .../javascripts/lunr/min/lunr.hu.min.js | 18 + .../javascripts/lunr/min/lunr.hy.min.js | 1 + .../javascripts/lunr/min/lunr.it.min.js | 18 + .../javascripts/lunr/min/lunr.ja.min.js | 1 + .../javascripts/lunr/min/lunr.jp.min.js | 1 + .../javascripts/lunr/min/lunr.kn.min.js | 1 + .../javascripts/lunr/min/lunr.ko.min.js | 1 + .../javascripts/lunr/min/lunr.multi.min.js | 1 + .../javascripts/lunr/min/lunr.nl.min.js | 18 + .../javascripts/lunr/min/lunr.no.min.js | 18 + .../javascripts/lunr/min/lunr.pt.min.js | 18 + .../javascripts/lunr/min/lunr.ro.min.js | 18 + .../javascripts/lunr/min/lunr.ru.min.js | 18 + .../javascripts/lunr/min/lunr.sa.min.js | 1 + .../lunr/min/lunr.stemmer.support.min.js | 1 + .../javascripts/lunr/min/lunr.sv.min.js | 18 + .../javascripts/lunr/min/lunr.ta.min.js | 1 + .../javascripts/lunr/min/lunr.te.min.js | 1 + .../javascripts/lunr/min/lunr.th.min.js | 1 + .../javascripts/lunr/min/lunr.tr.min.js | 18 + .../javascripts/lunr/min/lunr.vi.min.js | 1 + .../javascripts/lunr/min/lunr.zh.min.js | 1 + site/assets/javascripts/lunr/tinyseg.js | 206 + site/assets/javascripts/lunr/wordcut.js | 6708 +++++++++++++++++ .../workers/search.6ce7567c.min.js | 41 + .../workers/search.6ce7567c.min.js.map | 7 + site/assets/stylesheets/main.6f8fc17f.min.css | 1 + .../stylesheets/main.6f8fc17f.min.css.map | 1 + .../stylesheets/palette.06af60db.min.css | 1 + .../stylesheets/palette.06af60db.min.css.map | 1 + site/css/api.css | 110 + site/index.html | 1142 +++ site/sitemap.xml | 55 + site/sitemap.xml.gz | Bin 0 -> 279 bytes 60 files changed, 22643 insertions(+) create mode 100644 site/404.html create mode 100644 site/api/choreographer.browser_sync/index.html create mode 100644 site/api/choreographer.browsers.chromium/index.html create mode 100644 site/api/choreographer.browsers/index.html create mode 100644 site/api/choreographer.channels.pipe/index.html create mode 100644 site/api/choreographer.channels/index.html create mode 100644 site/api/choreographer.cli/index.html create mode 100644 site/api/choreographer.errors/index.html create mode 100644 site/api/choreographer.protocol.sync/index.html create mode 100644 site/api/choreographer.protocol/index.html create mode 100644 site/api/choreographer.utils/index.html create mode 100644 site/api/choreographer/index.html create mode 100644 site/assets/images/favicon.png create mode 100644 site/assets/javascripts/bundle.88dd0f4e.min.js create mode 100644 site/assets/javascripts/bundle.88dd0f4e.min.js.map create mode 100644 site/assets/javascripts/lunr/min/lunr.ar.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.da.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.de.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.du.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.el.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.es.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.fi.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.fr.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.he.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.hi.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.hu.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.hy.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.it.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.ja.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.jp.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.kn.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.ko.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.multi.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.nl.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.no.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.pt.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.ro.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.ru.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.sa.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.sv.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.ta.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.te.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.th.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.tr.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.vi.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.zh.min.js create mode 100644 site/assets/javascripts/lunr/tinyseg.js create mode 100644 site/assets/javascripts/lunr/wordcut.js create mode 100644 site/assets/javascripts/workers/search.6ce7567c.min.js create mode 100644 site/assets/javascripts/workers/search.6ce7567c.min.js.map create mode 100644 site/assets/stylesheets/main.6f8fc17f.min.css create mode 100644 site/assets/stylesheets/main.6f8fc17f.min.css.map create mode 100644 site/assets/stylesheets/palette.06af60db.min.css create mode 100644 site/assets/stylesheets/palette.06af60db.min.css.map create mode 100644 site/css/api.css create mode 100644 site/index.html create mode 100644 site/sitemap.xml create mode 100644 site/sitemap.xml.gz diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b6aaac22..9bf923b4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,6 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks %YAML 1.2 --- +exclude: "site/.*" repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v3.2.0 diff --git a/site/404.html b/site/404.html new file mode 100644 index 00000000..f71b9bd6 --- /dev/null +++ b/site/404.html @@ -0,0 +1,728 @@ + + + + + + + + + + + + + + + + + + + + + choreographer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ +

404 - Not found

+ +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + diff --git a/site/api/choreographer.browser_sync/index.html b/site/api/choreographer.browser_sync/index.html new file mode 100644 index 00000000..de72f583 --- /dev/null +++ b/site/api/choreographer.browser_sync/index.html @@ -0,0 +1,1545 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + browser_sync - choreographer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

+choreographer.browser_sync +

+

module: browser_sync

+
+

Provides the sync api: BrowserSync, TabSync.

+
+

Classes

+
+

class TabSync(choreographer.protocol.sync.TargetSync):

+
+

A wrapper for TargetSync, so user can use TabSync, not TargetSync.

+
+

attributes

+
+
browser - Not documented.
+
+
+
sessions - Not documented.
+
+
+
target_id - Not documented.
+
+

methods

+
+
__init__(self, target_id, browser)
+
+
+

Create a target after one ahs been created by the browser.

+
+
+
+
+
send_command(self, command, params)
+
+
+

Send a command to the first session in a target.

+

https://chromedevtools.github.io/devtools-protocol/

+
+

arguments

+
+
+

command + - devtools command to send

+
+
+

params + - the parameters to send

+
+
+
+
+
+
+

class BrowserSync(choreographer.protocol.sync.TargetSync):

+
+

BrowserSync is the sync implementation of Browser.

+
+

attributes

+
+
browser - Not documented.
+
+
+
sessions - Not documented.
+
+
+
target_id - Not documented.
+
+
+
tabs - Not documented.
+
+
+
targets - Not documented.
+
+
+
all_sessions - Not documented.
+
+
+
channel - Not documented.
+
+
+
broker - Not documented.
+
+
+
browser_impl - Not documented.
+
+

methods

+
+
__init__(self, path, browser_cls, channel_cls, kwargs)
+
+
+

Construct a new browser instance.

+
+

arguments

+
+
+

path + - The path to the browser executable.

+
+
+

browser_cls + - The type of browser (default: Chromium).

+
+
+

channel_cls + - The type of channel to browser (default: Pipe).

+
+
+

kwargs + - The arguments that the browser_cls takes. For example, +headless=True/False, enable_gpu=True/False, etc.

+
+
+
+
+
+
send_command(self, command, params)
+
+
+

Send a command to the first session in a target.

+

https://chromedevtools.github.io/devtools-protocol/

+
+

arguments

+
+
+

command + - devtools command to send

+
+
+

params + - the parameters to send

+
+
+
+
+
+
open(self)
+
+
+

Open the browser.

+
+
+
+
+
close(self)
+
+
+

Close the browser.

+
+
+
+
+
get_tab(self)
+
+
+

Get the first tab if there is one. Useful for default tabs.

+
+
+
+
+
start_output_thread(self, kwargs)
+
+
+

Start a separate thread that dumps all messages received to stdout.

+
+
+
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + diff --git a/site/api/choreographer.browsers.chromium/index.html b/site/api/choreographer.browsers.chromium/index.html new file mode 100644 index 00000000..b777b569 --- /dev/null +++ b/site/api/choreographer.browsers.chromium/index.html @@ -0,0 +1,1282 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + chromium - choreographer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

+choreographer.browsers.chromium +

+

module: chromium

+
+

Provides a class proving tools for running chromium browsers.

+
+

Classes

+
+

class Chromium():

+
+

Chromium represents an implementation of the chromium browser.

+

It also includes chromium-like browsers (chrome, edge, and brave).

+
+

attributes

+
+
path - Not documented.
+
+
+
gpu_enabled - Not documented.
+
+
+
headless - Not documented.
+
+
+
sandbox_enabled - Not documented.
+
+
+
skip_local - Not documented.
+
+
+
tmp_dir - Not documented.
+
+

methods

+
+
__init__(self, channel, path, kwargs)
+
+
+

Construct a chromium browser implementation.

+
+

arguments

+
+
+

channel + - the choreographer.Channel we'll be using (WebSockets? Pipe?)

+
+
+

path + - path to the browser

+
+
+

kwargs + -

+
+
+

gpu_enabled (default False) + - Turn on GPU? Doesn't work in all envs.

+
+
+

headless (default True) + - Actually launch a browser?

+
+
+

sandbox_enabled (default False) + - Enable sandbox- +a persnickety thing depending on environment, OS, user, etc

+
+
+

tmp_dir (default None) + - Manually set the temporary directory

+
+
+

raises

+
+
+

RuntimeError + - Too many kwargs, or browser not found.

+
+
+

NotImplementedError + - Pipe is the only channel type it'll accept right now.

+
+
+
+
+
+
logger_parser(cls, record, _old)
+
+
+

Parse (via logging.Filter.parse()) data from browser stderr for logging.

+
+

arguments

+
+
+

record + - the logging.LogRecord object to read/modify

+
+
+

_old + - data that was already stripped out.

+
+
+
+
+
+
get_popen_args(self)
+
+
+

Return the args needed to runc chromium with subprocess.Popen().

+
+
+
+
+
get_cli(self)
+
+
+

Return the CLI command for chromium.

+
+
+
+
+
get_env(self)
+
+
+

Return the env needed for chromium.

+
+
+
+
+
clean(self)
+
+
+

Clean up any leftovers form browser, like tmp files.

+
+
+
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + diff --git a/site/api/choreographer.browsers/index.html b/site/api/choreographer.browsers/index.html new file mode 100644 index 00000000..bcaad19e --- /dev/null +++ b/site/api/choreographer.browsers/index.html @@ -0,0 +1,1104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Index - choreographer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+ +
+ + + +
+
+ + + + +

+choreographer.browsers +

+

package: browsers

+
+

Contains implementations of browsers that choreographer can open.

+
+

Classes

+
+

class BrowserClosedError(RuntimeError):

+
+

An error for when the browser is closed accidently (during access).

+
+
+
+

class BrowserFailedError(RuntimeError):

+
+

An error for when the browser fails to launch.

+
+
+
+

class Chromium():

+
+

Chromium represents an implementation of the chromium browser.

+

It also includes chromium-like browsers (chrome, edge, and brave).

+
+

attributes

+
+
path - Not documented.
+
+
+
gpu_enabled - Not documented.
+
+
+
headless - Not documented.
+
+
+
sandbox_enabled - Not documented.
+
+
+
skip_local - Not documented.
+
+
+
tmp_dir - Not documented.
+
+

methods

+
+
__init__(self, channel, path, kwargs)
+
+
+

Construct a chromium browser implementation.

+
+

arguments

+
+
+

channel + - the choreographer.Channel we'll be using (WebSockets? Pipe?)

+
+
+

path + - path to the browser

+
+
+

kwargs + -

+
+
+

gpu_enabled (default False) + - Turn on GPU? Doesn't work in all envs.

+
+
+

headless (default True) + - Actually launch a browser?

+
+
+

sandbox_enabled (default False) + - Enable sandbox- +a persnickety thing depending on environment, OS, user, etc

+
+
+

tmp_dir (default None) + - Manually set the temporary directory

+
+
+

raises

+
+
+

RuntimeError + - Too many kwargs, or browser not found.

+
+
+

NotImplementedError + - Pipe is the only channel type it'll accept right now.

+
+
+
+
+
+
logger_parser(cls, record, _old)
+
+
+

Parse (via logging.Filter.parse()) data from browser stderr for logging.

+
+

arguments

+
+
+

record + - the logging.LogRecord object to read/modify

+
+
+

_old + - data that was already stripped out.

+
+
+
+
+
+
get_popen_args(self)
+
+
+

Return the args needed to runc chromium with subprocess.Popen().

+
+
+
+
+
get_cli(self)
+
+
+

Return the CLI command for chromium.

+
+
+
+
+
get_env(self)
+
+
+

Return the env needed for chromium.

+
+
+
+
+
clean(self)
+
+
+

Clean up any leftovers form browser, like tmp files.

+
+
+
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + diff --git a/site/api/choreographer.channels.pipe/index.html b/site/api/choreographer.channels.pipe/index.html new file mode 100644 index 00000000..6d342119 --- /dev/null +++ b/site/api/choreographer.channels.pipe/index.html @@ -0,0 +1,1128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + pipe - choreographer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

+choreographer.channels.pipe +

+

module: pipe

+
+

Provides a channel based on operating system file pipes.

+
+

Classes

+
+

class Pipe():

+
+

Defines an operating system pipe.

+
+

attributes

+
+
from_external_to_choreo - Not documented.
+
+
+
from_choreo_to_external - Not documented.
+
+
+
shutdown_lock - Not documented.
+
+

methods

+
+
__init__(self)
+
+
+

Construct a pipe using os functions.

+
+
+
+
+
write_json(self, obj)
+
+
+

Send one json down the pipe.

+
+

arguments

+
+
+

obj + - any python object that serializes to json.

+
+
+
+
+
+
read_jsons(self, blocking)
+
+
+

Read from the pipe and return one or more jsons in a list.

+
+

arguments

+
+
+

blocking + - The read option can be set to block or not.

+
+
+

returns

+
+

A list of jsons.

+
+
+
+
+
close(self)
+
+
+

Close the pipe.

+
+
+
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + diff --git a/site/api/choreographer.channels/index.html b/site/api/choreographer.channels/index.html new file mode 100644 index 00000000..e9918f1c --- /dev/null +++ b/site/api/choreographer.channels/index.html @@ -0,0 +1,1013 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Index - choreographer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+ +
+
+ + + +
+
+ + + + +

+choreographer.channels +

+

package: channels

+
+

Channels are classes that choreo and the browser use to communicate.

+

This is a low-level part of the API.

+
+

Classes

+
+

class BlockWarning(UserWarning):

+
+

A warning for when block modification operatins used on incompatible OS.

+
+
+
+

class ChannelClosedError(IOError):

+
+

An error to throw when the channel has closed from either end or error.

+
+
+
+

class JSONError(RuntimeError):

+
+

Another JSONError.

+
+
+
+

class Pipe():

+
+

Defines an operating system pipe.

+
+

attributes

+
+
from_external_to_choreo - Not documented.
+
+
+
from_choreo_to_external - Not documented.
+
+
+
shutdown_lock - Not documented.
+
+

methods

+
+
__init__(self)
+
+
+

Construct a pipe using os functions.

+
+
+
+
+
write_json(self, obj)
+
+
+

Send one json down the pipe.

+
+

arguments

+
+
+

obj + - any python object that serializes to json.

+
+
+
+
+
+
read_jsons(self, blocking)
+
+
+

Read from the pipe and return one or more jsons in a list.

+
+

arguments

+
+
+

blocking + - The read option can be set to block or not.

+
+
+

returns

+
+

A list of jsons.

+
+
+
+
+
close(self)
+
+
+

Close the pipe.

+
+
+
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + diff --git a/site/api/choreographer.cli/index.html b/site/api/choreographer.cli/index.html new file mode 100644 index 00000000..64d63609 --- /dev/null +++ b/site/api/choreographer.cli/index.html @@ -0,0 +1,909 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + cli - choreographer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

+choreographer.cli +

+

package: cli

+
+

cli provides some tools that are used on the commandline (and to download chrome).

+
+

Functions

+
get_chrome(arch, i, path)
+
+
+

Download google chrome from google-chrome-for-testing server.

+
+

arguments

+
+
+

arch + - the target platform/os, as understood by google's json directory.

+
+
+

i + - the chrome version: -1 being the latest version, 0 being the oldest +still in the testing directory.

+
+
+

path + - where to download it too (the folder).

+
+
+
+
get_chrome_sync(arch, i, path, verbose)
+
+
+

Download chrome synchronously: see get_chrome().

+
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + diff --git a/site/api/choreographer.errors/index.html b/site/api/choreographer.errors/index.html new file mode 100644 index 00000000..fea6962a --- /dev/null +++ b/site/api/choreographer.errors/index.html @@ -0,0 +1,1350 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + errors - choreographer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+ +
+
+ + + +
+
+ + + + +

+choreographer.errors +

+

module: errors

+
+

A compilation of the errors available in choreographer.

+
+

Classes

+
+

class BrowserClosedError(RuntimeError):

+
+

An error for when the browser is closed accidently (during access).

+
+
+
+

class BrowserFailedError(RuntimeError):

+
+

An error for when the browser fails to launch.

+
+
+
+

class BlockWarning(UserWarning):

+
+

A warning for when block modification operatins used on incompatible OS.

+
+
+
+

class ChannelClosedError(IOError):

+
+

An error to throw when the channel has closed from either end or error.

+
+
+
+

class DevtoolsProtocolError(Exception):

+
+

Raise a general error reported by the devtools protocol.

+
+

attributes

+
+
code - Not documented.
+
+
+
message - Not documented.
+
+

methods

+
+
__init__(self, response)
+
+
+

Construct a new DevtoolsProtocolError.

+
+

arguments

+
+
+

response + - the json response that contains the error

+
+
+
+
+
+
+

class ExperimentalFeatureWarning(UserWarning):

+
+

An warning to report that a feature may or may not work.

+
+
+
+

class MessageTypeError(TypeError):

+
+

An error for poorly formatted devtools protocol message.

+
+

methods

+
+
__init__(self, key, value, expected_type)
+
+
+

Construct a message about a poorly formed protocol message.

+
+

arguments

+
+
+

key + - the key that has the badly typed value

+
+
+

value + - the type of the value that is incorrect

+
+
+

expected_type + - the type that was expected

+
+
+
+
+
+
+

class MissingKeyError(ValueError):

+
+

An error for poorly formatted devtools protocol message.

+
+

methods

+
+
__init__(self, key, obj)
+
+
+

Construct a MissingKeyError specifying which key was missing.

+
+

arguments

+
+
+

key + - the missing key

+
+
+

obj + - the message without the key

+
+
+
+
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + diff --git a/site/api/choreographer.protocol.sync/index.html b/site/api/choreographer.protocol.sync/index.html new file mode 100644 index 00000000..8efa32a3 --- /dev/null +++ b/site/api/choreographer.protocol.sync/index.html @@ -0,0 +1,1310 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + sync - choreographer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

+choreographer.protocol.sync +

+

module: sync

+
+

Provide a lower-level sync interface to the Devtools Protocol.

+
+

Classes

+
+

class SessionSync():

+
+

A session is a single conversation with a single target.

+
+

attributes

+
+
browser - Not documented.
+
+
+
session_id - Not documented.
+
+
+
message_id - Not documented.
+
+

methods

+
+
__init__(self, browser, session_id)
+
+
+

Construct a session from the browser as an object.

+

A session is like an open conversation with a target. +All commands are sent on sessions.

+
+

arguments

+
+
+

browser + - a reference to the main browser

+
+
+

session_id + - the id given by the browser

+
+
+
+
+
+
send_command(self, command, params)
+
+
+

Send a devtools command on the session.

+

https://chromedevtools.github.io/devtools-protocol/

+
+

arguments

+
+
+

command + - devtools command to send

+
+
+

params + - the parameters to send

+
+
+
+
+
+
+

class TargetSync():

+
+

A target like a browser, tab, or others. It sends commands. It has sessions.

+
+

attributes

+
+
browser - Not documented.
+
+
+
sessions - Not documented.
+
+
+
target_id - Not documented.
+
+

methods

+
+
__init__(self, target_id, browser)
+
+
+

Create a target after one ahs been created by the browser.

+
+
+
+
+
send_command(self, command, params)
+
+
+

Send a command to the first session in a target.

+

https://chromedevtools.github.io/devtools-protocol/

+
+

arguments

+
+
+

command + - devtools command to send

+
+
+

params + - the parameters to send

+
+
+
+
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + diff --git a/site/api/choreographer.protocol/index.html b/site/api/choreographer.protocol/index.html new file mode 100644 index 00000000..c91179db --- /dev/null +++ b/site/api/choreographer.protocol/index.html @@ -0,0 +1,1301 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Index - choreographer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+
+ + + + +

+choreographer.protocol +

+

package: protocol

+
+

Provides various implementations of Session and Target.

+

It includes helpers and constants for the Chrome Devtools Protocol.

+
+

Imported Modules

+
sync - Provide a lower-level sync interface to the Devtools Protocol.
+

Functions

+
verify_params(obj)
+
+
+

Verify the message obj hast he proper keys and values.

+
+

arguments

+
+
+

obj + - the object to check.

+
+
+

Raises

+
+
+

MissingKeyError +- if a key is missing.

+
+
+

MessageTypeError +- if a value type is incorrect.

+
+
+

RuntimeError +- if there are strange keys.

+
+
+
+
calculate_message_key(response)
+
+
+

Given a response from the browser, calculate the key corresponding to the command.

+

Every message is uniquely identified by its sessionId and id (counter).

+
+

arguments

+
+
+

response + - the message for which to calculate the key.

+
+
+
+
match_message_key(response, key)
+
+
+

Report True if a response matches with a certain key (sessionId, id).

+
+

arguments

+
+
+

response + - the object response from the browser

+
+
+

key + - the (sessionId, id) key tubple we're looking for

+
+
+
+
is_event(response)
+
+
+

Return true if the browser response is an event notification.

+
+
+
get_target_id_from_result(response)
+
+
+

Extract target id from a browser response.

+
+

arguments

+
+
+

response + - the browser response to extract the targetId from.

+
+
+
+
get_session_id_from_result(response)
+
+
+

Extract session id from a browser response.

+
+

arguments

+
+
+

response + - the browser response to extract the sessionId from.

+
+
+
+
get_error_from_result(response)
+
+
+

Extract error from a browser response.

+
+

arguments

+
+
+

response + - the browser response to extract the error from.

+
+
+
+

Classes

+
+

class Ecode(enum.Enum):

+
+

Ecodes are a list of possible error codes chrome returns.

+
+

attributes

+
+
TARGET_NOT_FOUND - Self explanatory.
+
+
+
+

class DevtoolsProtocolError(Exception):

+
+

Raise a general error reported by the devtools protocol.

+
+

attributes

+
+
code - Not documented.
+
+
+
message - Not documented.
+
+

methods

+
+
__init__(self, response)
+
+
+

Construct a new DevtoolsProtocolError.

+
+

arguments

+
+
+

response + - the json response that contains the error

+
+
+
+
+
+
+

class MessageTypeError(TypeError):

+
+

An error for poorly formatted devtools protocol message.

+
+

methods

+
+
__init__(self, key, value, expected_type)
+
+
+

Construct a message about a poorly formed protocol message.

+
+

arguments

+
+
+

key + - the key that has the badly typed value

+
+
+

value + - the type of the value that is incorrect

+
+
+

expected_type + - the type that was expected

+
+
+
+
+
+
+

class MissingKeyError(ValueError):

+
+

An error for poorly formatted devtools protocol message.

+
+

methods

+
+
__init__(self, key, obj)
+
+
+

Construct a MissingKeyError specifying which key was missing.

+
+

arguments

+
+
+

key + - the missing key

+
+
+

obj + - the message without the key

+
+
+
+
+
+
+

class ExperimentalFeatureWarning(UserWarning):

+
+

An warning to report that a feature may or may not work.

+
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + diff --git a/site/api/choreographer.utils/index.html b/site/api/choreographer.utils/index.html new file mode 100644 index 00000000..94b989b7 --- /dev/null +++ b/site/api/choreographer.utils/index.html @@ -0,0 +1,1166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + utils - choreographer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

+choreographer.utils +

+

package: utils

+
+

Contains functions and class that primarily help us with the OS.

+
+

Functions

+
get_browser_path(args, kwargs)
+
+
+

Call browser_which() but check for user override first.

+

It looks for the browser in path.

+

Accepts the same arguments as `browser_which':

+
+

arguments

+
+
+

executable_names + - the list of names to look for

+
+
+

skip_local + - (default False) don't look for a choreo download of anything.

+
+
+
+

Classes

+
+

class TmpDirectory():

+
+

The python stdlib TemporaryDirectory wrapper for easier use.

+

Python's TemporaryDirectory suffered a couple API changes that mean +you can't call it the same way for similar versions. This wrapper is +also much more aggressive about deleting the directory when it's done, +not necessarily relying on OS functions.

+
+

attributes

+
+
temp_dir - Not documented.
+
+
+
path - Not documented.
+
+
+
exists - Not documented.
+
+

methods

+
+
__init__(self, path, sneak)
+
+
+

Construct a wrapped TemporaryDirectory.

+
+

arguments

+
+
+

path + - manually specify the directory to use

+
+
+

sneak + - (default False) avoid using /tmp +Ubuntu's snap will sandbox /tmp

+
+
+
+
+
+
clean(self)
+
+
+

Try several different ways to eliminate the temporary directory.

+
+
+
+
+
+

class TmpDirWarning(UserWarning):

+
+

A warning if for whatever reason we can't eliminate the tmp dir.

+
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + diff --git a/site/api/choreographer/index.html b/site/api/choreographer/index.html new file mode 100644 index 00000000..0287c540 --- /dev/null +++ b/site/api/choreographer/index.html @@ -0,0 +1,1224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Index - choreographer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

+choreographer +

+

package: choreographer

+
+

choreographer is a browser controller for python.

+

choreographer is natively async, so while there are two main entrypoints: +classes Browser and BrowserSync, the sync version is very limited, functioning +as a building block for more featureful implementations.

+

See the main README for a quickstart.

+
+

Classes

+
+

class BrowserSync(choreographer.protocol.sync.TargetSync):

+
+

BrowserSync is the sync implementation of Browser.

+
+

attributes

+
+
browser - Not documented.
+
+
+
sessions - Not documented.
+
+
+
target_id - Not documented.
+
+
+
tabs - Not documented.
+
+
+
targets - Not documented.
+
+
+
all_sessions - Not documented.
+
+
+
channel - Not documented.
+
+
+
broker - Not documented.
+
+
+
browser_impl - Not documented.
+
+

methods

+
+
__init__(self, path, browser_cls, channel_cls, kwargs)
+
+
+

Construct a new browser instance.

+
+

arguments

+
+
+

path + - The path to the browser executable.

+
+
+

browser_cls + - The type of browser (default: Chromium).

+
+
+

channel_cls + - The type of channel to browser (default: Pipe).

+
+
+

kwargs + - The arguments that the browser_cls takes. For example, +headless=True/False, enable_gpu=True/False, etc.

+
+
+
+
+
+
send_command(self, command, params)
+
+
+

Send a command to the first session in a target.

+

https://chromedevtools.github.io/devtools-protocol/

+
+

arguments

+
+
+

command + - devtools command to send

+
+
+

params + - the parameters to send

+
+
+
+
+
+
open(self)
+
+
+

Open the browser.

+
+
+
+
+
close(self)
+
+
+

Close the browser.

+
+
+
+
+
get_tab(self)
+
+
+

Get the first tab if there is one. Useful for default tabs.

+
+
+
+
+
start_output_thread(self, kwargs)
+
+
+

Start a separate thread that dumps all messages received to stdout.

+
+
+
+
+
+

class TabSync(choreographer.protocol.sync.TargetSync):

+
+

A wrapper for TargetSync, so user can use TabSync, not TargetSync.

+
+

attributes

+
+
browser - Not documented.
+
+
+
sessions - Not documented.
+
+
+
target_id - Not documented.
+
+

methods

+
+
__init__(self, target_id, browser)
+
+
+

Create a target after one ahs been created by the browser.

+
+
+
+
+
send_command(self, command, params)
+
+
+

Send a command to the first session in a target.

+

https://chromedevtools.github.io/devtools-protocol/

+
+

arguments

+
+
+

command + - devtools command to send

+
+
+

params + - the parameters to send

+
+
+
+
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + diff --git a/site/assets/images/favicon.png b/site/assets/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..1cf13b9f9d978896599290a74f77d5dbe7d1655c GIT binary patch literal 1870 zcmV-U2eJ5xP)Gc)JR9QMau)O=X#!i9;T z37kk-upj^(fsR36MHs_+1RCI)NNu9}lD0S{B^g8PN?Ww(5|~L#Ng*g{WsqleV}|#l zz8@ri&cTzw_h33bHI+12+kK6WN$h#n5cD8OQt`5kw6p~9H3()bUQ8OS4Q4HTQ=1Ol z_JAocz`fLbT2^{`8n~UAo=#AUOf=SOq4pYkt;XbC&f#7lb$*7=$na!mWCQ`dBQsO0 zLFBSPj*N?#u5&pf2t4XjEGH|=pPQ8xh7tpx;US5Cx_Ju;!O`ya-yF`)b%TEt5>eP1ZX~}sjjA%FJF?h7cX8=b!DZl<6%Cv z*G0uvvU+vmnpLZ2paivG-(cd*y3$hCIcsZcYOGh{$&)A6*XX&kXZd3G8m)G$Zz-LV z^GF3VAW^Mdv!)4OM8EgqRiz~*Cji;uzl2uC9^=8I84vNp;ltJ|q-*uQwGp2ma6cY7 z;`%`!9UXO@fr&Ebapfs34OmS9^u6$)bJxrucutf>`dKPKT%%*d3XlFVKunp9 zasduxjrjs>f8V=D|J=XNZp;_Zy^WgQ$9WDjgY=z@stwiEBm9u5*|34&1Na8BMjjgf3+SHcr`5~>oz1Y?SW^=K z^bTyO6>Gar#P_W2gEMwq)ot3; zREHn~U&Dp0l6YT0&k-wLwYjb?5zGK`W6S2v+K>AM(95m2C20L|3m~rN8dprPr@t)5lsk9Hu*W z?pS990s;Ez=+Rj{x7p``4>+c0G5^pYnB1^!TL=(?HLHZ+HicG{~4F1d^5Awl_2!1jICM-!9eoLhbbT^;yHcefyTAaqRcY zmuctDopPT!%k+}x%lZRKnzykr2}}XfG_ne?nRQO~?%hkzo;@RN{P6o`&mMUWBYMTe z6i8ChtjX&gXl`nvrU>jah)2iNM%JdjqoaeaU%yVn!^70x-flljp6Q5tK}5}&X8&&G zX3fpb3E(!rH=zVI_9Gjl45w@{(ITqngWFe7@9{mX;tO25Z_8 zQHEpI+FkTU#4xu>RkN>b3Tnc3UpWzPXWm#o55GKF09j^Mh~)K7{QqbO_~(@CVq! zS<8954|P8mXN2MRs86xZ&Q4EfM@JB94b=(YGuk)s&^jiSF=t3*oNK3`rD{H`yQ?d; ztE=laAUoZx5?RC8*WKOj`%LXEkgDd>&^Q4M^z`%u0rg-It=hLCVsq!Z%^6eB-OvOT zFZ28TN&cRmgU}Elrnk43)!>Z1FCPL2K$7}gwzIc48NX}#!A1BpJP?#v5wkNprhV** z?Cpalt1oH&{r!o3eSKc&ap)iz2BTn_VV`4>9M^b3;(YY}4>#ML6{~(4mH+?%07*qo IM6N<$f(jP3KmY&$ literal 0 HcmV?d00001 diff --git a/site/assets/javascripts/bundle.88dd0f4e.min.js b/site/assets/javascripts/bundle.88dd0f4e.min.js new file mode 100644 index 00000000..7f7e0ce6 --- /dev/null +++ b/site/assets/javascripts/bundle.88dd0f4e.min.js @@ -0,0 +1,15 @@ +"use strict";(()=>{var Wi=Object.create;var gr=Object.defineProperty;var Di=Object.getOwnPropertyDescriptor;var Vi=Object.getOwnPropertyNames,Vt=Object.getOwnPropertySymbols,Ni=Object.getPrototypeOf,yr=Object.prototype.hasOwnProperty,ao=Object.prototype.propertyIsEnumerable;var io=(e,t,r)=>t in e?gr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,$=(e,t)=>{for(var r in t||(t={}))yr.call(t,r)&&io(e,r,t[r]);if(Vt)for(var r of Vt(t))ao.call(t,r)&&io(e,r,t[r]);return e};var so=(e,t)=>{var r={};for(var o in e)yr.call(e,o)&&t.indexOf(o)<0&&(r[o]=e[o]);if(e!=null&&Vt)for(var o of Vt(e))t.indexOf(o)<0&&ao.call(e,o)&&(r[o]=e[o]);return r};var xr=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var zi=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of Vi(t))!yr.call(e,n)&&n!==r&&gr(e,n,{get:()=>t[n],enumerable:!(o=Di(t,n))||o.enumerable});return e};var Mt=(e,t,r)=>(r=e!=null?Wi(Ni(e)):{},zi(t||!e||!e.__esModule?gr(r,"default",{value:e,enumerable:!0}):r,e));var co=(e,t,r)=>new Promise((o,n)=>{var i=p=>{try{s(r.next(p))}catch(c){n(c)}},a=p=>{try{s(r.throw(p))}catch(c){n(c)}},s=p=>p.done?o(p.value):Promise.resolve(p.value).then(i,a);s((r=r.apply(e,t)).next())});var lo=xr((Er,po)=>{(function(e,t){typeof Er=="object"&&typeof po!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(Er,function(){"use strict";function e(r){var o=!0,n=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(k){return!!(k&&k!==document&&k.nodeName!=="HTML"&&k.nodeName!=="BODY"&&"classList"in k&&"contains"in k.classList)}function p(k){var ft=k.type,qe=k.tagName;return!!(qe==="INPUT"&&a[ft]&&!k.readOnly||qe==="TEXTAREA"&&!k.readOnly||k.isContentEditable)}function c(k){k.classList.contains("focus-visible")||(k.classList.add("focus-visible"),k.setAttribute("data-focus-visible-added",""))}function l(k){k.hasAttribute("data-focus-visible-added")&&(k.classList.remove("focus-visible"),k.removeAttribute("data-focus-visible-added"))}function f(k){k.metaKey||k.altKey||k.ctrlKey||(s(r.activeElement)&&c(r.activeElement),o=!0)}function u(k){o=!1}function d(k){s(k.target)&&(o||p(k.target))&&c(k.target)}function y(k){s(k.target)&&(k.target.classList.contains("focus-visible")||k.target.hasAttribute("data-focus-visible-added"))&&(n=!0,window.clearTimeout(i),i=window.setTimeout(function(){n=!1},100),l(k.target))}function L(k){document.visibilityState==="hidden"&&(n&&(o=!0),X())}function X(){document.addEventListener("mousemove",J),document.addEventListener("mousedown",J),document.addEventListener("mouseup",J),document.addEventListener("pointermove",J),document.addEventListener("pointerdown",J),document.addEventListener("pointerup",J),document.addEventListener("touchmove",J),document.addEventListener("touchstart",J),document.addEventListener("touchend",J)}function te(){document.removeEventListener("mousemove",J),document.removeEventListener("mousedown",J),document.removeEventListener("mouseup",J),document.removeEventListener("pointermove",J),document.removeEventListener("pointerdown",J),document.removeEventListener("pointerup",J),document.removeEventListener("touchmove",J),document.removeEventListener("touchstart",J),document.removeEventListener("touchend",J)}function J(k){k.target.nodeName&&k.target.nodeName.toLowerCase()==="html"||(o=!1,te())}document.addEventListener("keydown",f,!0),document.addEventListener("mousedown",u,!0),document.addEventListener("pointerdown",u,!0),document.addEventListener("touchstart",u,!0),document.addEventListener("visibilitychange",L,!0),X(),r.addEventListener("focus",d,!0),r.addEventListener("blur",y,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var qr=xr((hy,On)=>{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var $a=/["'&<>]/;On.exports=Pa;function Pa(e){var t=""+e,r=$a.exec(t);if(!r)return t;var o,n="",i=0,a=0;for(i=r.index;i{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof It=="object"&&typeof Yr=="object"?Yr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof It=="object"?It.ClipboardJS=r():t.ClipboardJS=r()})(It,function(){return function(){var e={686:function(o,n,i){"use strict";i.d(n,{default:function(){return Ui}});var a=i(279),s=i.n(a),p=i(370),c=i.n(p),l=i(817),f=i.n(l);function u(V){try{return document.execCommand(V)}catch(A){return!1}}var d=function(A){var M=f()(A);return u("cut"),M},y=d;function L(V){var A=document.documentElement.getAttribute("dir")==="rtl",M=document.createElement("textarea");M.style.fontSize="12pt",M.style.border="0",M.style.padding="0",M.style.margin="0",M.style.position="absolute",M.style[A?"right":"left"]="-9999px";var F=window.pageYOffset||document.documentElement.scrollTop;return M.style.top="".concat(F,"px"),M.setAttribute("readonly",""),M.value=V,M}var X=function(A,M){var F=L(A);M.container.appendChild(F);var D=f()(F);return u("copy"),F.remove(),D},te=function(A){var M=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},F="";return typeof A=="string"?F=X(A,M):A instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(A==null?void 0:A.type)?F=X(A.value,M):(F=f()(A),u("copy")),F},J=te;function k(V){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?k=function(M){return typeof M}:k=function(M){return M&&typeof Symbol=="function"&&M.constructor===Symbol&&M!==Symbol.prototype?"symbol":typeof M},k(V)}var ft=function(){var A=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},M=A.action,F=M===void 0?"copy":M,D=A.container,Y=A.target,$e=A.text;if(F!=="copy"&&F!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(Y!==void 0)if(Y&&k(Y)==="object"&&Y.nodeType===1){if(F==="copy"&&Y.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(F==="cut"&&(Y.hasAttribute("readonly")||Y.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if($e)return J($e,{container:D});if(Y)return F==="cut"?y(Y):J(Y,{container:D})},qe=ft;function Fe(V){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Fe=function(M){return typeof M}:Fe=function(M){return M&&typeof Symbol=="function"&&M.constructor===Symbol&&M!==Symbol.prototype?"symbol":typeof M},Fe(V)}function ki(V,A){if(!(V instanceof A))throw new TypeError("Cannot call a class as a function")}function no(V,A){for(var M=0;M0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof D.action=="function"?D.action:this.defaultAction,this.target=typeof D.target=="function"?D.target:this.defaultTarget,this.text=typeof D.text=="function"?D.text:this.defaultText,this.container=Fe(D.container)==="object"?D.container:document.body}},{key:"listenClick",value:function(D){var Y=this;this.listener=c()(D,"click",function($e){return Y.onClick($e)})}},{key:"onClick",value:function(D){var Y=D.delegateTarget||D.currentTarget,$e=this.action(Y)||"copy",Dt=qe({action:$e,container:this.container,target:this.target(Y),text:this.text(Y)});this.emit(Dt?"success":"error",{action:$e,text:Dt,trigger:Y,clearSelection:function(){Y&&Y.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(D){return vr("action",D)}},{key:"defaultTarget",value:function(D){var Y=vr("target",D);if(Y)return document.querySelector(Y)}},{key:"defaultText",value:function(D){return vr("text",D)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(D){var Y=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return J(D,Y)}},{key:"cut",value:function(D){return y(D)}},{key:"isSupported",value:function(){var D=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],Y=typeof D=="string"?[D]:D,$e=!!document.queryCommandSupported;return Y.forEach(function(Dt){$e=$e&&!!document.queryCommandSupported(Dt)}),$e}}]),M}(s()),Ui=Fi},828:function(o){var n=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,p){for(;s&&s.nodeType!==n;){if(typeof s.matches=="function"&&s.matches(p))return s;s=s.parentNode}}o.exports=a},438:function(o,n,i){var a=i(828);function s(l,f,u,d,y){var L=c.apply(this,arguments);return l.addEventListener(u,L,y),{destroy:function(){l.removeEventListener(u,L,y)}}}function p(l,f,u,d,y){return typeof l.addEventListener=="function"?s.apply(null,arguments):typeof u=="function"?s.bind(null,document).apply(null,arguments):(typeof l=="string"&&(l=document.querySelectorAll(l)),Array.prototype.map.call(l,function(L){return s(L,f,u,d,y)}))}function c(l,f,u,d){return function(y){y.delegateTarget=a(y.target,f),y.delegateTarget&&d.call(l,y)}}o.exports=p},879:function(o,n){n.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},n.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||n.node(i[0]))},n.string=function(i){return typeof i=="string"||i instanceof String},n.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}},370:function(o,n,i){var a=i(879),s=i(438);function p(u,d,y){if(!u&&!d&&!y)throw new Error("Missing required arguments");if(!a.string(d))throw new TypeError("Second argument must be a String");if(!a.fn(y))throw new TypeError("Third argument must be a Function");if(a.node(u))return c(u,d,y);if(a.nodeList(u))return l(u,d,y);if(a.string(u))return f(u,d,y);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(u,d,y){return u.addEventListener(d,y),{destroy:function(){u.removeEventListener(d,y)}}}function l(u,d,y){return Array.prototype.forEach.call(u,function(L){L.addEventListener(d,y)}),{destroy:function(){Array.prototype.forEach.call(u,function(L){L.removeEventListener(d,y)})}}}function f(u,d,y){return s(document.body,u,d,y)}o.exports=p},817:function(o){function n(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var p=window.getSelection(),c=document.createRange();c.selectNodeContents(i),p.removeAllRanges(),p.addRange(c),a=p.toString()}return a}o.exports=n},279:function(o){function n(){}n.prototype={on:function(i,a,s){var p=this.e||(this.e={});return(p[i]||(p[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var p=this;function c(){p.off(i,c),a.apply(s,arguments)}return c._=a,this.on(i,c,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),p=0,c=s.length;for(p;p0&&i[i.length-1])&&(c[0]===6||c[0]===2)){r=0;continue}if(c[0]===3&&(!i||c[1]>i[0]&&c[1]=e.length&&(e=void 0),{value:e&&e[o++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function N(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var o=r.call(e),n,i=[],a;try{for(;(t===void 0||t-- >0)&&!(n=o.next()).done;)i.push(n.value)}catch(s){a={error:s}}finally{try{n&&!n.done&&(r=o.return)&&r.call(o)}finally{if(a)throw a.error}}return i}function q(e,t,r){if(r||arguments.length===2)for(var o=0,n=t.length,i;o1||p(d,L)})},y&&(n[d]=y(n[d])))}function p(d,y){try{c(o[d](y))}catch(L){u(i[0][3],L)}}function c(d){d.value instanceof nt?Promise.resolve(d.value.v).then(l,f):u(i[0][2],d)}function l(d){p("next",d)}function f(d){p("throw",d)}function u(d,y){d(y),i.shift(),i.length&&p(i[0][0],i[0][1])}}function uo(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof he=="function"?he(e):e[Symbol.iterator](),r={},o("next"),o("throw"),o("return"),r[Symbol.asyncIterator]=function(){return this},r);function o(i){r[i]=e[i]&&function(a){return new Promise(function(s,p){a=e[i](a),n(s,p,a.done,a.value)})}}function n(i,a,s,p){Promise.resolve(p).then(function(c){i({value:c,done:s})},a)}}function H(e){return typeof e=="function"}function ut(e){var t=function(o){Error.call(o),o.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var zt=ut(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(o,n){return n+1+") "+o.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function Qe(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Ue=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,o,n,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=he(a),p=s.next();!p.done;p=s.next()){var c=p.value;c.remove(this)}}catch(L){t={error:L}}finally{try{p&&!p.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var l=this.initialTeardown;if(H(l))try{l()}catch(L){i=L instanceof zt?L.errors:[L]}var f=this._finalizers;if(f){this._finalizers=null;try{for(var u=he(f),d=u.next();!d.done;d=u.next()){var y=d.value;try{ho(y)}catch(L){i=i!=null?i:[],L instanceof zt?i=q(q([],N(i)),N(L.errors)):i.push(L)}}}catch(L){o={error:L}}finally{try{d&&!d.done&&(n=u.return)&&n.call(u)}finally{if(o)throw o.error}}}if(i)throw new zt(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)ho(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&Qe(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&Qe(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Tr=Ue.EMPTY;function qt(e){return e instanceof Ue||e&&"closed"in e&&H(e.remove)&&H(e.add)&&H(e.unsubscribe)}function ho(e){H(e)?e():e.unsubscribe()}var Pe={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var dt={setTimeout:function(e,t){for(var r=[],o=2;o0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var o=this,n=this,i=n.hasError,a=n.isStopped,s=n.observers;return i||a?Tr:(this.currentObservers=null,s.push(r),new Ue(function(){o.currentObservers=null,Qe(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var o=this,n=o.hasError,i=o.thrownError,a=o.isStopped;n?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new j;return r.source=this,r},t.create=function(r,o){return new To(r,o)},t}(j);var To=function(e){oe(t,e);function t(r,o){var n=e.call(this)||this;return n.destination=r,n.source=o,n}return t.prototype.next=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.next)===null||n===void 0||n.call(o,r)},t.prototype.error=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.error)===null||n===void 0||n.call(o,r)},t.prototype.complete=function(){var r,o;(o=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||o===void 0||o.call(r)},t.prototype._subscribe=function(r){var o,n;return(n=(o=this.source)===null||o===void 0?void 0:o.subscribe(r))!==null&&n!==void 0?n:Tr},t}(g);var _r=function(e){oe(t,e);function t(r){var o=e.call(this)||this;return o._value=r,o}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var o=e.prototype._subscribe.call(this,r);return!o.closed&&r.next(this._value),o},t.prototype.getValue=function(){var r=this,o=r.hasError,n=r.thrownError,i=r._value;if(o)throw n;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t}(g);var At={now:function(){return(At.delegate||Date).now()},delegate:void 0};var Ct=function(e){oe(t,e);function t(r,o,n){r===void 0&&(r=1/0),o===void 0&&(o=1/0),n===void 0&&(n=At);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=o,i._timestampProvider=n,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=o===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,o),i}return t.prototype.next=function(r){var o=this,n=o.isStopped,i=o._buffer,a=o._infiniteTimeWindow,s=o._timestampProvider,p=o._windowTime;n||(i.push(r),!a&&i.push(s.now()+p)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var o=this._innerSubscribe(r),n=this,i=n._infiniteTimeWindow,a=n._buffer,s=a.slice(),p=0;p0?e.prototype.schedule.call(this,r,o):(this.delay=o,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,o){return o>0||this.closed?e.prototype.execute.call(this,r,o):this._execute(r,o)},t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!=null&&n>0||n==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.flush(this),0)},t}(gt);var Lo=function(e){oe(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t}(yt);var kr=new Lo(Oo);var Mo=function(e){oe(t,e);function t(r,o){var n=e.call(this,r,o)||this;return n.scheduler=r,n.work=o,n}return t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!==null&&n>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.actions.push(this),r._scheduled||(r._scheduled=vt.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,o,n){var i;if(n===void 0&&(n=0),n!=null?n>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,o,n);var a=r.actions;o!=null&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==o&&(vt.cancelAnimationFrame(o),r._scheduled=void 0)},t}(gt);var _o=function(e){oe(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var o=this._scheduled;this._scheduled=void 0;var n=this.actions,i;r=r||n.shift();do if(i=r.execute(r.state,r.delay))break;while((r=n[0])&&r.id===o&&n.shift());if(this._active=!1,i){for(;(r=n[0])&&r.id===o&&n.shift();)r.unsubscribe();throw i}},t}(yt);var me=new _o(Mo);var S=new j(function(e){return e.complete()});function Yt(e){return e&&H(e.schedule)}function Hr(e){return e[e.length-1]}function Xe(e){return H(Hr(e))?e.pop():void 0}function ke(e){return Yt(Hr(e))?e.pop():void 0}function Bt(e,t){return typeof Hr(e)=="number"?e.pop():t}var xt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Gt(e){return H(e==null?void 0:e.then)}function Jt(e){return H(e[bt])}function Xt(e){return Symbol.asyncIterator&&H(e==null?void 0:e[Symbol.asyncIterator])}function Zt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Zi(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var er=Zi();function tr(e){return H(e==null?void 0:e[er])}function rr(e){return fo(this,arguments,function(){var r,o,n,i;return Nt(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.tries.push([1,,9,10]),a.label=2;case 2:return[4,nt(r.read())];case 3:return o=a.sent(),n=o.value,i=o.done,i?[4,nt(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,nt(n)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function or(e){return H(e==null?void 0:e.getReader)}function U(e){if(e instanceof j)return e;if(e!=null){if(Jt(e))return ea(e);if(xt(e))return ta(e);if(Gt(e))return ra(e);if(Xt(e))return Ao(e);if(tr(e))return oa(e);if(or(e))return na(e)}throw Zt(e)}function ea(e){return new j(function(t){var r=e[bt]();if(H(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function ta(e){return new j(function(t){for(var r=0;r=2;return function(o){return o.pipe(e?b(function(n,i){return e(n,i,o)}):le,Te(1),r?De(t):Qo(function(){return new ir}))}}function jr(e){return e<=0?function(){return S}:E(function(t,r){var o=[];t.subscribe(T(r,function(n){o.push(n),e=2,!0))}function pe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new g}:t,o=e.resetOnError,n=o===void 0?!0:o,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,p=s===void 0?!0:s;return function(c){var l,f,u,d=0,y=!1,L=!1,X=function(){f==null||f.unsubscribe(),f=void 0},te=function(){X(),l=u=void 0,y=L=!1},J=function(){var k=l;te(),k==null||k.unsubscribe()};return E(function(k,ft){d++,!L&&!y&&X();var qe=u=u!=null?u:r();ft.add(function(){d--,d===0&&!L&&!y&&(f=Ur(J,p))}),qe.subscribe(ft),!l&&d>0&&(l=new at({next:function(Fe){return qe.next(Fe)},error:function(Fe){L=!0,X(),f=Ur(te,n,Fe),qe.error(Fe)},complete:function(){y=!0,X(),f=Ur(te,a),qe.complete()}}),U(k).subscribe(l))})(c)}}function Ur(e,t){for(var r=[],o=2;oe.next(document)),e}function P(e,t=document){return Array.from(t.querySelectorAll(e))}function R(e,t=document){let r=fe(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function fe(e,t=document){return t.querySelector(e)||void 0}function Ie(){var e,t,r,o;return(o=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?o:void 0}var wa=O(h(document.body,"focusin"),h(document.body,"focusout")).pipe(_e(1),Q(void 0),m(()=>Ie()||document.body),G(1));function et(e){return wa.pipe(m(t=>e.contains(t)),K())}function $t(e,t){return C(()=>O(h(e,"mouseenter").pipe(m(()=>!0)),h(e,"mouseleave").pipe(m(()=>!1))).pipe(t?Ht(r=>Le(+!r*t)):le,Q(e.matches(":hover"))))}function Jo(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Jo(e,r)}function x(e,t,...r){let o=document.createElement(e);if(t)for(let n of Object.keys(t))typeof t[n]!="undefined"&&(typeof t[n]!="boolean"?o.setAttribute(n,t[n]):o.setAttribute(n,""));for(let n of r)Jo(o,n);return o}function sr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function Tt(e){let t=x("script",{src:e});return C(()=>(document.head.appendChild(t),O(h(t,"load"),h(t,"error").pipe(v(()=>$r(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(m(()=>{}),_(()=>document.head.removeChild(t)),Te(1))))}var Xo=new g,Ta=C(()=>typeof ResizeObserver=="undefined"?Tt("https://unpkg.com/resize-observer-polyfill"):I(void 0)).pipe(m(()=>new ResizeObserver(e=>e.forEach(t=>Xo.next(t)))),v(e=>O(Ye,I(e)).pipe(_(()=>e.disconnect()))),G(1));function ce(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ge(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return Ta.pipe(w(r=>r.observe(t)),v(r=>Xo.pipe(b(o=>o.target===t),_(()=>r.unobserve(t)))),m(()=>ce(e)),Q(ce(e)))}function St(e){return{width:e.scrollWidth,height:e.scrollHeight}}function cr(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function Zo(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function Ve(e){return{x:e.offsetLeft,y:e.offsetTop}}function en(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function tn(e){return O(h(window,"load"),h(window,"resize")).pipe(Me(0,me),m(()=>Ve(e)),Q(Ve(e)))}function pr(e){return{x:e.scrollLeft,y:e.scrollTop}}function Ne(e){return O(h(e,"scroll"),h(window,"scroll"),h(window,"resize")).pipe(Me(0,me),m(()=>pr(e)),Q(pr(e)))}var rn=new g,Sa=C(()=>I(new IntersectionObserver(e=>{for(let t of e)rn.next(t)},{threshold:0}))).pipe(v(e=>O(Ye,I(e)).pipe(_(()=>e.disconnect()))),G(1));function tt(e){return Sa.pipe(w(t=>t.observe(e)),v(t=>rn.pipe(b(({target:r})=>r===e),_(()=>t.unobserve(e)),m(({isIntersecting:r})=>r))))}function on(e,t=16){return Ne(e).pipe(m(({y:r})=>{let o=ce(e),n=St(e);return r>=n.height-o.height-t}),K())}var lr={drawer:R("[data-md-toggle=drawer]"),search:R("[data-md-toggle=search]")};function nn(e){return lr[e].checked}function Je(e,t){lr[e].checked!==t&&lr[e].click()}function ze(e){let t=lr[e];return h(t,"change").pipe(m(()=>t.checked),Q(t.checked))}function Oa(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function La(){return O(h(window,"compositionstart").pipe(m(()=>!0)),h(window,"compositionend").pipe(m(()=>!1))).pipe(Q(!1))}function an(){let e=h(window,"keydown").pipe(b(t=>!(t.metaKey||t.ctrlKey)),m(t=>({mode:nn("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),b(({mode:t,type:r})=>{if(t==="global"){let o=Ie();if(typeof o!="undefined")return!Oa(o,r)}return!0}),pe());return La().pipe(v(t=>t?S:e))}function ye(){return new URL(location.href)}function lt(e,t=!1){if(B("navigation.instant")&&!t){let r=x("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function sn(){return new g}function cn(){return location.hash.slice(1)}function on(e){let t=x("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Ma(e){return O(h(window,"hashchange"),e).pipe(m(cn),Q(cn()),b(t=>t.length>0),G(1))}function ln(e){return Ma(e).pipe(m(t=>fe(`[id="${t}"]`)),b(t=>typeof t!="undefined"))}function Pt(e){let t=matchMedia(e);return ar(r=>t.addListener(()=>r(t.matches))).pipe(Q(t.matches))}function mn(){let e=matchMedia("print");return O(h(window,"beforeprint").pipe(m(()=>!0)),h(window,"afterprint").pipe(m(()=>!1))).pipe(Q(e.matches))}function Nr(e,t){return e.pipe(v(r=>r?t():S))}function zr(e,t){return new j(r=>{let o=new XMLHttpRequest;return o.open("GET",`${e}`),o.responseType="blob",o.addEventListener("load",()=>{o.status>=200&&o.status<300?(r.next(o.response),r.complete()):r.error(new Error(o.statusText))}),o.addEventListener("error",()=>{r.error(new Error("Network error"))}),o.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(o.addEventListener("progress",n=>{var i;if(n.lengthComputable)t.progress$.next(n.loaded/n.total*100);else{let a=(i=o.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(n.loaded/+a*100)}}),t.progress$.next(5)),o.send(),()=>o.abort()})}function je(e,t){return zr(e,t).pipe(v(r=>r.text()),m(r=>JSON.parse(r)),G(1))}function fn(e,t){let r=new DOMParser;return zr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/html")),G(1))}function un(e,t){let r=new DOMParser;return zr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/xml")),G(1))}function dn(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function hn(){return O(h(window,"scroll",{passive:!0}),h(window,"resize",{passive:!0})).pipe(m(dn),Q(dn()))}function bn(){return{width:innerWidth,height:innerHeight}}function vn(){return h(window,"resize",{passive:!0}).pipe(m(bn),Q(bn()))}function gn(){return z([hn(),vn()]).pipe(m(([e,t])=>({offset:e,size:t})),G(1))}function mr(e,{viewport$:t,header$:r}){let o=t.pipe(ee("size")),n=z([o,r]).pipe(m(()=>Ve(e)));return z([r,t,n]).pipe(m(([{height:i},{offset:a,size:s},{x:p,y:c}])=>({offset:{x:a.x-p,y:a.y-c+i},size:s})))}function _a(e){return h(e,"message",t=>t.data)}function Aa(e){let t=new g;return t.subscribe(r=>e.postMessage(r)),t}function yn(e,t=new Worker(e)){let r=_a(t),o=Aa(t),n=new g;n.subscribe(o);let i=o.pipe(Z(),ie(!0));return n.pipe(Z(),Re(r.pipe(W(i))),pe())}var Ca=R("#__config"),Ot=JSON.parse(Ca.textContent);Ot.base=`${new URL(Ot.base,ye())}`;function xe(){return Ot}function B(e){return Ot.features.includes(e)}function Ee(e,t){return typeof t!="undefined"?Ot.translations[e].replace("#",t.toString()):Ot.translations[e]}function Se(e,t=document){return R(`[data-md-component=${e}]`,t)}function ae(e,t=document){return P(`[data-md-component=${e}]`,t)}function ka(e){let t=R(".md-typeset > :first-child",e);return h(t,"click",{once:!0}).pipe(m(()=>R(".md-typeset",e)),m(r=>({hash:__md_hash(r.innerHTML)})))}function xn(e){if(!B("announce.dismiss")||!e.childElementCount)return S;if(!e.hidden){let t=R(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return C(()=>{let t=new g;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),ka(e).pipe(w(r=>t.next(r)),_(()=>t.complete()),m(r=>$({ref:e},r)))})}function Ha(e,{target$:t}){return t.pipe(m(r=>({hidden:r!==e})))}function En(e,t){let r=new g;return r.subscribe(({hidden:o})=>{e.hidden=o}),Ha(e,t).pipe(w(o=>r.next(o)),_(()=>r.complete()),m(o=>$({ref:e},o)))}function Rt(e,t){return t==="inline"?x("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"})):x("div",{class:"md-tooltip",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"}))}function wn(...e){return x("div",{class:"md-tooltip2",role:"tooltip"},x("div",{class:"md-tooltip2__inner md-typeset"},e))}function Tn(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return x("aside",{class:"md-annotation",tabIndex:0},Rt(t),x("a",{href:r,class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}else return x("aside",{class:"md-annotation",tabIndex:0},Rt(t),x("span",{class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}function Sn(e){return x("button",{class:"md-clipboard md-icon",title:Ee("clipboard.copy"),"data-clipboard-target":`#${e} > code`})}var Ln=Mt(qr());function Qr(e,t){let r=t&2,o=t&1,n=Object.keys(e.terms).filter(p=>!e.terms[p]).reduce((p,c)=>[...p,x("del",null,(0,Ln.default)(c))," "],[]).slice(0,-1),i=xe(),a=new URL(e.location,i.base);B("search.highlight")&&a.searchParams.set("h",Object.entries(e.terms).filter(([,p])=>p).reduce((p,[c])=>`${p} ${c}`.trim(),""));let{tags:s}=xe();return x("a",{href:`${a}`,class:"md-search-result__link",tabIndex:-1},x("article",{class:"md-search-result__article md-typeset","data-md-score":e.score.toFixed(2)},r>0&&x("div",{class:"md-search-result__icon md-icon"}),r>0&&x("h1",null,e.title),r<=0&&x("h2",null,e.title),o>0&&e.text.length>0&&e.text,e.tags&&x("nav",{class:"md-tags"},e.tags.map(p=>{let c=s?p in s?`md-tag-icon md-tag--${s[p]}`:"md-tag-icon":"";return x("span",{class:`md-tag ${c}`},p)})),o>0&&n.length>0&&x("p",{class:"md-search-result__terms"},Ee("search.result.term.missing"),": ",...n)))}function Mn(e){let t=e[0].score,r=[...e],o=xe(),n=r.findIndex(l=>!`${new URL(l.location,o.base)}`.includes("#")),[i]=r.splice(n,1),a=r.findIndex(l=>l.scoreQr(l,1)),...p.length?[x("details",{class:"md-search-result__more"},x("summary",{tabIndex:-1},x("div",null,p.length>0&&p.length===1?Ee("search.result.more.one"):Ee("search.result.more.other",p.length))),...p.map(l=>Qr(l,1)))]:[]];return x("li",{class:"md-search-result__item"},c)}function _n(e){return x("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>x("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?sr(r):r)))}function Kr(e){let t=`tabbed-control tabbed-control--${e}`;return x("div",{class:t,hidden:!0},x("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function An(e){return x("div",{class:"md-typeset__scrollwrap"},x("div",{class:"md-typeset__table"},e))}function Ra(e){var o;let t=xe(),r=new URL(`../${e.version}/`,t.base);return x("li",{class:"md-version__item"},x("a",{href:`${r}`,class:"md-version__link"},e.title,((o=t.version)==null?void 0:o.alias)&&e.aliases.length>0&&x("span",{class:"md-version__alias"},e.aliases[0])))}function Cn(e,t){var o;let r=xe();return e=e.filter(n=>{var i;return!((i=n.properties)!=null&&i.hidden)}),x("div",{class:"md-version"},x("button",{class:"md-version__current","aria-label":Ee("select.version")},t.title,((o=r.version)==null?void 0:o.alias)&&t.aliases.length>0&&x("span",{class:"md-version__alias"},t.aliases[0])),x("ul",{class:"md-version__list"},e.map(Ra)))}var Ia=0;function ja(e){let t=z([et(e),$t(e)]).pipe(m(([o,n])=>o||n),K()),r=C(()=>Zo(e)).pipe(ne(Ne),pt(1),He(t),m(()=>en(e)));return t.pipe(Ae(o=>o),v(()=>z([t,r])),m(([o,n])=>({active:o,offset:n})),pe())}function Fa(e,t){let{content$:r,viewport$:o}=t,n=`__tooltip2_${Ia++}`;return C(()=>{let i=new g,a=new _r(!1);i.pipe(Z(),ie(!1)).subscribe(a);let s=a.pipe(Ht(c=>Le(+!c*250,kr)),K(),v(c=>c?r:S),w(c=>c.id=n),pe());z([i.pipe(m(({active:c})=>c)),s.pipe(v(c=>$t(c,250)),Q(!1))]).pipe(m(c=>c.some(l=>l))).subscribe(a);let p=a.pipe(b(c=>c),re(s,o),m(([c,l,{size:f}])=>{let u=e.getBoundingClientRect(),d=u.width/2;if(l.role==="tooltip")return{x:d,y:8+u.height};if(u.y>=f.height/2){let{height:y}=ce(l);return{x:d,y:-16-y}}else return{x:d,y:16+u.height}}));return z([s,i,p]).subscribe(([c,{offset:l},f])=>{c.style.setProperty("--md-tooltip-host-x",`${l.x}px`),c.style.setProperty("--md-tooltip-host-y",`${l.y}px`),c.style.setProperty("--md-tooltip-x",`${f.x}px`),c.style.setProperty("--md-tooltip-y",`${f.y}px`),c.classList.toggle("md-tooltip2--top",f.y<0),c.classList.toggle("md-tooltip2--bottom",f.y>=0)}),a.pipe(b(c=>c),re(s,(c,l)=>l),b(c=>c.role==="tooltip")).subscribe(c=>{let l=ce(R(":scope > *",c));c.style.setProperty("--md-tooltip-width",`${l.width}px`),c.style.setProperty("--md-tooltip-tail","0px")}),a.pipe(K(),ve(me),re(s)).subscribe(([c,l])=>{l.classList.toggle("md-tooltip2--active",c)}),z([a.pipe(b(c=>c)),s]).subscribe(([c,l])=>{l.role==="dialog"?(e.setAttribute("aria-controls",n),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",n)}),a.pipe(b(c=>!c)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),ja(e).pipe(w(c=>i.next(c)),_(()=>i.complete()),m(c=>$({ref:e},c)))})}function mt(e,{viewport$:t},r=document.body){return Fa(e,{content$:new j(o=>{let n=e.title,i=wn(n);return o.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",n)}}),viewport$:t})}function Ua(e,t){let r=C(()=>z([tn(e),Ne(t)])).pipe(m(([{x:o,y:n},i])=>{let{width:a,height:s}=ce(e);return{x:o-i.x+a/2,y:n-i.y+s/2}}));return et(e).pipe(v(o=>r.pipe(m(n=>({active:o,offset:n})),Te(+!o||1/0))))}function kn(e,t,{target$:r}){let[o,n]=Array.from(e.children);return C(()=>{let i=new g,a=i.pipe(Z(),ie(!0));return i.subscribe({next({offset:s}){e.style.setProperty("--md-tooltip-x",`${s.x}px`),e.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),tt(e).pipe(W(a)).subscribe(s=>{e.toggleAttribute("data-md-visible",s)}),O(i.pipe(b(({active:s})=>s)),i.pipe(_e(250),b(({active:s})=>!s))).subscribe({next({active:s}){s?e.prepend(o):o.remove()},complete(){e.prepend(o)}}),i.pipe(Me(16,me)).subscribe(({active:s})=>{o.classList.toggle("md-tooltip--active",s)}),i.pipe(pt(125,me),b(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:s})=>s)).subscribe({next(s){s?e.style.setProperty("--md-tooltip-0",`${-s}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),h(n,"click").pipe(W(a),b(s=>!(s.metaKey||s.ctrlKey))).subscribe(s=>{s.stopPropagation(),s.preventDefault()}),h(n,"mousedown").pipe(W(a),re(i)).subscribe(([s,{active:p}])=>{var c;if(s.button!==0||s.metaKey||s.ctrlKey)s.preventDefault();else if(p){s.preventDefault();let l=e.parentElement.closest(".md-annotation");l instanceof HTMLElement?l.focus():(c=Ie())==null||c.blur()}}),r.pipe(W(a),b(s=>s===o),Ge(125)).subscribe(()=>e.focus()),Ua(e,t).pipe(w(s=>i.next(s)),_(()=>i.complete()),m(s=>$({ref:e},s)))})}function Wa(e){return e.tagName==="CODE"?P(".c, .c1, .cm",e):[e]}function Da(e){let t=[];for(let r of Wa(e)){let o=[],n=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=n.nextNode();i;i=n.nextNode())o.push(i);for(let i of o){let a;for(;a=/(\(\d+\))(!)?/.exec(i.textContent);){let[,s,p]=a;if(typeof p=="undefined"){let c=i.splitText(a.index);i=c.splitText(s.length),t.push(c)}else{i.textContent=s,t.push(i);break}}}}return t}function Hn(e,t){t.append(...Array.from(e.childNodes))}function fr(e,t,{target$:r,print$:o}){let n=t.closest("[id]"),i=n==null?void 0:n.id,a=new Map;for(let s of Da(t)){let[,p]=s.textContent.match(/\((\d+)\)/);fe(`:scope > li:nth-child(${p})`,e)&&(a.set(p,Tn(p,i)),s.replaceWith(a.get(p)))}return a.size===0?S:C(()=>{let s=new g,p=s.pipe(Z(),ie(!0)),c=[];for(let[l,f]of a)c.push([R(".md-typeset",f),R(`:scope > li:nth-child(${l})`,e)]);return o.pipe(W(p)).subscribe(l=>{e.hidden=!l,e.classList.toggle("md-annotation-list",l);for(let[f,u]of c)l?Hn(f,u):Hn(u,f)}),O(...[...a].map(([,l])=>kn(l,t,{target$:r}))).pipe(_(()=>s.complete()),pe())})}function $n(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return $n(t)}}function On(e,t){return C(()=>{let r=$n(e);return typeof r!="undefined"?fr(r,e,t):S})}var Rn=Mt(Br());var Va=0;function In(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return In(t)}}function Na(e){return ge(e).pipe(m(({width:t})=>({scrollable:St(e).width>t})),ee("scrollable"))}function jn(e,t){let{matches:r}=matchMedia("(hover)"),o=C(()=>{let n=new g,i=n.pipe(jr(1));n.subscribe(({scrollable:c})=>{c&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let a=[];if(Rn.default.isSupported()&&(e.closest(".copy")||B("content.code.copy")&&!e.closest(".no-copy"))){let c=e.closest("pre");c.id=`__code_${Va++}`;let l=Sn(c.id);c.insertBefore(l,e),B("content.tooltips")&&a.push(mt(l,{viewport$}))}let s=e.closest(".highlight");if(s instanceof HTMLElement){let c=In(s);if(typeof c!="undefined"&&(s.classList.contains("annotate")||B("content.code.annotate"))){let l=fr(c,e,t);a.push(ge(s).pipe(W(i),m(({width:f,height:u})=>f&&u),K(),v(f=>f?l:S)))}}return P(":scope > span[id]",e).length&&e.classList.add("md-code__content"),Na(e).pipe(w(c=>n.next(c)),_(()=>n.complete()),m(c=>$({ref:e},c)),Re(...a))});return B("content.lazy")?tt(e).pipe(b(n=>n),Te(1),v(()=>o)):o}function za(e,{target$:t,print$:r}){let o=!0;return O(t.pipe(m(n=>n.closest("details:not([open])")),b(n=>e===n),m(()=>({action:"open",reveal:!0}))),r.pipe(b(n=>n||!o),w(()=>o=e.open),m(n=>({action:n?"open":"close"}))))}function Fn(e,t){return C(()=>{let r=new g;return r.subscribe(({action:o,reveal:n})=>{e.toggleAttribute("open",o==="open"),n&&e.scrollIntoView()}),za(e,t).pipe(w(o=>r.next(o)),_(()=>r.complete()),m(o=>$({ref:e},o)))})}var Un=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.flowchartTitleText{fill:var(--md-mermaid-label-fg-color)}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel p,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel p{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color);stroke-width:.05rem}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}.classDiagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs #classDiagram-compositionEnd,defs #classDiagram-compositionStart,defs #classDiagram-dependencyEnd,defs #classDiagram-dependencyStart,defs #classDiagram-extensionEnd,defs #classDiagram-extensionStart{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs #classDiagram-aggregationEnd,defs #classDiagram-aggregationStart{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}.statediagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}.entityTitleText{fill:var(--md-mermaid-label-fg-color)}.attributeBoxEven,.attributeBoxOdd{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityBox{fill:var(--md-mermaid-label-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityLabel{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.relationshipLabelBox{fill:var(--md-mermaid-label-bg-color);fill-opacity:1;background-color:var(--md-mermaid-label-bg-color);opacity:1}.relationshipLabel{fill:var(--md-mermaid-label-fg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs #ONE_OR_MORE_END *,defs #ONE_OR_MORE_START *,defs #ONLY_ONE_END *,defs #ONLY_ONE_START *,defs #ZERO_OR_MORE_END *,defs #ZERO_OR_MORE_START *,defs #ZERO_OR_ONE_END *,defs #ZERO_OR_ONE_START *{stroke:var(--md-mermaid-edge-color)!important}defs #ZERO_OR_MORE_END circle,defs #ZERO_OR_MORE_START circle{fill:var(--md-mermaid-label-bg-color)}text:not([class]):last-child{fill:var(--md-mermaid-label-fg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var Gr,Qa=0;function Ka(){return typeof mermaid=="undefined"||mermaid instanceof Element?Tt("https://unpkg.com/mermaid@11/dist/mermaid.min.js"):I(void 0)}function Wn(e){return e.classList.remove("mermaid"),Gr||(Gr=Ka().pipe(w(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Un,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),m(()=>{}),G(1))),Gr.subscribe(()=>co(this,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${Qa++}`,r=x("div",{class:"mermaid"}),o=e.textContent,{svg:n,fn:i}=yield mermaid.render(t,o),a=r.attachShadow({mode:"closed"});a.innerHTML=n,e.replaceWith(r),i==null||i(a)})),Gr.pipe(m(()=>({ref:e})))}var Dn=x("table");function Vn(e){return e.replaceWith(Dn),Dn.replaceWith(An(e)),I({ref:e})}function Ya(e){let t=e.find(r=>r.checked)||e[0];return O(...e.map(r=>h(r,"change").pipe(m(()=>R(`label[for="${r.id}"]`))))).pipe(Q(R(`label[for="${t.id}"]`)),m(r=>({active:r})))}function Nn(e,{viewport$:t,target$:r}){let o=R(".tabbed-labels",e),n=P(":scope > input",e),i=Kr("prev");e.append(i);let a=Kr("next");return e.append(a),C(()=>{let s=new g,p=s.pipe(Z(),ie(!0));z([s,ge(e),tt(e)]).pipe(W(p),Me(1,me)).subscribe({next([{active:c},l]){let f=Ve(c),{width:u}=ce(c);e.style.setProperty("--md-indicator-x",`${f.x}px`),e.style.setProperty("--md-indicator-width",`${u}px`);let d=pr(o);(f.xd.x+l.width)&&o.scrollTo({left:Math.max(0,f.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),z([Ne(o),ge(o)]).pipe(W(p)).subscribe(([c,l])=>{let f=St(o);i.hidden=c.x<16,a.hidden=c.x>f.width-l.width-16}),O(h(i,"click").pipe(m(()=>-1)),h(a,"click").pipe(m(()=>1))).pipe(W(p)).subscribe(c=>{let{width:l}=ce(o);o.scrollBy({left:l*c,behavior:"smooth"})}),r.pipe(W(p),b(c=>n.includes(c))).subscribe(c=>c.click()),o.classList.add("tabbed-labels--linked");for(let c of n){let l=R(`label[for="${c.id}"]`);l.replaceChildren(x("a",{href:`#${l.htmlFor}`,tabIndex:-1},...Array.from(l.childNodes))),h(l.firstElementChild,"click").pipe(W(p),b(f=>!(f.metaKey||f.ctrlKey)),w(f=>{f.preventDefault(),f.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${l.htmlFor}`),l.click()})}return B("content.tabs.link")&&s.pipe(Ce(1),re(t)).subscribe(([{active:c},{offset:l}])=>{let f=c.innerText.trim();if(c.hasAttribute("data-md-switching"))c.removeAttribute("data-md-switching");else{let u=e.offsetTop-l.y;for(let y of P("[data-tabs]"))for(let L of P(":scope > input",y)){let X=R(`label[for="${L.id}"]`);if(X!==c&&X.innerText.trim()===f){X.setAttribute("data-md-switching",""),L.click();break}}window.scrollTo({top:e.offsetTop-u});let d=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([f,...d])])}}),s.pipe(W(p)).subscribe(()=>{for(let c of P("audio, video",e))c.pause()}),Ya(n).pipe(w(c=>s.next(c)),_(()=>s.complete()),m(c=>$({ref:e},c)))}).pipe(Ke(se))}function zn(e,{viewport$:t,target$:r,print$:o}){return O(...P(".annotate:not(.highlight)",e).map(n=>On(n,{target$:r,print$:o})),...P("pre:not(.mermaid) > code",e).map(n=>jn(n,{target$:r,print$:o})),...P("pre.mermaid",e).map(n=>Wn(n)),...P("table:not([class])",e).map(n=>Vn(n)),...P("details",e).map(n=>Fn(n,{target$:r,print$:o})),...P("[data-tabs]",e).map(n=>Nn(n,{viewport$:t,target$:r})),...P("[title]",e).filter(()=>B("content.tooltips")).map(n=>mt(n,{viewport$:t})))}function Ba(e,{alert$:t}){return t.pipe(v(r=>O(I(!0),I(!1).pipe(Ge(2e3))).pipe(m(o=>({message:r,active:o})))))}function qn(e,t){let r=R(".md-typeset",e);return C(()=>{let o=new g;return o.subscribe(({message:n,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=n}),Ba(e,t).pipe(w(n=>o.next(n)),_(()=>o.complete()),m(n=>$({ref:e},n)))})}var Ga=0;function Ja(e,t){document.body.append(e);let{width:r}=ce(e);e.style.setProperty("--md-tooltip-width",`${r}px`),e.remove();let o=cr(t),n=typeof o!="undefined"?Ne(o):I({x:0,y:0}),i=O(et(t),$t(t)).pipe(K());return z([i,n]).pipe(m(([a,s])=>{let{x:p,y:c}=Ve(t),l=ce(t),f=t.closest("table");return f&&t.parentElement&&(p+=f.offsetLeft+t.parentElement.offsetLeft,c+=f.offsetTop+t.parentElement.offsetTop),{active:a,offset:{x:p-s.x+l.width/2-r/2,y:c-s.y+l.height+8}}}))}function Qn(e){let t=e.title;if(!t.length)return S;let r=`__tooltip_${Ga++}`,o=Rt(r,"inline"),n=R(".md-typeset",o);return n.innerHTML=t,C(()=>{let i=new g;return i.subscribe({next({offset:a}){o.style.setProperty("--md-tooltip-x",`${a.x}px`),o.style.setProperty("--md-tooltip-y",`${a.y}px`)},complete(){o.style.removeProperty("--md-tooltip-x"),o.style.removeProperty("--md-tooltip-y")}}),O(i.pipe(b(({active:a})=>a)),i.pipe(_e(250),b(({active:a})=>!a))).subscribe({next({active:a}){a?(e.insertAdjacentElement("afterend",o),e.setAttribute("aria-describedby",r),e.removeAttribute("title")):(o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t))},complete(){o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t)}}),i.pipe(Me(16,me)).subscribe(({active:a})=>{o.classList.toggle("md-tooltip--active",a)}),i.pipe(pt(125,me),b(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:a})=>a)).subscribe({next(a){a?o.style.setProperty("--md-tooltip-0",`${-a}px`):o.style.removeProperty("--md-tooltip-0")},complete(){o.style.removeProperty("--md-tooltip-0")}}),Ja(o,e).pipe(w(a=>i.next(a)),_(()=>i.complete()),m(a=>$({ref:e},a)))}).pipe(Ke(se))}function Xa({viewport$:e}){if(!B("header.autohide"))return I(!1);let t=e.pipe(m(({offset:{y:n}})=>n),Be(2,1),m(([n,i])=>[nMath.abs(i-n.y)>100),m(([,[n]])=>n),K()),o=ze("search");return z([e,o]).pipe(m(([{offset:n},i])=>n.y>400&&!i),K(),v(n=>n?r:I(!1)),Q(!1))}function Kn(e,t){return C(()=>z([ge(e),Xa(t)])).pipe(m(([{height:r},o])=>({height:r,hidden:o})),K((r,o)=>r.height===o.height&&r.hidden===o.hidden),G(1))}function Yn(e,{header$:t,main$:r}){return C(()=>{let o=new g,n=o.pipe(Z(),ie(!0));o.pipe(ee("active"),He(t)).subscribe(([{active:a},{hidden:s}])=>{e.classList.toggle("md-header--shadow",a&&!s),e.hidden=s});let i=ue(P("[title]",e)).pipe(b(()=>B("content.tooltips")),ne(a=>Qn(a)));return r.subscribe(o),t.pipe(W(n),m(a=>$({ref:e},a)),Re(i.pipe(W(n))))})}function Za(e,{viewport$:t,header$:r}){return mr(e,{viewport$:t,header$:r}).pipe(m(({offset:{y:o}})=>{let{height:n}=ce(e);return{active:o>=n}}),ee("active"))}function Bn(e,t){return C(()=>{let r=new g;r.subscribe({next({active:n}){e.classList.toggle("md-header__title--active",n)},complete(){e.classList.remove("md-header__title--active")}});let o=fe(".md-content h1");return typeof o=="undefined"?S:Za(o,t).pipe(w(n=>r.next(n)),_(()=>r.complete()),m(n=>$({ref:e},n)))})}function Gn(e,{viewport$:t,header$:r}){let o=r.pipe(m(({height:i})=>i),K()),n=o.pipe(v(()=>ge(e).pipe(m(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),ee("bottom"))));return z([o,n,t]).pipe(m(([i,{top:a,bottom:s},{offset:{y:p},size:{height:c}}])=>(c=Math.max(0,c-Math.max(0,a-p,i)-Math.max(0,c+p-s)),{offset:a-i,height:c,active:a-i<=p})),K((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function es(e){let t=__md_get("__palette")||{index:e.findIndex(o=>matchMedia(o.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return I(...e).pipe(ne(o=>h(o,"change").pipe(m(()=>o))),Q(e[r]),m(o=>({index:e.indexOf(o),color:{media:o.getAttribute("data-md-color-media"),scheme:o.getAttribute("data-md-color-scheme"),primary:o.getAttribute("data-md-color-primary"),accent:o.getAttribute("data-md-color-accent")}})),G(1))}function Jn(e){let t=P("input",e),r=x("meta",{name:"theme-color"});document.head.appendChild(r);let o=x("meta",{name:"color-scheme"});document.head.appendChild(o);let n=Pt("(prefers-color-scheme: light)");return C(()=>{let i=new g;return i.subscribe(a=>{if(document.body.setAttribute("data-md-color-switching",""),a.color.media==="(prefers-color-scheme)"){let s=matchMedia("(prefers-color-scheme: light)"),p=document.querySelector(s.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");a.color.scheme=p.getAttribute("data-md-color-scheme"),a.color.primary=p.getAttribute("data-md-color-primary"),a.color.accent=p.getAttribute("data-md-color-accent")}for(let[s,p]of Object.entries(a.color))document.body.setAttribute(`data-md-color-${s}`,p);for(let s=0;sa.key==="Enter"),re(i,(a,s)=>s)).subscribe(({index:a})=>{a=(a+1)%t.length,t[a].click(),t[a].focus()}),i.pipe(m(()=>{let a=Se("header"),s=window.getComputedStyle(a);return o.content=s.colorScheme,s.backgroundColor.match(/\d+/g).map(p=>(+p).toString(16).padStart(2,"0")).join("")})).subscribe(a=>r.content=`#${a}`),i.pipe(ve(se)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),es(t).pipe(W(n.pipe(Ce(1))),ct(),w(a=>i.next(a)),_(()=>i.complete()),m(a=>$({ref:e},a)))})}function Xn(e,{progress$:t}){return C(()=>{let r=new g;return r.subscribe(({value:o})=>{e.style.setProperty("--md-progress-value",`${o}`)}),t.pipe(w(o=>r.next({value:o})),_(()=>r.complete()),m(o=>({ref:e,value:o})))})}var Jr=Mt(Br());function ts(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function Zn({alert$:e}){Jr.default.isSupported()&&new j(t=>{new Jr.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||ts(R(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe(w(t=>{t.trigger.focus()}),m(()=>Ee("clipboard.copied"))).subscribe(e)}function ei(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,e}function rs(e,t){let r=new Map;for(let o of P("url",e)){let n=R("loc",o),i=[ei(new URL(n.textContent),t)];r.set(`${i[0]}`,i);for(let a of P("[rel=alternate]",o)){let s=a.getAttribute("href");s!=null&&i.push(ei(new URL(s),t))}}return r}function ur(e){return un(new URL("sitemap.xml",e)).pipe(m(t=>rs(t,new URL(e))),de(()=>I(new Map)))}function os(e,t){if(!(e.target instanceof Element))return S;let r=e.target.closest("a");if(r===null)return S;if(r.target||e.metaKey||e.ctrlKey)return S;let o=new URL(r.href);return o.search=o.hash="",t.has(`${o}`)?(e.preventDefault(),I(new URL(r.href))):S}function ti(e){let t=new Map;for(let r of P(":scope > *",e.head))t.set(r.outerHTML,r);return t}function ri(e){for(let t of P("[href], [src]",e))for(let r of["href","src"]){let o=t.getAttribute(r);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){t[r]=t[r];break}}return I(e)}function ns(e){for(let o of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...B("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let n=fe(o),i=fe(o,e);typeof n!="undefined"&&typeof i!="undefined"&&n.replaceWith(i)}let t=ti(document);for(let[o,n]of ti(e))t.has(o)?t.delete(o):document.head.appendChild(n);for(let o of t.values()){let n=o.getAttribute("name");n!=="theme-color"&&n!=="color-scheme"&&o.remove()}let r=Se("container");return We(P("script",r)).pipe(v(o=>{let n=e.createElement("script");if(o.src){for(let i of o.getAttributeNames())n.setAttribute(i,o.getAttribute(i));return o.replaceWith(n),new j(i=>{n.onload=()=>i.complete()})}else return n.textContent=o.textContent,o.replaceWith(n),S}),Z(),ie(document))}function oi({location$:e,viewport$:t,progress$:r}){let o=xe();if(location.protocol==="file:")return S;let n=ur(o.base);I(document).subscribe(ri);let i=h(document.body,"click").pipe(He(n),v(([p,c])=>os(p,c)),pe()),a=h(window,"popstate").pipe(m(ye),pe());i.pipe(re(t)).subscribe(([p,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",p)}),O(i,a).subscribe(e);let s=e.pipe(ee("pathname"),v(p=>fn(p,{progress$:r}).pipe(de(()=>(lt(p,!0),S)))),v(ri),v(ns),pe());return O(s.pipe(re(e,(p,c)=>c)),s.pipe(v(()=>e),ee("pathname"),v(()=>e),ee("hash")),e.pipe(K((p,c)=>p.pathname===c.pathname&&p.hash===c.hash),v(()=>i),w(()=>history.back()))).subscribe(p=>{var c,l;history.state!==null||!p.hash?window.scrollTo(0,(l=(c=history.state)==null?void 0:c.y)!=null?l:0):(history.scrollRestoration="auto",on(p.hash),history.scrollRestoration="manual")}),e.subscribe(()=>{history.scrollRestoration="manual"}),h(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),t.pipe(ee("offset"),_e(100)).subscribe(({offset:p})=>{history.replaceState(p,"")}),s}var ni=Mt(qr());function ii(e){let t=e.separator.split("|").map(n=>n.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":n).join("|"),r=new RegExp(t,"img"),o=(n,i,a)=>`${i}${a}`;return n=>{n=n.replace(/[\s*+\-:~^]+/g," ").trim();let i=new RegExp(`(^|${e.separator}|)(${n.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&").replace(r,"|")})`,"img");return a=>(0,ni.default)(a).replace(i,o).replace(/<\/mark>(\s+)]*>/img,"$1")}}function jt(e){return e.type===1}function dr(e){return e.type===3}function ai(e,t){let r=yn(e);return O(I(location.protocol!=="file:"),ze("search")).pipe(Ae(o=>o),v(()=>t)).subscribe(({config:o,docs:n})=>r.next({type:0,data:{config:o,docs:n,options:{suggest:B("search.suggest")}}})),r}function si(e){var l;let{selectedVersionSitemap:t,selectedVersionBaseURL:r,currentLocation:o,currentBaseURL:n}=e,i=(l=Xr(n))==null?void 0:l.pathname;if(i===void 0)return;let a=ss(o.pathname,i);if(a===void 0)return;let s=ps(t.keys());if(!t.has(s))return;let p=Xr(a,s);if(!p||!t.has(p.href))return;let c=Xr(a,r);if(c)return c.hash=o.hash,c.search=o.search,c}function Xr(e,t){try{return new URL(e,t)}catch(r){return}}function ss(e,t){if(e.startsWith(t))return e.slice(t.length)}function cs(e,t){let r=Math.min(e.length,t.length),o;for(o=0;oS)),o=r.pipe(m(n=>{let[,i]=t.base.match(/([^/]+)\/?$/);return n.find(({version:a,aliases:s})=>a===i||s.includes(i))||n[0]}));r.pipe(m(n=>new Map(n.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),v(n=>h(document.body,"click").pipe(b(i=>!i.metaKey&&!i.ctrlKey),re(o),v(([i,a])=>{if(i.target instanceof Element){let s=i.target.closest("a");if(s&&!s.target&&n.has(s.href)){let p=s.href;return!i.target.closest(".md-version")&&n.get(p)===a?S:(i.preventDefault(),I(new URL(p)))}}return S}),v(i=>ur(i).pipe(m(a=>{var s;return(s=si({selectedVersionSitemap:a,selectedVersionBaseURL:i,currentLocation:ye(),currentBaseURL:t.base}))!=null?s:i})))))).subscribe(n=>lt(n,!0)),z([r,o]).subscribe(([n,i])=>{R(".md-header__topic").appendChild(Cn(n,i))}),e.pipe(v(()=>o)).subscribe(n=>{var a;let i=__md_get("__outdated",sessionStorage);if(i===null){i=!0;let s=((a=t.version)==null?void 0:a.default)||"latest";Array.isArray(s)||(s=[s]);e:for(let p of s)for(let c of n.aliases.concat(n.version))if(new RegExp(p,"i").test(c)){i=!1;break e}__md_set("__outdated",i,sessionStorage)}if(i)for(let s of ae("outdated"))s.hidden=!1})}function ls(e,{worker$:t}){let{searchParams:r}=ye();r.has("q")&&(Je("search",!0),e.value=r.get("q"),e.focus(),ze("search").pipe(Ae(i=>!i)).subscribe(()=>{let i=ye();i.searchParams.delete("q"),history.replaceState({},"",`${i}`)}));let o=et(e),n=O(t.pipe(Ae(jt)),h(e,"keyup"),o).pipe(m(()=>e.value),K());return z([n,o]).pipe(m(([i,a])=>({value:i,focus:a})),G(1))}function pi(e,{worker$:t}){let r=new g,o=r.pipe(Z(),ie(!0));z([t.pipe(Ae(jt)),r],(i,a)=>a).pipe(ee("value")).subscribe(({value:i})=>t.next({type:2,data:i})),r.pipe(ee("focus")).subscribe(({focus:i})=>{i&&Je("search",i)}),h(e.form,"reset").pipe(W(o)).subscribe(()=>e.focus());let n=R("header [for=__search]");return h(n,"click").subscribe(()=>e.focus()),ls(e,{worker$:t}).pipe(w(i=>r.next(i)),_(()=>r.complete()),m(i=>$({ref:e},i)),G(1))}function li(e,{worker$:t,query$:r}){let o=new g,n=on(e.parentElement).pipe(b(Boolean)),i=e.parentElement,a=R(":scope > :first-child",e),s=R(":scope > :last-child",e);ze("search").subscribe(l=>s.setAttribute("role",l?"list":"presentation")),o.pipe(re(r),Wr(t.pipe(Ae(jt)))).subscribe(([{items:l},{value:f}])=>{switch(l.length){case 0:a.textContent=f.length?Ee("search.result.none"):Ee("search.result.placeholder");break;case 1:a.textContent=Ee("search.result.one");break;default:let u=sr(l.length);a.textContent=Ee("search.result.other",u)}});let p=o.pipe(w(()=>s.innerHTML=""),v(({items:l})=>O(I(...l.slice(0,10)),I(...l.slice(10)).pipe(Be(4),Vr(n),v(([f])=>f)))),m(Mn),pe());return p.subscribe(l=>s.appendChild(l)),p.pipe(ne(l=>{let f=fe("details",l);return typeof f=="undefined"?S:h(f,"toggle").pipe(W(o),m(()=>f))})).subscribe(l=>{l.open===!1&&l.offsetTop<=i.scrollTop&&i.scrollTo({top:l.offsetTop})}),t.pipe(b(dr),m(({data:l})=>l)).pipe(w(l=>o.next(l)),_(()=>o.complete()),m(l=>$({ref:e},l)))}function ms(e,{query$:t}){return t.pipe(m(({value:r})=>{let o=ye();return o.hash="",r=r.replace(/\s+/g,"+").replace(/&/g,"%26").replace(/=/g,"%3D"),o.search=`q=${r}`,{url:o}}))}function mi(e,t){let r=new g,o=r.pipe(Z(),ie(!0));return r.subscribe(({url:n})=>{e.setAttribute("data-clipboard-text",e.href),e.href=`${n}`}),h(e,"click").pipe(W(o)).subscribe(n=>n.preventDefault()),ms(e,t).pipe(w(n=>r.next(n)),_(()=>r.complete()),m(n=>$({ref:e},n)))}function fi(e,{worker$:t,keyboard$:r}){let o=new g,n=Se("search-query"),i=O(h(n,"keydown"),h(n,"focus")).pipe(ve(se),m(()=>n.value),K());return o.pipe(He(i),m(([{suggest:s},p])=>{let c=p.split(/([\s-]+)/);if(s!=null&&s.length&&c[c.length-1]){let l=s[s.length-1];l.startsWith(c[c.length-1])&&(c[c.length-1]=l)}else c.length=0;return c})).subscribe(s=>e.innerHTML=s.join("").replace(/\s/g," ")),r.pipe(b(({mode:s})=>s==="search")).subscribe(s=>{switch(s.type){case"ArrowRight":e.innerText.length&&n.selectionStart===n.value.length&&(n.value=e.innerText);break}}),t.pipe(b(dr),m(({data:s})=>s)).pipe(w(s=>o.next(s)),_(()=>o.complete()),m(()=>({ref:e})))}function ui(e,{index$:t,keyboard$:r}){let o=xe();try{let n=ai(o.search,t),i=Se("search-query",e),a=Se("search-result",e);h(e,"click").pipe(b(({target:p})=>p instanceof Element&&!!p.closest("a"))).subscribe(()=>Je("search",!1)),r.pipe(b(({mode:p})=>p==="search")).subscribe(p=>{let c=Ie();switch(p.type){case"Enter":if(c===i){let l=new Map;for(let f of P(":first-child [href]",a)){let u=f.firstElementChild;l.set(f,parseFloat(u.getAttribute("data-md-score")))}if(l.size){let[[f]]=[...l].sort(([,u],[,d])=>d-u);f.click()}p.claim()}break;case"Escape":case"Tab":Je("search",!1),i.blur();break;case"ArrowUp":case"ArrowDown":if(typeof c=="undefined")i.focus();else{let l=[i,...P(":not(details) > [href], summary, details[open] [href]",a)],f=Math.max(0,(Math.max(0,l.indexOf(c))+l.length+(p.type==="ArrowUp"?-1:1))%l.length);l[f].focus()}p.claim();break;default:i!==Ie()&&i.focus()}}),r.pipe(b(({mode:p})=>p==="global")).subscribe(p=>{switch(p.type){case"f":case"s":case"/":i.focus(),i.select(),p.claim();break}});let s=pi(i,{worker$:n});return O(s,li(a,{worker$:n,query$:s})).pipe(Re(...ae("search-share",e).map(p=>mi(p,{query$:s})),...ae("search-suggest",e).map(p=>fi(p,{worker$:n,keyboard$:r}))))}catch(n){return e.hidden=!0,Ye}}function di(e,{index$:t,location$:r}){return z([t,r.pipe(Q(ye()),b(o=>!!o.searchParams.get("h")))]).pipe(m(([o,n])=>ii(o.config)(n.searchParams.get("h"))),m(o=>{var a;let n=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let s=i.nextNode();s;s=i.nextNode())if((a=s.parentElement)!=null&&a.offsetHeight){let p=s.textContent,c=o(p);c.length>p.length&&n.set(s,c)}for(let[s,p]of n){let{childNodes:c}=x("span",null,p);s.replaceWith(...Array.from(c))}return{ref:e,nodes:n}}))}function fs(e,{viewport$:t,main$:r}){let o=e.closest(".md-grid"),n=o.offsetTop-o.parentElement.offsetTop;return z([r,t]).pipe(m(([{offset:i,height:a},{offset:{y:s}}])=>(a=a+Math.min(n,Math.max(0,s-i))-n,{height:a,locked:s>=i+n})),K((i,a)=>i.height===a.height&&i.locked===a.locked))}function Zr(e,o){var n=o,{header$:t}=n,r=so(n,["header$"]);let i=R(".md-sidebar__scrollwrap",e),{y:a}=Ve(i);return C(()=>{let s=new g,p=s.pipe(Z(),ie(!0)),c=s.pipe(Me(0,me));return c.pipe(re(t)).subscribe({next([{height:l},{height:f}]){i.style.height=`${l-2*a}px`,e.style.top=`${f}px`},complete(){i.style.height="",e.style.top=""}}),c.pipe(Ae()).subscribe(()=>{for(let l of P(".md-nav__link--active[href]",e)){if(!l.clientHeight)continue;let f=l.closest(".md-sidebar__scrollwrap");if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:d}=ce(f);f.scrollTo({top:u-d/2})}}}),ue(P("label[tabindex]",e)).pipe(ne(l=>h(l,"click").pipe(ve(se),m(()=>l),W(p)))).subscribe(l=>{let f=R(`[id="${l.htmlFor}"]`);R(`[aria-labelledby="${l.id}"]`).setAttribute("aria-expanded",`${f.checked}`)}),fs(e,r).pipe(w(l=>s.next(l)),_(()=>s.complete()),m(l=>$({ref:e},l)))})}function hi(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return st(je(`${r}/releases/latest`).pipe(de(()=>S),m(o=>({version:o.tag_name})),De({})),je(r).pipe(de(()=>S),m(o=>({stars:o.stargazers_count,forks:o.forks_count})),De({}))).pipe(m(([o,n])=>$($({},o),n)))}else{let r=`https://api.github.com/users/${e}`;return je(r).pipe(m(o=>({repositories:o.public_repos})),De({}))}}function bi(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return st(je(`${r}/releases/permalink/latest`).pipe(de(()=>S),m(({tag_name:o})=>({version:o})),De({})),je(r).pipe(de(()=>S),m(({star_count:o,forks_count:n})=>({stars:o,forks:n})),De({}))).pipe(m(([o,n])=>$($({},o),n)))}function vi(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,o]=t;return hi(r,o)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,o]=t;return bi(r,o)}return S}var us;function ds(e){return us||(us=C(()=>{let t=__md_get("__source",sessionStorage);if(t)return I(t);if(ae("consent").length){let o=__md_get("__consent");if(!(o&&o.github))return S}return vi(e.href).pipe(w(o=>__md_set("__source",o,sessionStorage)))}).pipe(de(()=>S),b(t=>Object.keys(t).length>0),m(t=>({facts:t})),G(1)))}function gi(e){let t=R(":scope > :last-child",e);return C(()=>{let r=new g;return r.subscribe(({facts:o})=>{t.appendChild(_n(o)),t.classList.add("md-source__repository--active")}),ds(e).pipe(w(o=>r.next(o)),_(()=>r.complete()),m(o=>$({ref:e},o)))})}function hs(e,{viewport$:t,header$:r}){return ge(document.body).pipe(v(()=>mr(e,{header$:r,viewport$:t})),m(({offset:{y:o}})=>({hidden:o>=10})),ee("hidden"))}function yi(e,t){return C(()=>{let r=new g;return r.subscribe({next({hidden:o}){e.hidden=o},complete(){e.hidden=!1}}),(B("navigation.tabs.sticky")?I({hidden:!1}):hs(e,t)).pipe(w(o=>r.next(o)),_(()=>r.complete()),m(o=>$({ref:e},o)))})}function bs(e,{viewport$:t,header$:r}){let o=new Map,n=P(".md-nav__link",e);for(let s of n){let p=decodeURIComponent(s.hash.substring(1)),c=fe(`[id="${p}"]`);typeof c!="undefined"&&o.set(s,c)}let i=r.pipe(ee("height"),m(({height:s})=>{let p=Se("main"),c=R(":scope > :first-child",p);return s+.8*(c.offsetTop-p.offsetTop)}),pe());return ge(document.body).pipe(ee("height"),v(s=>C(()=>{let p=[];return I([...o].reduce((c,[l,f])=>{for(;p.length&&o.get(p[p.length-1]).tagName>=f.tagName;)p.pop();let u=f.offsetTop;for(;!u&&f.parentElement;)f=f.parentElement,u=f.offsetTop;let d=f.offsetParent;for(;d;d=d.offsetParent)u+=d.offsetTop;return c.set([...p=[...p,l]].reverse(),u)},new Map))}).pipe(m(p=>new Map([...p].sort(([,c],[,l])=>c-l))),He(i),v(([p,c])=>t.pipe(Fr(([l,f],{offset:{y:u},size:d})=>{let y=u+d.height>=Math.floor(s.height);for(;f.length;){let[,L]=f[0];if(L-c=u&&!y)f=[l.pop(),...f];else break}return[l,f]},[[],[...p]]),K((l,f)=>l[0]===f[0]&&l[1]===f[1])))))).pipe(m(([s,p])=>({prev:s.map(([c])=>c),next:p.map(([c])=>c)})),Q({prev:[],next:[]}),Be(2,1),m(([s,p])=>s.prev.length{let i=new g,a=i.pipe(Z(),ie(!0));if(i.subscribe(({prev:s,next:p})=>{for(let[c]of p)c.classList.remove("md-nav__link--passed"),c.classList.remove("md-nav__link--active");for(let[c,[l]]of s.entries())l.classList.add("md-nav__link--passed"),l.classList.toggle("md-nav__link--active",c===s.length-1)}),B("toc.follow")){let s=O(t.pipe(_e(1),m(()=>{})),t.pipe(_e(250),m(()=>"smooth")));i.pipe(b(({prev:p})=>p.length>0),He(o.pipe(ve(se))),re(s)).subscribe(([[{prev:p}],c])=>{let[l]=p[p.length-1];if(l.offsetHeight){let f=cr(l);if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:d}=ce(f);f.scrollTo({top:u-d/2,behavior:c})}}})}return B("navigation.tracking")&&t.pipe(W(a),ee("offset"),_e(250),Ce(1),W(n.pipe(Ce(1))),ct({delay:250}),re(i)).subscribe(([,{prev:s}])=>{let p=ye(),c=s[s.length-1];if(c&&c.length){let[l]=c,{hash:f}=new URL(l.href);p.hash!==f&&(p.hash=f,history.replaceState({},"",`${p}`))}else p.hash="",history.replaceState({},"",`${p}`)}),bs(e,{viewport$:t,header$:r}).pipe(w(s=>i.next(s)),_(()=>i.complete()),m(s=>$({ref:e},s)))})}function vs(e,{viewport$:t,main$:r,target$:o}){let n=t.pipe(m(({offset:{y:a}})=>a),Be(2,1),m(([a,s])=>a>s&&s>0),K()),i=r.pipe(m(({active:a})=>a));return z([i,n]).pipe(m(([a,s])=>!(a&&s)),K(),W(o.pipe(Ce(1))),ie(!0),ct({delay:250}),m(a=>({hidden:a})))}function Ei(e,{viewport$:t,header$:r,main$:o,target$:n}){let i=new g,a=i.pipe(Z(),ie(!0));return i.subscribe({next({hidden:s}){e.hidden=s,s?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(W(a),ee("height")).subscribe(({height:s})=>{e.style.top=`${s+16}px`}),h(e,"click").subscribe(s=>{s.preventDefault(),window.scrollTo({top:0})}),vs(e,{viewport$:t,main$:o,target$:n}).pipe(w(s=>i.next(s)),_(()=>i.complete()),m(s=>$({ref:e},s)))}function wi({document$:e,viewport$:t}){e.pipe(v(()=>P(".md-ellipsis")),ne(r=>tt(r).pipe(W(e.pipe(Ce(1))),b(o=>o),m(()=>r),Te(1))),b(r=>r.offsetWidth{let o=r.innerText,n=r.closest("a")||r;return n.title=o,B("content.tooltips")?mt(n,{viewport$:t}).pipe(W(e.pipe(Ce(1))),_(()=>n.removeAttribute("title"))):S})).subscribe(),B("content.tooltips")&&e.pipe(v(()=>P(".md-status")),ne(r=>mt(r,{viewport$:t}))).subscribe()}function Ti({document$:e,tablet$:t}){e.pipe(v(()=>P(".md-toggle--indeterminate")),w(r=>{r.indeterminate=!0,r.checked=!1}),ne(r=>h(r,"change").pipe(Dr(()=>r.classList.contains("md-toggle--indeterminate")),m(()=>r))),re(t)).subscribe(([r,o])=>{r.classList.remove("md-toggle--indeterminate"),o&&(r.checked=!1)})}function gs(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function Si({document$:e}){e.pipe(v(()=>P("[data-md-scrollfix]")),w(t=>t.removeAttribute("data-md-scrollfix")),b(gs),ne(t=>h(t,"touchstart").pipe(m(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}function Oi({viewport$:e,tablet$:t}){z([ze("search"),t]).pipe(m(([r,o])=>r&&!o),v(r=>I(r).pipe(Ge(r?400:100))),re(e)).subscribe(([r,{offset:{y:o}}])=>{if(r)document.body.setAttribute("data-md-scrolllock",""),document.body.style.top=`-${o}px`;else{let n=-1*parseInt(document.body.style.top,10);document.body.removeAttribute("data-md-scrolllock"),document.body.style.top="",n&&window.scrollTo(0,n)}})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let o=e[r];typeof o=="string"?o=document.createTextNode(o):o.parentNode&&o.parentNode.removeChild(o),r?t.insertBefore(this.previousSibling,o):t.replaceChild(o,this)}}}));function ys(){return location.protocol==="file:"?Tt(`${new URL("search/search_index.js",eo.base)}`).pipe(m(()=>__index),G(1)):je(new URL("search/search_index.json",eo.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var ot=Go(),Ut=sn(),Lt=ln(Ut),to=an(),Oe=gn(),hr=Pt("(min-width: 960px)"),Mi=Pt("(min-width: 1220px)"),_i=mn(),eo=xe(),Ai=document.forms.namedItem("search")?ys():Ye,ro=new g;Zn({alert$:ro});var oo=new g;B("navigation.instant")&&oi({location$:Ut,viewport$:Oe,progress$:oo}).subscribe(ot);var Li;((Li=eo.version)==null?void 0:Li.provider)==="mike"&&ci({document$:ot});O(Ut,Lt).pipe(Ge(125)).subscribe(()=>{Je("drawer",!1),Je("search",!1)});to.pipe(b(({mode:e})=>e==="global")).subscribe(e=>{switch(e.type){case"p":case",":let t=fe("link[rel=prev]");typeof t!="undefined"&<(t);break;case"n":case".":let r=fe("link[rel=next]");typeof r!="undefined"&<(r);break;case"Enter":let o=Ie();o instanceof HTMLLabelElement&&o.click()}});wi({viewport$:Oe,document$:ot});Ti({document$:ot,tablet$:hr});Si({document$:ot});Oi({viewport$:Oe,tablet$:hr});var rt=Kn(Se("header"),{viewport$:Oe}),Ft=ot.pipe(m(()=>Se("main")),v(e=>Gn(e,{viewport$:Oe,header$:rt})),G(1)),xs=O(...ae("consent").map(e=>En(e,{target$:Lt})),...ae("dialog").map(e=>qn(e,{alert$:ro})),...ae("palette").map(e=>Jn(e)),...ae("progress").map(e=>Xn(e,{progress$:oo})),...ae("search").map(e=>ui(e,{index$:Ai,keyboard$:to})),...ae("source").map(e=>gi(e))),Es=C(()=>O(...ae("announce").map(e=>xn(e)),...ae("content").map(e=>zn(e,{viewport$:Oe,target$:Lt,print$:_i})),...ae("content").map(e=>B("search.highlight")?di(e,{index$:Ai,location$:Ut}):S),...ae("header").map(e=>Yn(e,{viewport$:Oe,header$:rt,main$:Ft})),...ae("header-title").map(e=>Bn(e,{viewport$:Oe,header$:rt})),...ae("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?Nr(Mi,()=>Zr(e,{viewport$:Oe,header$:rt,main$:Ft})):Nr(hr,()=>Zr(e,{viewport$:Oe,header$:rt,main$:Ft}))),...ae("tabs").map(e=>yi(e,{viewport$:Oe,header$:rt})),...ae("toc").map(e=>xi(e,{viewport$:Oe,header$:rt,main$:Ft,target$:Lt})),...ae("top").map(e=>Ei(e,{viewport$:Oe,header$:rt,main$:Ft,target$:Lt})))),Ci=ot.pipe(v(()=>Es),Re(xs),G(1));Ci.subscribe();window.document$=ot;window.location$=Ut;window.target$=Lt;window.keyboard$=to;window.viewport$=Oe;window.tablet$=hr;window.screen$=Mi;window.print$=_i;window.alert$=ro;window.progress$=oo;window.component$=Ci;})(); +//# sourceMappingURL=bundle.88dd0f4e.min.js.map diff --git a/site/assets/javascripts/bundle.88dd0f4e.min.js.map b/site/assets/javascripts/bundle.88dd0f4e.min.js.map new file mode 100644 index 00000000..950327a8 --- /dev/null +++ b/site/assets/javascripts/bundle.88dd0f4e.min.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["node_modules/focus-visible/dist/focus-visible.js", "node_modules/escape-html/index.js", "node_modules/clipboard/dist/clipboard.js", "src/templates/assets/javascripts/bundle.ts", "node_modules/tslib/tslib.es6.mjs", "node_modules/rxjs/src/internal/util/isFunction.ts", "node_modules/rxjs/src/internal/util/createErrorClass.ts", "node_modules/rxjs/src/internal/util/UnsubscriptionError.ts", "node_modules/rxjs/src/internal/util/arrRemove.ts", "node_modules/rxjs/src/internal/Subscription.ts", "node_modules/rxjs/src/internal/config.ts", "node_modules/rxjs/src/internal/scheduler/timeoutProvider.ts", "node_modules/rxjs/src/internal/util/reportUnhandledError.ts", "node_modules/rxjs/src/internal/util/noop.ts", "node_modules/rxjs/src/internal/NotificationFactories.ts", "node_modules/rxjs/src/internal/util/errorContext.ts", "node_modules/rxjs/src/internal/Subscriber.ts", "node_modules/rxjs/src/internal/symbol/observable.ts", "node_modules/rxjs/src/internal/util/identity.ts", "node_modules/rxjs/src/internal/util/pipe.ts", "node_modules/rxjs/src/internal/Observable.ts", "node_modules/rxjs/src/internal/util/lift.ts", "node_modules/rxjs/src/internal/operators/OperatorSubscriber.ts", "node_modules/rxjs/src/internal/scheduler/animationFrameProvider.ts", "node_modules/rxjs/src/internal/util/ObjectUnsubscribedError.ts", "node_modules/rxjs/src/internal/Subject.ts", "node_modules/rxjs/src/internal/BehaviorSubject.ts", "node_modules/rxjs/src/internal/scheduler/dateTimestampProvider.ts", "node_modules/rxjs/src/internal/ReplaySubject.ts", "node_modules/rxjs/src/internal/scheduler/Action.ts", "node_modules/rxjs/src/internal/scheduler/intervalProvider.ts", "node_modules/rxjs/src/internal/scheduler/AsyncAction.ts", "node_modules/rxjs/src/internal/Scheduler.ts", "node_modules/rxjs/src/internal/scheduler/AsyncScheduler.ts", "node_modules/rxjs/src/internal/scheduler/async.ts", "node_modules/rxjs/src/internal/scheduler/QueueAction.ts", "node_modules/rxjs/src/internal/scheduler/QueueScheduler.ts", "node_modules/rxjs/src/internal/scheduler/queue.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameAction.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameScheduler.ts", "node_modules/rxjs/src/internal/scheduler/animationFrame.ts", "node_modules/rxjs/src/internal/observable/empty.ts", "node_modules/rxjs/src/internal/util/isScheduler.ts", "node_modules/rxjs/src/internal/util/args.ts", "node_modules/rxjs/src/internal/util/isArrayLike.ts", "node_modules/rxjs/src/internal/util/isPromise.ts", "node_modules/rxjs/src/internal/util/isInteropObservable.ts", "node_modules/rxjs/src/internal/util/isAsyncIterable.ts", "node_modules/rxjs/src/internal/util/throwUnobservableError.ts", "node_modules/rxjs/src/internal/symbol/iterator.ts", "node_modules/rxjs/src/internal/util/isIterable.ts", "node_modules/rxjs/src/internal/util/isReadableStreamLike.ts", "node_modules/rxjs/src/internal/observable/innerFrom.ts", "node_modules/rxjs/src/internal/util/executeSchedule.ts", "node_modules/rxjs/src/internal/operators/observeOn.ts", "node_modules/rxjs/src/internal/operators/subscribeOn.ts", "node_modules/rxjs/src/internal/scheduled/scheduleObservable.ts", "node_modules/rxjs/src/internal/scheduled/schedulePromise.ts", "node_modules/rxjs/src/internal/scheduled/scheduleArray.ts", "node_modules/rxjs/src/internal/scheduled/scheduleIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleAsyncIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleReadableStreamLike.ts", "node_modules/rxjs/src/internal/scheduled/scheduled.ts", "node_modules/rxjs/src/internal/observable/from.ts", "node_modules/rxjs/src/internal/observable/of.ts", "node_modules/rxjs/src/internal/observable/throwError.ts", "node_modules/rxjs/src/internal/util/EmptyError.ts", "node_modules/rxjs/src/internal/util/isDate.ts", "node_modules/rxjs/src/internal/operators/map.ts", "node_modules/rxjs/src/internal/util/mapOneOrManyArgs.ts", "node_modules/rxjs/src/internal/util/argsArgArrayOrObject.ts", "node_modules/rxjs/src/internal/util/createObject.ts", "node_modules/rxjs/src/internal/observable/combineLatest.ts", "node_modules/rxjs/src/internal/operators/mergeInternals.ts", "node_modules/rxjs/src/internal/operators/mergeMap.ts", "node_modules/rxjs/src/internal/operators/mergeAll.ts", "node_modules/rxjs/src/internal/operators/concatAll.ts", "node_modules/rxjs/src/internal/observable/concat.ts", "node_modules/rxjs/src/internal/observable/defer.ts", "node_modules/rxjs/src/internal/observable/fromEvent.ts", "node_modules/rxjs/src/internal/observable/fromEventPattern.ts", "node_modules/rxjs/src/internal/observable/timer.ts", "node_modules/rxjs/src/internal/observable/merge.ts", "node_modules/rxjs/src/internal/observable/never.ts", "node_modules/rxjs/src/internal/util/argsOrArgArray.ts", "node_modules/rxjs/src/internal/operators/filter.ts", "node_modules/rxjs/src/internal/observable/zip.ts", "node_modules/rxjs/src/internal/operators/audit.ts", "node_modules/rxjs/src/internal/operators/auditTime.ts", "node_modules/rxjs/src/internal/operators/bufferCount.ts", "node_modules/rxjs/src/internal/operators/catchError.ts", "node_modules/rxjs/src/internal/operators/scanInternals.ts", "node_modules/rxjs/src/internal/operators/combineLatest.ts", "node_modules/rxjs/src/internal/operators/combineLatestWith.ts", "node_modules/rxjs/src/internal/operators/debounce.ts", "node_modules/rxjs/src/internal/operators/debounceTime.ts", "node_modules/rxjs/src/internal/operators/defaultIfEmpty.ts", "node_modules/rxjs/src/internal/operators/take.ts", "node_modules/rxjs/src/internal/operators/ignoreElements.ts", "node_modules/rxjs/src/internal/operators/mapTo.ts", "node_modules/rxjs/src/internal/operators/delayWhen.ts", "node_modules/rxjs/src/internal/operators/delay.ts", "node_modules/rxjs/src/internal/operators/distinctUntilChanged.ts", "node_modules/rxjs/src/internal/operators/distinctUntilKeyChanged.ts", "node_modules/rxjs/src/internal/operators/throwIfEmpty.ts", "node_modules/rxjs/src/internal/operators/endWith.ts", "node_modules/rxjs/src/internal/operators/finalize.ts", "node_modules/rxjs/src/internal/operators/first.ts", "node_modules/rxjs/src/internal/operators/takeLast.ts", "node_modules/rxjs/src/internal/operators/merge.ts", "node_modules/rxjs/src/internal/operators/mergeWith.ts", "node_modules/rxjs/src/internal/operators/repeat.ts", "node_modules/rxjs/src/internal/operators/scan.ts", "node_modules/rxjs/src/internal/operators/share.ts", "node_modules/rxjs/src/internal/operators/shareReplay.ts", "node_modules/rxjs/src/internal/operators/skip.ts", "node_modules/rxjs/src/internal/operators/skipUntil.ts", "node_modules/rxjs/src/internal/operators/startWith.ts", "node_modules/rxjs/src/internal/operators/switchMap.ts", "node_modules/rxjs/src/internal/operators/takeUntil.ts", "node_modules/rxjs/src/internal/operators/takeWhile.ts", "node_modules/rxjs/src/internal/operators/tap.ts", "node_modules/rxjs/src/internal/operators/throttle.ts", "node_modules/rxjs/src/internal/operators/throttleTime.ts", "node_modules/rxjs/src/internal/operators/withLatestFrom.ts", "node_modules/rxjs/src/internal/operators/zip.ts", "node_modules/rxjs/src/internal/operators/zipWith.ts", "src/templates/assets/javascripts/browser/document/index.ts", "src/templates/assets/javascripts/browser/element/_/index.ts", "src/templates/assets/javascripts/browser/element/focus/index.ts", "src/templates/assets/javascripts/browser/element/hover/index.ts", "src/templates/assets/javascripts/utilities/h/index.ts", "src/templates/assets/javascripts/utilities/round/index.ts", "src/templates/assets/javascripts/browser/script/index.ts", "src/templates/assets/javascripts/browser/element/size/_/index.ts", "src/templates/assets/javascripts/browser/element/size/content/index.ts", "src/templates/assets/javascripts/browser/element/offset/_/index.ts", "src/templates/assets/javascripts/browser/element/offset/content/index.ts", "src/templates/assets/javascripts/browser/element/visibility/index.ts", "src/templates/assets/javascripts/browser/toggle/index.ts", "src/templates/assets/javascripts/browser/keyboard/index.ts", "src/templates/assets/javascripts/browser/location/_/index.ts", "src/templates/assets/javascripts/browser/location/hash/index.ts", "src/templates/assets/javascripts/browser/media/index.ts", "src/templates/assets/javascripts/browser/request/index.ts", "src/templates/assets/javascripts/browser/viewport/offset/index.ts", "src/templates/assets/javascripts/browser/viewport/size/index.ts", "src/templates/assets/javascripts/browser/viewport/_/index.ts", "src/templates/assets/javascripts/browser/viewport/at/index.ts", "src/templates/assets/javascripts/browser/worker/index.ts", "src/templates/assets/javascripts/_/index.ts", "src/templates/assets/javascripts/components/_/index.ts", "src/templates/assets/javascripts/components/announce/index.ts", "src/templates/assets/javascripts/components/consent/index.ts", "src/templates/assets/javascripts/templates/tooltip/index.tsx", "src/templates/assets/javascripts/templates/annotation/index.tsx", "src/templates/assets/javascripts/templates/clipboard/index.tsx", "src/templates/assets/javascripts/templates/search/index.tsx", "src/templates/assets/javascripts/templates/source/index.tsx", "src/templates/assets/javascripts/templates/tabbed/index.tsx", "src/templates/assets/javascripts/templates/table/index.tsx", "src/templates/assets/javascripts/templates/version/index.tsx", "src/templates/assets/javascripts/components/tooltip2/index.ts", "src/templates/assets/javascripts/components/content/annotation/_/index.ts", "src/templates/assets/javascripts/components/content/annotation/list/index.ts", "src/templates/assets/javascripts/components/content/annotation/block/index.ts", "src/templates/assets/javascripts/components/content/code/_/index.ts", "src/templates/assets/javascripts/components/content/details/index.ts", "src/templates/assets/javascripts/components/content/mermaid/index.css", "src/templates/assets/javascripts/components/content/mermaid/index.ts", "src/templates/assets/javascripts/components/content/table/index.ts", "src/templates/assets/javascripts/components/content/tabs/index.ts", "src/templates/assets/javascripts/components/content/_/index.ts", "src/templates/assets/javascripts/components/dialog/index.ts", "src/templates/assets/javascripts/components/tooltip/index.ts", "src/templates/assets/javascripts/components/header/_/index.ts", "src/templates/assets/javascripts/components/header/title/index.ts", "src/templates/assets/javascripts/components/main/index.ts", "src/templates/assets/javascripts/components/palette/index.ts", "src/templates/assets/javascripts/components/progress/index.ts", "src/templates/assets/javascripts/integrations/clipboard/index.ts", "src/templates/assets/javascripts/integrations/sitemap/index.ts", "src/templates/assets/javascripts/integrations/instant/index.ts", "src/templates/assets/javascripts/integrations/search/highlighter/index.ts", "src/templates/assets/javascripts/integrations/search/worker/message/index.ts", "src/templates/assets/javascripts/integrations/search/worker/_/index.ts", "src/templates/assets/javascripts/integrations/version/findurl/index.ts", "src/templates/assets/javascripts/integrations/version/index.ts", "src/templates/assets/javascripts/components/search/query/index.ts", "src/templates/assets/javascripts/components/search/result/index.ts", "src/templates/assets/javascripts/components/search/share/index.ts", "src/templates/assets/javascripts/components/search/suggest/index.ts", "src/templates/assets/javascripts/components/search/_/index.ts", "src/templates/assets/javascripts/components/search/highlight/index.ts", "src/templates/assets/javascripts/components/sidebar/index.ts", "src/templates/assets/javascripts/components/source/facts/github/index.ts", "src/templates/assets/javascripts/components/source/facts/gitlab/index.ts", "src/templates/assets/javascripts/components/source/facts/_/index.ts", "src/templates/assets/javascripts/components/source/_/index.ts", "src/templates/assets/javascripts/components/tabs/index.ts", "src/templates/assets/javascripts/components/toc/index.ts", "src/templates/assets/javascripts/components/top/index.ts", "src/templates/assets/javascripts/patches/ellipsis/index.ts", "src/templates/assets/javascripts/patches/indeterminate/index.ts", "src/templates/assets/javascripts/patches/scrollfix/index.ts", "src/templates/assets/javascripts/patches/scrolllock/index.ts", "src/templates/assets/javascripts/polyfills/index.ts"], + "sourcesContent": ["(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? factory() :\n typeof define === 'function' && define.amd ? define(factory) :\n (factory());\n}(this, (function () { 'use strict';\n\n /**\n * Applies the :focus-visible polyfill at the given scope.\n * A scope in this case is either the top-level Document or a Shadow Root.\n *\n * @param {(Document|ShadowRoot)} scope\n * @see https://github.com/WICG/focus-visible\n */\n function applyFocusVisiblePolyfill(scope) {\n var hadKeyboardEvent = true;\n var hadFocusVisibleRecently = false;\n var hadFocusVisibleRecentlyTimeout = null;\n\n var inputTypesAllowlist = {\n text: true,\n search: true,\n url: true,\n tel: true,\n email: true,\n password: true,\n number: true,\n date: true,\n month: true,\n week: true,\n time: true,\n datetime: true,\n 'datetime-local': true\n };\n\n /**\n * Helper function for legacy browsers and iframes which sometimes focus\n * elements like document, body, and non-interactive SVG.\n * @param {Element} el\n */\n function isValidFocusTarget(el) {\n if (\n el &&\n el !== document &&\n el.nodeName !== 'HTML' &&\n el.nodeName !== 'BODY' &&\n 'classList' in el &&\n 'contains' in el.classList\n ) {\n return true;\n }\n return false;\n }\n\n /**\n * Computes whether the given element should automatically trigger the\n * `focus-visible` class being added, i.e. whether it should always match\n * `:focus-visible` when focused.\n * @param {Element} el\n * @return {boolean}\n */\n function focusTriggersKeyboardModality(el) {\n var type = el.type;\n var tagName = el.tagName;\n\n if (tagName === 'INPUT' && inputTypesAllowlist[type] && !el.readOnly) {\n return true;\n }\n\n if (tagName === 'TEXTAREA' && !el.readOnly) {\n return true;\n }\n\n if (el.isContentEditable) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Add the `focus-visible` class to the given element if it was not added by\n * the author.\n * @param {Element} el\n */\n function addFocusVisibleClass(el) {\n if (el.classList.contains('focus-visible')) {\n return;\n }\n el.classList.add('focus-visible');\n el.setAttribute('data-focus-visible-added', '');\n }\n\n /**\n * Remove the `focus-visible` class from the given element if it was not\n * originally added by the author.\n * @param {Element} el\n */\n function removeFocusVisibleClass(el) {\n if (!el.hasAttribute('data-focus-visible-added')) {\n return;\n }\n el.classList.remove('focus-visible');\n el.removeAttribute('data-focus-visible-added');\n }\n\n /**\n * If the most recent user interaction was via the keyboard;\n * and the key press did not include a meta, alt/option, or control key;\n * then the modality is keyboard. Otherwise, the modality is not keyboard.\n * Apply `focus-visible` to any current active element and keep track\n * of our keyboard modality state with `hadKeyboardEvent`.\n * @param {KeyboardEvent} e\n */\n function onKeyDown(e) {\n if (e.metaKey || e.altKey || e.ctrlKey) {\n return;\n }\n\n if (isValidFocusTarget(scope.activeElement)) {\n addFocusVisibleClass(scope.activeElement);\n }\n\n hadKeyboardEvent = true;\n }\n\n /**\n * If at any point a user clicks with a pointing device, ensure that we change\n * the modality away from keyboard.\n * This avoids the situation where a user presses a key on an already focused\n * element, and then clicks on a different element, focusing it with a\n * pointing device, while we still think we're in keyboard modality.\n * @param {Event} e\n */\n function onPointerDown(e) {\n hadKeyboardEvent = false;\n }\n\n /**\n * On `focus`, add the `focus-visible` class to the target if:\n * - the target received focus as a result of keyboard navigation, or\n * - the event target is an element that will likely require interaction\n * via the keyboard (e.g. a text box)\n * @param {Event} e\n */\n function onFocus(e) {\n // Prevent IE from focusing the document or HTML element.\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {\n addFocusVisibleClass(e.target);\n }\n }\n\n /**\n * On `blur`, remove the `focus-visible` class from the target.\n * @param {Event} e\n */\n function onBlur(e) {\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (\n e.target.classList.contains('focus-visible') ||\n e.target.hasAttribute('data-focus-visible-added')\n ) {\n // To detect a tab/window switch, we look for a blur event followed\n // rapidly by a visibility change.\n // If we don't see a visibility change within 100ms, it's probably a\n // regular focus change.\n hadFocusVisibleRecently = true;\n window.clearTimeout(hadFocusVisibleRecentlyTimeout);\n hadFocusVisibleRecentlyTimeout = window.setTimeout(function() {\n hadFocusVisibleRecently = false;\n }, 100);\n removeFocusVisibleClass(e.target);\n }\n }\n\n /**\n * If the user changes tabs, keep track of whether or not the previously\n * focused element had .focus-visible.\n * @param {Event} e\n */\n function onVisibilityChange(e) {\n if (document.visibilityState === 'hidden') {\n // If the tab becomes active again, the browser will handle calling focus\n // on the element (Safari actually calls it twice).\n // If this tab change caused a blur on an element with focus-visible,\n // re-apply the class when the user switches back to the tab.\n if (hadFocusVisibleRecently) {\n hadKeyboardEvent = true;\n }\n addInitialPointerMoveListeners();\n }\n }\n\n /**\n * Add a group of listeners to detect usage of any pointing devices.\n * These listeners will be added when the polyfill first loads, and anytime\n * the window is blurred, so that they are active when the window regains\n * focus.\n */\n function addInitialPointerMoveListeners() {\n document.addEventListener('mousemove', onInitialPointerMove);\n document.addEventListener('mousedown', onInitialPointerMove);\n document.addEventListener('mouseup', onInitialPointerMove);\n document.addEventListener('pointermove', onInitialPointerMove);\n document.addEventListener('pointerdown', onInitialPointerMove);\n document.addEventListener('pointerup', onInitialPointerMove);\n document.addEventListener('touchmove', onInitialPointerMove);\n document.addEventListener('touchstart', onInitialPointerMove);\n document.addEventListener('touchend', onInitialPointerMove);\n }\n\n function removeInitialPointerMoveListeners() {\n document.removeEventListener('mousemove', onInitialPointerMove);\n document.removeEventListener('mousedown', onInitialPointerMove);\n document.removeEventListener('mouseup', onInitialPointerMove);\n document.removeEventListener('pointermove', onInitialPointerMove);\n document.removeEventListener('pointerdown', onInitialPointerMove);\n document.removeEventListener('pointerup', onInitialPointerMove);\n document.removeEventListener('touchmove', onInitialPointerMove);\n document.removeEventListener('touchstart', onInitialPointerMove);\n document.removeEventListener('touchend', onInitialPointerMove);\n }\n\n /**\n * When the polfyill first loads, assume the user is in keyboard modality.\n * If any event is received from a pointing device (e.g. mouse, pointer,\n * touch), turn off keyboard modality.\n * This accounts for situations where focus enters the page from the URL bar.\n * @param {Event} e\n */\n function onInitialPointerMove(e) {\n // Work around a Safari quirk that fires a mousemove on whenever the\n // window blurs, even if you're tabbing out of the page. \u00AF\\_(\u30C4)_/\u00AF\n if (e.target.nodeName && e.target.nodeName.toLowerCase() === 'html') {\n return;\n }\n\n hadKeyboardEvent = false;\n removeInitialPointerMoveListeners();\n }\n\n // For some kinds of state, we are interested in changes at the global scope\n // only. For example, global pointer input, global key presses and global\n // visibility change should affect the state at every scope:\n document.addEventListener('keydown', onKeyDown, true);\n document.addEventListener('mousedown', onPointerDown, true);\n document.addEventListener('pointerdown', onPointerDown, true);\n document.addEventListener('touchstart', onPointerDown, true);\n document.addEventListener('visibilitychange', onVisibilityChange, true);\n\n addInitialPointerMoveListeners();\n\n // For focus and blur, we specifically care about state changes in the local\n // scope. This is because focus / blur events that originate from within a\n // shadow root are not re-dispatched from the host element if it was already\n // the active element in its own scope:\n scope.addEventListener('focus', onFocus, true);\n scope.addEventListener('blur', onBlur, true);\n\n // We detect that a node is a ShadowRoot by ensuring that it is a\n // DocumentFragment and also has a host property. This check covers native\n // implementation and polyfill implementation transparently. If we only cared\n // about the native implementation, we could just check if the scope was\n // an instance of a ShadowRoot.\n if (scope.nodeType === Node.DOCUMENT_FRAGMENT_NODE && scope.host) {\n // Since a ShadowRoot is a special kind of DocumentFragment, it does not\n // have a root element to add a class to. So, we add this attribute to the\n // host element instead:\n scope.host.setAttribute('data-js-focus-visible', '');\n } else if (scope.nodeType === Node.DOCUMENT_NODE) {\n document.documentElement.classList.add('js-focus-visible');\n document.documentElement.setAttribute('data-js-focus-visible', '');\n }\n }\n\n // It is important to wrap all references to global window and document in\n // these checks to support server-side rendering use cases\n // @see https://github.com/WICG/focus-visible/issues/199\n if (typeof window !== 'undefined' && typeof document !== 'undefined') {\n // Make the polyfill helper globally available. This can be used as a signal\n // to interested libraries that wish to coordinate with the polyfill for e.g.,\n // applying the polyfill to a shadow root:\n window.applyFocusVisiblePolyfill = applyFocusVisiblePolyfill;\n\n // Notify interested libraries of the polyfill's presence, in case the\n // polyfill was loaded lazily:\n var event;\n\n try {\n event = new CustomEvent('focus-visible-polyfill-ready');\n } catch (error) {\n // IE11 does not support using CustomEvent as a constructor directly:\n event = document.createEvent('CustomEvent');\n event.initCustomEvent('focus-visible-polyfill-ready', false, false, {});\n }\n\n window.dispatchEvent(event);\n }\n\n if (typeof document !== 'undefined') {\n // Apply the polyfill to the global document, so that no JavaScript\n // coordination is required to use the polyfill in the top-level document:\n applyFocusVisiblePolyfill(document);\n }\n\n})));\n", "/*!\n * escape-html\n * Copyright(c) 2012-2013 TJ Holowaychuk\n * Copyright(c) 2015 Andreas Lubbe\n * Copyright(c) 2015 Tiancheng \"Timothy\" Gu\n * MIT Licensed\n */\n\n'use strict';\n\n/**\n * Module variables.\n * @private\n */\n\nvar matchHtmlRegExp = /[\"'&<>]/;\n\n/**\n * Module exports.\n * @public\n */\n\nmodule.exports = escapeHtml;\n\n/**\n * Escape special characters in the given string of html.\n *\n * @param {string} string The string to escape for inserting into HTML\n * @return {string}\n * @public\n */\n\nfunction escapeHtml(string) {\n var str = '' + string;\n var match = matchHtmlRegExp.exec(str);\n\n if (!match) {\n return str;\n }\n\n var escape;\n var html = '';\n var index = 0;\n var lastIndex = 0;\n\n for (index = match.index; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34: // \"\n escape = '"';\n break;\n case 38: // &\n escape = '&';\n break;\n case 39: // '\n escape = ''';\n break;\n case 60: // <\n escape = '<';\n break;\n case 62: // >\n escape = '>';\n break;\n default:\n continue;\n }\n\n if (lastIndex !== index) {\n html += str.substring(lastIndex, index);\n }\n\n lastIndex = index + 1;\n html += escape;\n }\n\n return lastIndex !== index\n ? html + str.substring(lastIndex, index)\n : html;\n}\n", "/*!\n * clipboard.js v2.0.11\n * https://clipboardjs.com/\n *\n * Licensed MIT \u00A9 Zeno Rocha\n */\n(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ClipboardJS\"] = factory();\n\telse\n\t\troot[\"ClipboardJS\"] = factory();\n})(this, function() {\nreturn /******/ (function() { // webpackBootstrap\n/******/ \tvar __webpack_modules__ = ({\n\n/***/ 686:\n/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\n\n// EXPORTS\n__webpack_require__.d(__webpack_exports__, {\n \"default\": function() { return /* binding */ clipboard; }\n});\n\n// EXTERNAL MODULE: ./node_modules/tiny-emitter/index.js\nvar tiny_emitter = __webpack_require__(279);\nvar tiny_emitter_default = /*#__PURE__*/__webpack_require__.n(tiny_emitter);\n// EXTERNAL MODULE: ./node_modules/good-listener/src/listen.js\nvar listen = __webpack_require__(370);\nvar listen_default = /*#__PURE__*/__webpack_require__.n(listen);\n// EXTERNAL MODULE: ./node_modules/select/src/select.js\nvar src_select = __webpack_require__(817);\nvar select_default = /*#__PURE__*/__webpack_require__.n(src_select);\n;// CONCATENATED MODULE: ./src/common/command.js\n/**\n * Executes a given operation type.\n * @param {String} type\n * @return {Boolean}\n */\nfunction command(type) {\n try {\n return document.execCommand(type);\n } catch (err) {\n return false;\n }\n}\n;// CONCATENATED MODULE: ./src/actions/cut.js\n\n\n/**\n * Cut action wrapper.\n * @param {String|HTMLElement} target\n * @return {String}\n */\n\nvar ClipboardActionCut = function ClipboardActionCut(target) {\n var selectedText = select_default()(target);\n command('cut');\n return selectedText;\n};\n\n/* harmony default export */ var actions_cut = (ClipboardActionCut);\n;// CONCATENATED MODULE: ./src/common/create-fake-element.js\n/**\n * Creates a fake textarea element with a value.\n * @param {String} value\n * @return {HTMLElement}\n */\nfunction createFakeElement(value) {\n var isRTL = document.documentElement.getAttribute('dir') === 'rtl';\n var fakeElement = document.createElement('textarea'); // Prevent zooming on iOS\n\n fakeElement.style.fontSize = '12pt'; // Reset box model\n\n fakeElement.style.border = '0';\n fakeElement.style.padding = '0';\n fakeElement.style.margin = '0'; // Move element out of screen horizontally\n\n fakeElement.style.position = 'absolute';\n fakeElement.style[isRTL ? 'right' : 'left'] = '-9999px'; // Move element to the same position vertically\n\n var yPosition = window.pageYOffset || document.documentElement.scrollTop;\n fakeElement.style.top = \"\".concat(yPosition, \"px\");\n fakeElement.setAttribute('readonly', '');\n fakeElement.value = value;\n return fakeElement;\n}\n;// CONCATENATED MODULE: ./src/actions/copy.js\n\n\n\n/**\n * Create fake copy action wrapper using a fake element.\n * @param {String} target\n * @param {Object} options\n * @return {String}\n */\n\nvar fakeCopyAction = function fakeCopyAction(value, options) {\n var fakeElement = createFakeElement(value);\n options.container.appendChild(fakeElement);\n var selectedText = select_default()(fakeElement);\n command('copy');\n fakeElement.remove();\n return selectedText;\n};\n/**\n * Copy action wrapper.\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @return {String}\n */\n\n\nvar ClipboardActionCopy = function ClipboardActionCopy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n var selectedText = '';\n\n if (typeof target === 'string') {\n selectedText = fakeCopyAction(target, options);\n } else if (target instanceof HTMLInputElement && !['text', 'search', 'url', 'tel', 'password'].includes(target === null || target === void 0 ? void 0 : target.type)) {\n // If input type doesn't support `setSelectionRange`. Simulate it. https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange\n selectedText = fakeCopyAction(target.value, options);\n } else {\n selectedText = select_default()(target);\n command('copy');\n }\n\n return selectedText;\n};\n\n/* harmony default export */ var actions_copy = (ClipboardActionCopy);\n;// CONCATENATED MODULE: ./src/actions/default.js\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\n\n\n/**\n * Inner function which performs selection from either `text` or `target`\n * properties and then executes copy or cut operations.\n * @param {Object} options\n */\n\nvar ClipboardActionDefault = function ClipboardActionDefault() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n // Defines base properties passed from constructor.\n var _options$action = options.action,\n action = _options$action === void 0 ? 'copy' : _options$action,\n container = options.container,\n target = options.target,\n text = options.text; // Sets the `action` to be performed which can be either 'copy' or 'cut'.\n\n if (action !== 'copy' && action !== 'cut') {\n throw new Error('Invalid \"action\" value, use either \"copy\" or \"cut\"');\n } // Sets the `target` property using an element that will be have its content copied.\n\n\n if (target !== undefined) {\n if (target && _typeof(target) === 'object' && target.nodeType === 1) {\n if (action === 'copy' && target.hasAttribute('disabled')) {\n throw new Error('Invalid \"target\" attribute. Please use \"readonly\" instead of \"disabled\" attribute');\n }\n\n if (action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {\n throw new Error('Invalid \"target\" attribute. You can\\'t cut text from elements with \"readonly\" or \"disabled\" attributes');\n }\n } else {\n throw new Error('Invalid \"target\" value, use a valid Element');\n }\n } // Define selection strategy based on `text` property.\n\n\n if (text) {\n return actions_copy(text, {\n container: container\n });\n } // Defines which selection strategy based on `target` property.\n\n\n if (target) {\n return action === 'cut' ? actions_cut(target) : actions_copy(target, {\n container: container\n });\n }\n};\n\n/* harmony default export */ var actions_default = (ClipboardActionDefault);\n;// CONCATENATED MODULE: ./src/clipboard.js\nfunction clipboard_typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { clipboard_typeof = function _typeof(obj) { return typeof obj; }; } else { clipboard_typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return clipboard_typeof(obj); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (clipboard_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n\n\n\n\n\n/**\n * Helper function to retrieve attribute value.\n * @param {String} suffix\n * @param {Element} element\n */\n\nfunction getAttributeValue(suffix, element) {\n var attribute = \"data-clipboard-\".concat(suffix);\n\n if (!element.hasAttribute(attribute)) {\n return;\n }\n\n return element.getAttribute(attribute);\n}\n/**\n * Base class which takes one or more elements, adds event listeners to them,\n * and instantiates a new `ClipboardAction` on each click.\n */\n\n\nvar Clipboard = /*#__PURE__*/function (_Emitter) {\n _inherits(Clipboard, _Emitter);\n\n var _super = _createSuper(Clipboard);\n\n /**\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n * @param {Object} options\n */\n function Clipboard(trigger, options) {\n var _this;\n\n _classCallCheck(this, Clipboard);\n\n _this = _super.call(this);\n\n _this.resolveOptions(options);\n\n _this.listenClick(trigger);\n\n return _this;\n }\n /**\n * Defines if attributes would be resolved using internal setter functions\n * or custom functions that were passed in the constructor.\n * @param {Object} options\n */\n\n\n _createClass(Clipboard, [{\n key: \"resolveOptions\",\n value: function resolveOptions() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n this.action = typeof options.action === 'function' ? options.action : this.defaultAction;\n this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;\n this.text = typeof options.text === 'function' ? options.text : this.defaultText;\n this.container = clipboard_typeof(options.container) === 'object' ? options.container : document.body;\n }\n /**\n * Adds a click event listener to the passed trigger.\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n */\n\n }, {\n key: \"listenClick\",\n value: function listenClick(trigger) {\n var _this2 = this;\n\n this.listener = listen_default()(trigger, 'click', function (e) {\n return _this2.onClick(e);\n });\n }\n /**\n * Defines a new `ClipboardAction` on each click event.\n * @param {Event} e\n */\n\n }, {\n key: \"onClick\",\n value: function onClick(e) {\n var trigger = e.delegateTarget || e.currentTarget;\n var action = this.action(trigger) || 'copy';\n var text = actions_default({\n action: action,\n container: this.container,\n target: this.target(trigger),\n text: this.text(trigger)\n }); // Fires an event based on the copy operation result.\n\n this.emit(text ? 'success' : 'error', {\n action: action,\n text: text,\n trigger: trigger,\n clearSelection: function clearSelection() {\n if (trigger) {\n trigger.focus();\n }\n\n window.getSelection().removeAllRanges();\n }\n });\n }\n /**\n * Default `action` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultAction\",\n value: function defaultAction(trigger) {\n return getAttributeValue('action', trigger);\n }\n /**\n * Default `target` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultTarget\",\n value: function defaultTarget(trigger) {\n var selector = getAttributeValue('target', trigger);\n\n if (selector) {\n return document.querySelector(selector);\n }\n }\n /**\n * Allow fire programmatically a copy action\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @returns Text copied.\n */\n\n }, {\n key: \"defaultText\",\n\n /**\n * Default `text` lookup function.\n * @param {Element} trigger\n */\n value: function defaultText(trigger) {\n return getAttributeValue('text', trigger);\n }\n /**\n * Destroy lifecycle.\n */\n\n }, {\n key: \"destroy\",\n value: function destroy() {\n this.listener.destroy();\n }\n }], [{\n key: \"copy\",\n value: function copy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n return actions_copy(target, options);\n }\n /**\n * Allow fire programmatically a cut action\n * @param {String|HTMLElement} target\n * @returns Text cutted.\n */\n\n }, {\n key: \"cut\",\n value: function cut(target) {\n return actions_cut(target);\n }\n /**\n * Returns the support of the given action, or all actions if no action is\n * given.\n * @param {String} [action]\n */\n\n }, {\n key: \"isSupported\",\n value: function isSupported() {\n var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['copy', 'cut'];\n var actions = typeof action === 'string' ? [action] : action;\n var support = !!document.queryCommandSupported;\n actions.forEach(function (action) {\n support = support && !!document.queryCommandSupported(action);\n });\n return support;\n }\n }]);\n\n return Clipboard;\n}((tiny_emitter_default()));\n\n/* harmony default export */ var clipboard = (Clipboard);\n\n/***/ }),\n\n/***/ 828:\n/***/ (function(module) {\n\nvar DOCUMENT_NODE_TYPE = 9;\n\n/**\n * A polyfill for Element.matches()\n */\nif (typeof Element !== 'undefined' && !Element.prototype.matches) {\n var proto = Element.prototype;\n\n proto.matches = proto.matchesSelector ||\n proto.mozMatchesSelector ||\n proto.msMatchesSelector ||\n proto.oMatchesSelector ||\n proto.webkitMatchesSelector;\n}\n\n/**\n * Finds the closest parent that matches a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @return {Function}\n */\nfunction closest (element, selector) {\n while (element && element.nodeType !== DOCUMENT_NODE_TYPE) {\n if (typeof element.matches === 'function' &&\n element.matches(selector)) {\n return element;\n }\n element = element.parentNode;\n }\n}\n\nmodule.exports = closest;\n\n\n/***/ }),\n\n/***/ 438:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar closest = __webpack_require__(828);\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction _delegate(element, selector, type, callback, useCapture) {\n var listenerFn = listener.apply(this, arguments);\n\n element.addEventListener(type, listenerFn, useCapture);\n\n return {\n destroy: function() {\n element.removeEventListener(type, listenerFn, useCapture);\n }\n }\n}\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element|String|Array} [elements]\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction delegate(elements, selector, type, callback, useCapture) {\n // Handle the regular Element usage\n if (typeof elements.addEventListener === 'function') {\n return _delegate.apply(null, arguments);\n }\n\n // Handle Element-less usage, it defaults to global delegation\n if (typeof type === 'function') {\n // Use `document` as the first parameter, then apply arguments\n // This is a short way to .unshift `arguments` without running into deoptimizations\n return _delegate.bind(null, document).apply(null, arguments);\n }\n\n // Handle Selector-based usage\n if (typeof elements === 'string') {\n elements = document.querySelectorAll(elements);\n }\n\n // Handle Array-like based usage\n return Array.prototype.map.call(elements, function (element) {\n return _delegate(element, selector, type, callback, useCapture);\n });\n}\n\n/**\n * Finds closest match and invokes callback.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Function}\n */\nfunction listener(element, selector, type, callback) {\n return function(e) {\n e.delegateTarget = closest(e.target, selector);\n\n if (e.delegateTarget) {\n callback.call(element, e);\n }\n }\n}\n\nmodule.exports = delegate;\n\n\n/***/ }),\n\n/***/ 879:\n/***/ (function(__unused_webpack_module, exports) {\n\n/**\n * Check if argument is a HTML element.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.node = function(value) {\n return value !== undefined\n && value instanceof HTMLElement\n && value.nodeType === 1;\n};\n\n/**\n * Check if argument is a list of HTML elements.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.nodeList = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return value !== undefined\n && (type === '[object NodeList]' || type === '[object HTMLCollection]')\n && ('length' in value)\n && (value.length === 0 || exports.node(value[0]));\n};\n\n/**\n * Check if argument is a string.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.string = function(value) {\n return typeof value === 'string'\n || value instanceof String;\n};\n\n/**\n * Check if argument is a function.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.fn = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return type === '[object Function]';\n};\n\n\n/***/ }),\n\n/***/ 370:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar is = __webpack_require__(879);\nvar delegate = __webpack_require__(438);\n\n/**\n * Validates all params and calls the right\n * listener function based on its target type.\n *\n * @param {String|HTMLElement|HTMLCollection|NodeList} target\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listen(target, type, callback) {\n if (!target && !type && !callback) {\n throw new Error('Missing required arguments');\n }\n\n if (!is.string(type)) {\n throw new TypeError('Second argument must be a String');\n }\n\n if (!is.fn(callback)) {\n throw new TypeError('Third argument must be a Function');\n }\n\n if (is.node(target)) {\n return listenNode(target, type, callback);\n }\n else if (is.nodeList(target)) {\n return listenNodeList(target, type, callback);\n }\n else if (is.string(target)) {\n return listenSelector(target, type, callback);\n }\n else {\n throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList');\n }\n}\n\n/**\n * Adds an event listener to a HTML element\n * and returns a remove listener function.\n *\n * @param {HTMLElement} node\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNode(node, type, callback) {\n node.addEventListener(type, callback);\n\n return {\n destroy: function() {\n node.removeEventListener(type, callback);\n }\n }\n}\n\n/**\n * Add an event listener to a list of HTML elements\n * and returns a remove listener function.\n *\n * @param {NodeList|HTMLCollection} nodeList\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNodeList(nodeList, type, callback) {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.addEventListener(type, callback);\n });\n\n return {\n destroy: function() {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.removeEventListener(type, callback);\n });\n }\n }\n}\n\n/**\n * Add an event listener to a selector\n * and returns a remove listener function.\n *\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenSelector(selector, type, callback) {\n return delegate(document.body, selector, type, callback);\n}\n\nmodule.exports = listen;\n\n\n/***/ }),\n\n/***/ 817:\n/***/ (function(module) {\n\nfunction select(element) {\n var selectedText;\n\n if (element.nodeName === 'SELECT') {\n element.focus();\n\n selectedText = element.value;\n }\n else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {\n var isReadOnly = element.hasAttribute('readonly');\n\n if (!isReadOnly) {\n element.setAttribute('readonly', '');\n }\n\n element.select();\n element.setSelectionRange(0, element.value.length);\n\n if (!isReadOnly) {\n element.removeAttribute('readonly');\n }\n\n selectedText = element.value;\n }\n else {\n if (element.hasAttribute('contenteditable')) {\n element.focus();\n }\n\n var selection = window.getSelection();\n var range = document.createRange();\n\n range.selectNodeContents(element);\n selection.removeAllRanges();\n selection.addRange(range);\n\n selectedText = selection.toString();\n }\n\n return selectedText;\n}\n\nmodule.exports = select;\n\n\n/***/ }),\n\n/***/ 279:\n/***/ (function(module) {\n\nfunction E () {\n // Keep this empty so it's easier to inherit from\n // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)\n}\n\nE.prototype = {\n on: function (name, callback, ctx) {\n var e = this.e || (this.e = {});\n\n (e[name] || (e[name] = [])).push({\n fn: callback,\n ctx: ctx\n });\n\n return this;\n },\n\n once: function (name, callback, ctx) {\n var self = this;\n function listener () {\n self.off(name, listener);\n callback.apply(ctx, arguments);\n };\n\n listener._ = callback\n return this.on(name, listener, ctx);\n },\n\n emit: function (name) {\n var data = [].slice.call(arguments, 1);\n var evtArr = ((this.e || (this.e = {}))[name] || []).slice();\n var i = 0;\n var len = evtArr.length;\n\n for (i; i < len; i++) {\n evtArr[i].fn.apply(evtArr[i].ctx, data);\n }\n\n return this;\n },\n\n off: function (name, callback) {\n var e = this.e || (this.e = {});\n var evts = e[name];\n var liveEvents = [];\n\n if (evts && callback) {\n for (var i = 0, len = evts.length; i < len; i++) {\n if (evts[i].fn !== callback && evts[i].fn._ !== callback)\n liveEvents.push(evts[i]);\n }\n }\n\n // Remove event from queue to prevent memory leak\n // Suggested by https://github.com/lazd\n // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910\n\n (liveEvents.length)\n ? e[name] = liveEvents\n : delete e[name];\n\n return this;\n }\n};\n\nmodule.exports = E;\nmodule.exports.TinyEmitter = E;\n\n\n/***/ })\n\n/******/ \t});\n/************************************************************************/\n/******/ \t// The module cache\n/******/ \tvar __webpack_module_cache__ = {};\n/******/ \t\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(__webpack_module_cache__[moduleId]) {\n/******/ \t\t\treturn __webpack_module_cache__[moduleId].exports;\n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = __webpack_module_cache__[moduleId] = {\n/******/ \t\t\t// no module.id needed\n/******/ \t\t\t// no module.loaded needed\n/******/ \t\t\texports: {}\n/******/ \t\t};\n/******/ \t\n/******/ \t\t// Execute the module function\n/******/ \t\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n/******/ \t\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/ \t\n/************************************************************************/\n/******/ \t/* webpack/runtime/compat get default export */\n/******/ \t!function() {\n/******/ \t\t// getDefaultExport function for compatibility with non-harmony modules\n/******/ \t\t__webpack_require__.n = function(module) {\n/******/ \t\t\tvar getter = module && module.__esModule ?\n/******/ \t\t\t\tfunction() { return module['default']; } :\n/******/ \t\t\t\tfunction() { return module; };\n/******/ \t\t\t__webpack_require__.d(getter, { a: getter });\n/******/ \t\t\treturn getter;\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/define property getters */\n/******/ \t!function() {\n/******/ \t\t// define getter functions for harmony exports\n/******/ \t\t__webpack_require__.d = function(exports, definition) {\n/******/ \t\t\tfor(var key in definition) {\n/******/ \t\t\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n/******/ \t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n/******/ \t\t\t\t}\n/******/ \t\t\t}\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/hasOwnProperty shorthand */\n/******/ \t!function() {\n/******/ \t\t__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }\n/******/ \t}();\n/******/ \t\n/************************************************************************/\n/******/ \t// module exports must be returned from runtime so entry inlining is disabled\n/******/ \t// startup\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(686);\n/******/ })()\n.default;\n});", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport \"focus-visible\"\n\nimport {\n EMPTY,\n NEVER,\n Observable,\n Subject,\n defer,\n delay,\n filter,\n map,\n merge,\n mergeWith,\n shareReplay,\n switchMap\n} from \"rxjs\"\n\nimport { configuration, feature } from \"./_\"\nimport {\n at,\n getActiveElement,\n getOptionalElement,\n requestJSON,\n setLocation,\n setToggle,\n watchDocument,\n watchKeyboard,\n watchLocation,\n watchLocationTarget,\n watchMedia,\n watchPrint,\n watchScript,\n watchViewport\n} from \"./browser\"\nimport {\n getComponentElement,\n getComponentElements,\n mountAnnounce,\n mountBackToTop,\n mountConsent,\n mountContent,\n mountDialog,\n mountHeader,\n mountHeaderTitle,\n mountPalette,\n mountProgress,\n mountSearch,\n mountSearchHighlight,\n mountSidebar,\n mountSource,\n mountTableOfContents,\n mountTabs,\n watchHeader,\n watchMain\n} from \"./components\"\nimport {\n SearchIndex,\n setupClipboardJS,\n setupInstantNavigation,\n setupVersionSelector\n} from \"./integrations\"\nimport {\n patchEllipsis,\n patchIndeterminate,\n patchScrollfix,\n patchScrolllock\n} from \"./patches\"\nimport \"./polyfills\"\n\n/* ----------------------------------------------------------------------------\n * Functions - @todo refactor\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch search index\n *\n * @returns Search index observable\n */\nfunction fetchSearchIndex(): Observable {\n if (location.protocol === \"file:\") {\n return watchScript(\n `${new URL(\"search/search_index.js\", config.base)}`\n )\n .pipe(\n // @ts-ignore - @todo fix typings\n map(() => __index),\n shareReplay(1)\n )\n } else {\n return requestJSON(\n new URL(\"search/search_index.json\", config.base)\n )\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Application\n * ------------------------------------------------------------------------- */\n\n/* Yay, JavaScript is available */\ndocument.documentElement.classList.remove(\"no-js\")\ndocument.documentElement.classList.add(\"js\")\n\n/* Set up navigation observables and subjects */\nconst document$ = watchDocument()\nconst location$ = watchLocation()\nconst target$ = watchLocationTarget(location$)\nconst keyboard$ = watchKeyboard()\n\n/* Set up media observables */\nconst viewport$ = watchViewport()\nconst tablet$ = watchMedia(\"(min-width: 960px)\")\nconst screen$ = watchMedia(\"(min-width: 1220px)\")\nconst print$ = watchPrint()\n\n/* Retrieve search index, if search is enabled */\nconst config = configuration()\nconst index$ = document.forms.namedItem(\"search\")\n ? fetchSearchIndex()\n : NEVER\n\n/* Set up Clipboard.js integration */\nconst alert$ = new Subject()\nsetupClipboardJS({ alert$ })\n\n/* Set up progress indicator */\nconst progress$ = new Subject()\n\n/* Set up instant navigation, if enabled */\nif (feature(\"navigation.instant\"))\n setupInstantNavigation({ location$, viewport$, progress$ })\n .subscribe(document$)\n\n/* Set up version selector */\nif (config.version?.provider === \"mike\")\n setupVersionSelector({ document$ })\n\n/* Always close drawer and search on navigation */\nmerge(location$, target$)\n .pipe(\n delay(125)\n )\n .subscribe(() => {\n setToggle(\"drawer\", false)\n setToggle(\"search\", false)\n })\n\n/* Set up global keyboard handlers */\nkeyboard$\n .pipe(\n filter(({ mode }) => mode === \"global\")\n )\n .subscribe(key => {\n switch (key.type) {\n\n /* Go to previous page */\n case \"p\":\n case \",\":\n const prev = getOptionalElement(\"link[rel=prev]\")\n if (typeof prev !== \"undefined\")\n setLocation(prev)\n break\n\n /* Go to next page */\n case \"n\":\n case \".\":\n const next = getOptionalElement(\"link[rel=next]\")\n if (typeof next !== \"undefined\")\n setLocation(next)\n break\n\n /* Expand navigation, see https://bit.ly/3ZjG5io */\n case \"Enter\":\n const active = getActiveElement()\n if (active instanceof HTMLLabelElement)\n active.click()\n }\n })\n\n/* Set up patches */\npatchEllipsis({ viewport$, document$ })\npatchIndeterminate({ document$, tablet$ })\npatchScrollfix({ document$ })\npatchScrolllock({ viewport$, tablet$ })\n\n/* Set up header and main area observable */\nconst header$ = watchHeader(getComponentElement(\"header\"), { viewport$ })\nconst main$ = document$\n .pipe(\n map(() => getComponentElement(\"main\")),\n switchMap(el => watchMain(el, { viewport$, header$ })),\n shareReplay(1)\n )\n\n/* Set up control component observables */\nconst control$ = merge(\n\n /* Consent */\n ...getComponentElements(\"consent\")\n .map(el => mountConsent(el, { target$ })),\n\n /* Dialog */\n ...getComponentElements(\"dialog\")\n .map(el => mountDialog(el, { alert$ })),\n\n /* Color palette */\n ...getComponentElements(\"palette\")\n .map(el => mountPalette(el)),\n\n /* Progress bar */\n ...getComponentElements(\"progress\")\n .map(el => mountProgress(el, { progress$ })),\n\n /* Search */\n ...getComponentElements(\"search\")\n .map(el => mountSearch(el, { index$, keyboard$ })),\n\n /* Repository information */\n ...getComponentElements(\"source\")\n .map(el => mountSource(el))\n)\n\n/* Set up content component observables */\nconst content$ = defer(() => merge(\n\n /* Announcement bar */\n ...getComponentElements(\"announce\")\n .map(el => mountAnnounce(el)),\n\n /* Content */\n ...getComponentElements(\"content\")\n .map(el => mountContent(el, { viewport$, target$, print$ })),\n\n /* Search highlighting */\n ...getComponentElements(\"content\")\n .map(el => feature(\"search.highlight\")\n ? mountSearchHighlight(el, { index$, location$ })\n : EMPTY\n ),\n\n /* Header */\n ...getComponentElements(\"header\")\n .map(el => mountHeader(el, { viewport$, header$, main$ })),\n\n /* Header title */\n ...getComponentElements(\"header-title\")\n .map(el => mountHeaderTitle(el, { viewport$, header$ })),\n\n /* Sidebar */\n ...getComponentElements(\"sidebar\")\n .map(el => el.getAttribute(\"data-md-type\") === \"navigation\"\n ? at(screen$, () => mountSidebar(el, { viewport$, header$, main$ }))\n : at(tablet$, () => mountSidebar(el, { viewport$, header$, main$ }))\n ),\n\n /* Navigation tabs */\n ...getComponentElements(\"tabs\")\n .map(el => mountTabs(el, { viewport$, header$ })),\n\n /* Table of contents */\n ...getComponentElements(\"toc\")\n .map(el => mountTableOfContents(el, {\n viewport$, header$, main$, target$\n })),\n\n /* Back-to-top button */\n ...getComponentElements(\"top\")\n .map(el => mountBackToTop(el, { viewport$, header$, main$, target$ }))\n))\n\n/* Set up component observables */\nconst component$ = document$\n .pipe(\n switchMap(() => content$),\n mergeWith(control$),\n shareReplay(1)\n )\n\n/* Subscribe to all components */\ncomponent$.subscribe()\n\n/* ----------------------------------------------------------------------------\n * Exports\n * ------------------------------------------------------------------------- */\n\nwindow.document$ = document$ /* Document observable */\nwindow.location$ = location$ /* Location subject */\nwindow.target$ = target$ /* Location target observable */\nwindow.keyboard$ = keyboard$ /* Keyboard observable */\nwindow.viewport$ = viewport$ /* Viewport observable */\nwindow.tablet$ = tablet$ /* Media tablet observable */\nwindow.screen$ = screen$ /* Media screen observable */\nwindow.print$ = print$ /* Media print observable */\nwindow.alert$ = alert$ /* Alert subject */\nwindow.progress$ = progress$ /* Progress indicator subject */\nwindow.component$ = component$ /* Component observable */\n", "/******************************************************************************\nCopyright (c) Microsoft Corporation.\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\nPERFORMANCE OF THIS SOFTWARE.\n***************************************************************************** */\n/* global Reflect, Promise, SuppressedError, Symbol, Iterator */\n\nvar extendStatics = function(d, b) {\n extendStatics = Object.setPrototypeOf ||\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\n return extendStatics(d, b);\n};\n\nexport function __extends(d, b) {\n if (typeof b !== \"function\" && b !== null)\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\n extendStatics(d, b);\n function __() { this.constructor = d; }\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\n}\n\nexport var __assign = function() {\n __assign = Object.assign || function __assign(t) {\n for (var s, i = 1, n = arguments.length; i < n; i++) {\n s = arguments[i];\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\n }\n return t;\n }\n return __assign.apply(this, arguments);\n}\n\nexport function __rest(s, e) {\n var t = {};\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\n t[p] = s[p];\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\n t[p[i]] = s[p[i]];\n }\n return t;\n}\n\nexport function __decorate(decorators, target, key, desc) {\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\n return c > 3 && r && Object.defineProperty(target, key, r), r;\n}\n\nexport function __param(paramIndex, decorator) {\n return function (target, key) { decorator(target, key, paramIndex); }\n}\n\nexport function __esDecorate(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {\n function accept(f) { if (f !== void 0 && typeof f !== \"function\") throw new TypeError(\"Function expected\"); return f; }\n var kind = contextIn.kind, key = kind === \"getter\" ? \"get\" : kind === \"setter\" ? \"set\" : \"value\";\n var target = !descriptorIn && ctor ? contextIn[\"static\"] ? ctor : ctor.prototype : null;\n var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});\n var _, done = false;\n for (var i = decorators.length - 1; i >= 0; i--) {\n var context = {};\n for (var p in contextIn) context[p] = p === \"access\" ? {} : contextIn[p];\n for (var p in contextIn.access) context.access[p] = contextIn.access[p];\n context.addInitializer = function (f) { if (done) throw new TypeError(\"Cannot add initializers after decoration has completed\"); extraInitializers.push(accept(f || null)); };\n var result = (0, decorators[i])(kind === \"accessor\" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);\n if (kind === \"accessor\") {\n if (result === void 0) continue;\n if (result === null || typeof result !== \"object\") throw new TypeError(\"Object expected\");\n if (_ = accept(result.get)) descriptor.get = _;\n if (_ = accept(result.set)) descriptor.set = _;\n if (_ = accept(result.init)) initializers.unshift(_);\n }\n else if (_ = accept(result)) {\n if (kind === \"field\") initializers.unshift(_);\n else descriptor[key] = _;\n }\n }\n if (target) Object.defineProperty(target, contextIn.name, descriptor);\n done = true;\n};\n\nexport function __runInitializers(thisArg, initializers, value) {\n var useValue = arguments.length > 2;\n for (var i = 0; i < initializers.length; i++) {\n value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);\n }\n return useValue ? value : void 0;\n};\n\nexport function __propKey(x) {\n return typeof x === \"symbol\" ? x : \"\".concat(x);\n};\n\nexport function __setFunctionName(f, name, prefix) {\n if (typeof name === \"symbol\") name = name.description ? \"[\".concat(name.description, \"]\") : \"\";\n return Object.defineProperty(f, \"name\", { configurable: true, value: prefix ? \"\".concat(prefix, \" \", name) : name });\n};\n\nexport function __metadata(metadataKey, metadataValue) {\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\n}\n\nexport function __awaiter(thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n}\n\nexport function __generator(thisArg, body) {\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, tries: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === \"function\" ? Iterator : Object).prototype);\n return g.next = verb(0), g[\"throw\"] = verb(1), g[\"return\"] = verb(2), typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\n function verb(n) { return function (v) { return step([n, v]); }; }\n function step(op) {\n if (f) throw new TypeError(\"Generator is already executing.\");\n while (g && (g = 0, op[0] && (_ = 0)), _) try {\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\n if (y = 0, t) op = [op[0] & 2, t.value];\n switch (op[0]) {\n case 0: case 1: t = op; break;\n case 4: _.label++; return { value: op[1], done: false };\n case 5: _.label++; y = op[1]; op = [0]; continue;\n case 7: op = _.ops.pop(); _.tries.pop(); continue;\n default:\n if (!(t = _.tries, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\n if (t[2]) _.ops.pop();\n _.tries.pop(); continue;\n }\n op = body.call(thisArg, _);\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\n }\n}\n\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\n if (k2 === undefined) k2 = k;\n var desc = Object.getOwnPropertyDescriptor(m, k);\n if (!desc || (\"get\" in desc ? !m.__esModule : desc.writable || desc.configurable)) {\n desc = { enumerable: true, get: function() { return m[k]; } };\n }\n Object.defineProperty(o, k2, desc);\n}) : (function(o, m, k, k2) {\n if (k2 === undefined) k2 = k;\n o[k2] = m[k];\n});\n\nexport function __exportStar(m, o) {\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\n}\n\nexport function __values(o) {\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\n if (m) return m.call(o);\n if (o && typeof o.length === \"number\") return {\n next: function () {\n if (o && i >= o.length) o = void 0;\n return { value: o && o[i++], done: !o };\n }\n };\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\n}\n\nexport function __read(o, n) {\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\n if (!m) return o;\n var i = m.call(o), r, ar = [], e;\n try {\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\n }\n catch (error) { e = { error: error }; }\n finally {\n try {\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\n }\n finally { if (e) throw e.error; }\n }\n return ar;\n}\n\n/** @deprecated */\nexport function __spread() {\n for (var ar = [], i = 0; i < arguments.length; i++)\n ar = ar.concat(__read(arguments[i]));\n return ar;\n}\n\n/** @deprecated */\nexport function __spreadArrays() {\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\n r[k] = a[j];\n return r;\n}\n\nexport function __spreadArray(to, from, pack) {\n if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {\n if (ar || !(i in from)) {\n if (!ar) ar = Array.prototype.slice.call(from, 0, i);\n ar[i] = from[i];\n }\n }\n return to.concat(ar || Array.prototype.slice.call(from));\n}\n\nexport function __await(v) {\n return this instanceof __await ? (this.v = v, this) : new __await(v);\n}\n\nexport function __asyncGenerator(thisArg, _arguments, generator) {\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\n return i = Object.create((typeof AsyncIterator === \"function\" ? AsyncIterator : Object).prototype), verb(\"next\"), verb(\"throw\"), verb(\"return\", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;\n function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }\n function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\n function fulfill(value) { resume(\"next\", value); }\n function reject(value) { resume(\"throw\", value); }\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\n}\n\nexport function __asyncDelegator(o) {\n var i, p;\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: false } : f ? f(v) : v; } : f; }\n}\n\nexport function __asyncValues(o) {\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\n var m = o[Symbol.asyncIterator], i;\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\n}\n\nexport function __makeTemplateObject(cooked, raw) {\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\n return cooked;\n};\n\nvar __setModuleDefault = Object.create ? (function(o, v) {\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\n}) : function(o, v) {\n o[\"default\"] = v;\n};\n\nexport function __importStar(mod) {\n if (mod && mod.__esModule) return mod;\n var result = {};\n if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\n __setModuleDefault(result, mod);\n return result;\n}\n\nexport function __importDefault(mod) {\n return (mod && mod.__esModule) ? mod : { default: mod };\n}\n\nexport function __classPrivateFieldGet(receiver, state, kind, f) {\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\n return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\n}\n\nexport function __classPrivateFieldSet(receiver, state, value, kind, f) {\n if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\n return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\n}\n\nexport function __classPrivateFieldIn(state, receiver) {\n if (receiver === null || (typeof receiver !== \"object\" && typeof receiver !== \"function\")) throw new TypeError(\"Cannot use 'in' operator on non-object\");\n return typeof state === \"function\" ? receiver === state : state.has(receiver);\n}\n\nexport function __addDisposableResource(env, value, async) {\n if (value !== null && value !== void 0) {\n if (typeof value !== \"object\" && typeof value !== \"function\") throw new TypeError(\"Object expected.\");\n var dispose, inner;\n if (async) {\n if (!Symbol.asyncDispose) throw new TypeError(\"Symbol.asyncDispose is not defined.\");\n dispose = value[Symbol.asyncDispose];\n }\n if (dispose === void 0) {\n if (!Symbol.dispose) throw new TypeError(\"Symbol.dispose is not defined.\");\n dispose = value[Symbol.dispose];\n if (async) inner = dispose;\n }\n if (typeof dispose !== \"function\") throw new TypeError(\"Object not disposable.\");\n if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };\n env.stack.push({ value: value, dispose: dispose, async: async });\n }\n else if (async) {\n env.stack.push({ async: true });\n }\n return value;\n}\n\nvar _SuppressedError = typeof SuppressedError === \"function\" ? SuppressedError : function (error, suppressed, message) {\n var e = new Error(message);\n return e.name = \"SuppressedError\", e.error = error, e.suppressed = suppressed, e;\n};\n\nexport function __disposeResources(env) {\n function fail(e) {\n env.error = env.hasError ? new _SuppressedError(e, env.error, \"An error was suppressed during disposal.\") : e;\n env.hasError = true;\n }\n var r, s = 0;\n function next() {\n while (r = env.stack.pop()) {\n try {\n if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);\n if (r.dispose) {\n var result = r.dispose.call(r.value);\n if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });\n }\n else s |= 1;\n }\n catch (e) {\n fail(e);\n }\n }\n if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();\n if (env.hasError) throw env.error;\n }\n return next();\n}\n\nexport default {\n __extends,\n __assign,\n __rest,\n __decorate,\n __param,\n __metadata,\n __awaiter,\n __generator,\n __createBinding,\n __exportStar,\n __values,\n __read,\n __spread,\n __spreadArrays,\n __spreadArray,\n __await,\n __asyncGenerator,\n __asyncDelegator,\n __asyncValues,\n __makeTemplateObject,\n __importStar,\n __importDefault,\n __classPrivateFieldGet,\n __classPrivateFieldSet,\n __classPrivateFieldIn,\n __addDisposableResource,\n __disposeResources,\n};\n", "/**\n * Returns true if the object is a function.\n * @param value The value to check\n */\nexport function isFunction(value: any): value is (...args: any[]) => any {\n return typeof value === 'function';\n}\n", "/**\n * Used to create Error subclasses until the community moves away from ES5.\n *\n * This is because compiling from TypeScript down to ES5 has issues with subclassing Errors\n * as well as other built-in types: https://github.com/Microsoft/TypeScript/issues/12123\n *\n * @param createImpl A factory function to create the actual constructor implementation. The returned\n * function should be a named function that calls `_super` internally.\n */\nexport function createErrorClass(createImpl: (_super: any) => any): T {\n const _super = (instance: any) => {\n Error.call(instance);\n instance.stack = new Error().stack;\n };\n\n const ctorFunc = createImpl(_super);\n ctorFunc.prototype = Object.create(Error.prototype);\n ctorFunc.prototype.constructor = ctorFunc;\n return ctorFunc;\n}\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface UnsubscriptionError extends Error {\n readonly errors: any[];\n}\n\nexport interface UnsubscriptionErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (errors: any[]): UnsubscriptionError;\n}\n\n/**\n * An error thrown when one or more errors have occurred during the\n * `unsubscribe` of a {@link Subscription}.\n */\nexport const UnsubscriptionError: UnsubscriptionErrorCtor = createErrorClass(\n (_super) =>\n function UnsubscriptionErrorImpl(this: any, errors: (Error | string)[]) {\n _super(this);\n this.message = errors\n ? `${errors.length} errors occurred during unsubscription:\n${errors.map((err, i) => `${i + 1}) ${err.toString()}`).join('\\n ')}`\n : '';\n this.name = 'UnsubscriptionError';\n this.errors = errors;\n }\n);\n", "/**\n * Removes an item from an array, mutating it.\n * @param arr The array to remove the item from\n * @param item The item to remove\n */\nexport function arrRemove(arr: T[] | undefined | null, item: T) {\n if (arr) {\n const index = arr.indexOf(item);\n 0 <= index && arr.splice(index, 1);\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { UnsubscriptionError } from './util/UnsubscriptionError';\nimport { SubscriptionLike, TeardownLogic, Unsubscribable } from './types';\nimport { arrRemove } from './util/arrRemove';\n\n/**\n * Represents a disposable resource, such as the execution of an Observable. A\n * Subscription has one important method, `unsubscribe`, that takes no argument\n * and just disposes the resource held by the subscription.\n *\n * Additionally, subscriptions may be grouped together through the `add()`\n * method, which will attach a child Subscription to the current Subscription.\n * When a Subscription is unsubscribed, all its children (and its grandchildren)\n * will be unsubscribed as well.\n *\n * @class Subscription\n */\nexport class Subscription implements SubscriptionLike {\n /** @nocollapse */\n public static EMPTY = (() => {\n const empty = new Subscription();\n empty.closed = true;\n return empty;\n })();\n\n /**\n * A flag to indicate whether this Subscription has already been unsubscribed.\n */\n public closed = false;\n\n private _parentage: Subscription[] | Subscription | null = null;\n\n /**\n * The list of registered finalizers to execute upon unsubscription. Adding and removing from this\n * list occurs in the {@link #add} and {@link #remove} methods.\n */\n private _finalizers: Exclude[] | null = null;\n\n /**\n * @param initialTeardown A function executed first as part of the finalization\n * process that is kicked off when {@link #unsubscribe} is called.\n */\n constructor(private initialTeardown?: () => void) {}\n\n /**\n * Disposes the resources held by the subscription. May, for instance, cancel\n * an ongoing Observable execution or cancel any other type of work that\n * started when the Subscription was created.\n * @return {void}\n */\n unsubscribe(): void {\n let errors: any[] | undefined;\n\n if (!this.closed) {\n this.closed = true;\n\n // Remove this from it's parents.\n const { _parentage } = this;\n if (_parentage) {\n this._parentage = null;\n if (Array.isArray(_parentage)) {\n for (const parent of _parentage) {\n parent.remove(this);\n }\n } else {\n _parentage.remove(this);\n }\n }\n\n const { initialTeardown: initialFinalizer } = this;\n if (isFunction(initialFinalizer)) {\n try {\n initialFinalizer();\n } catch (e) {\n errors = e instanceof UnsubscriptionError ? e.errors : [e];\n }\n }\n\n const { _finalizers } = this;\n if (_finalizers) {\n this._finalizers = null;\n for (const finalizer of _finalizers) {\n try {\n execFinalizer(finalizer);\n } catch (err) {\n errors = errors ?? [];\n if (err instanceof UnsubscriptionError) {\n errors = [...errors, ...err.errors];\n } else {\n errors.push(err);\n }\n }\n }\n }\n\n if (errors) {\n throw new UnsubscriptionError(errors);\n }\n }\n }\n\n /**\n * Adds a finalizer to this subscription, so that finalization will be unsubscribed/called\n * when this subscription is unsubscribed. If this subscription is already {@link #closed},\n * because it has already been unsubscribed, then whatever finalizer is passed to it\n * will automatically be executed (unless the finalizer itself is also a closed subscription).\n *\n * Closed Subscriptions cannot be added as finalizers to any subscription. Adding a closed\n * subscription to a any subscription will result in no operation. (A noop).\n *\n * Adding a subscription to itself, or adding `null` or `undefined` will not perform any\n * operation at all. (A noop).\n *\n * `Subscription` instances that are added to this instance will automatically remove themselves\n * if they are unsubscribed. Functions and {@link Unsubscribable} objects that you wish to remove\n * will need to be removed manually with {@link #remove}\n *\n * @param teardown The finalization logic to add to this subscription.\n */\n add(teardown: TeardownLogic): void {\n // Only add the finalizer if it's not undefined\n // and don't add a subscription to itself.\n if (teardown && teardown !== this) {\n if (this.closed) {\n // If this subscription is already closed,\n // execute whatever finalizer is handed to it automatically.\n execFinalizer(teardown);\n } else {\n if (teardown instanceof Subscription) {\n // We don't add closed subscriptions, and we don't add the same subscription\n // twice. Subscription unsubscribe is idempotent.\n if (teardown.closed || teardown._hasParent(this)) {\n return;\n }\n teardown._addParent(this);\n }\n (this._finalizers = this._finalizers ?? []).push(teardown);\n }\n }\n }\n\n /**\n * Checks to see if a this subscription already has a particular parent.\n * This will signal that this subscription has already been added to the parent in question.\n * @param parent the parent to check for\n */\n private _hasParent(parent: Subscription) {\n const { _parentage } = this;\n return _parentage === parent || (Array.isArray(_parentage) && _parentage.includes(parent));\n }\n\n /**\n * Adds a parent to this subscription so it can be removed from the parent if it\n * unsubscribes on it's own.\n *\n * NOTE: THIS ASSUMES THAT {@link _hasParent} HAS ALREADY BEEN CHECKED.\n * @param parent The parent subscription to add\n */\n private _addParent(parent: Subscription) {\n const { _parentage } = this;\n this._parentage = Array.isArray(_parentage) ? (_parentage.push(parent), _parentage) : _parentage ? [_parentage, parent] : parent;\n }\n\n /**\n * Called on a child when it is removed via {@link #remove}.\n * @param parent The parent to remove\n */\n private _removeParent(parent: Subscription) {\n const { _parentage } = this;\n if (_parentage === parent) {\n this._parentage = null;\n } else if (Array.isArray(_parentage)) {\n arrRemove(_parentage, parent);\n }\n }\n\n /**\n * Removes a finalizer from this subscription that was previously added with the {@link #add} method.\n *\n * Note that `Subscription` instances, when unsubscribed, will automatically remove themselves\n * from every other `Subscription` they have been added to. This means that using the `remove` method\n * is not a common thing and should be used thoughtfully.\n *\n * If you add the same finalizer instance of a function or an unsubscribable object to a `Subscription` instance\n * more than once, you will need to call `remove` the same number of times to remove all instances.\n *\n * All finalizer instances are removed to free up memory upon unsubscription.\n *\n * @param teardown The finalizer to remove from this subscription\n */\n remove(teardown: Exclude): void {\n const { _finalizers } = this;\n _finalizers && arrRemove(_finalizers, teardown);\n\n if (teardown instanceof Subscription) {\n teardown._removeParent(this);\n }\n }\n}\n\nexport const EMPTY_SUBSCRIPTION = Subscription.EMPTY;\n\nexport function isSubscription(value: any): value is Subscription {\n return (\n value instanceof Subscription ||\n (value && 'closed' in value && isFunction(value.remove) && isFunction(value.add) && isFunction(value.unsubscribe))\n );\n}\n\nfunction execFinalizer(finalizer: Unsubscribable | (() => void)) {\n if (isFunction(finalizer)) {\n finalizer();\n } else {\n finalizer.unsubscribe();\n }\n}\n", "import { Subscriber } from './Subscriber';\nimport { ObservableNotification } from './types';\n\n/**\n * The {@link GlobalConfig} object for RxJS. It is used to configure things\n * like how to react on unhandled errors.\n */\nexport const config: GlobalConfig = {\n onUnhandledError: null,\n onStoppedNotification: null,\n Promise: undefined,\n useDeprecatedSynchronousErrorHandling: false,\n useDeprecatedNextContext: false,\n};\n\n/**\n * The global configuration object for RxJS, used to configure things\n * like how to react on unhandled errors. Accessible via {@link config}\n * object.\n */\nexport interface GlobalConfig {\n /**\n * A registration point for unhandled errors from RxJS. These are errors that\n * cannot were not handled by consuming code in the usual subscription path. For\n * example, if you have this configured, and you subscribe to an observable without\n * providing an error handler, errors from that subscription will end up here. This\n * will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onUnhandledError: ((err: any) => void) | null;\n\n /**\n * A registration point for notifications that cannot be sent to subscribers because they\n * have completed, errored or have been explicitly unsubscribed. By default, next, complete\n * and error notifications sent to stopped subscribers are noops. However, sometimes callers\n * might want a different behavior. For example, with sources that attempt to report errors\n * to stopped subscribers, a caller can configure RxJS to throw an unhandled error instead.\n * This will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onStoppedNotification: ((notification: ObservableNotification, subscriber: Subscriber) => void) | null;\n\n /**\n * The promise constructor used by default for {@link Observable#toPromise toPromise} and {@link Observable#forEach forEach}\n * methods.\n *\n * @deprecated As of version 8, RxJS will no longer support this sort of injection of a\n * Promise constructor. If you need a Promise implementation other than native promises,\n * please polyfill/patch Promise as you see appropriate. Will be removed in v8.\n */\n Promise?: PromiseConstructorLike;\n\n /**\n * If true, turns on synchronous error rethrowing, which is a deprecated behavior\n * in v6 and higher. This behavior enables bad patterns like wrapping a subscribe\n * call in a try/catch block. It also enables producer interference, a nasty bug\n * where a multicast can be broken for all observers by a downstream consumer with\n * an unhandled error. DO NOT USE THIS FLAG UNLESS IT'S NEEDED TO BUY TIME\n * FOR MIGRATION REASONS.\n *\n * @deprecated As of version 8, RxJS will no longer support synchronous throwing\n * of unhandled errors. All errors will be thrown on a separate call stack to prevent bad\n * behaviors described above. Will be removed in v8.\n */\n useDeprecatedSynchronousErrorHandling: boolean;\n\n /**\n * If true, enables an as-of-yet undocumented feature from v5: The ability to access\n * `unsubscribe()` via `this` context in `next` functions created in observers passed\n * to `subscribe`.\n *\n * This is being removed because the performance was severely problematic, and it could also cause\n * issues when types other than POJOs are passed to subscribe as subscribers, as they will likely have\n * their `this` context overwritten.\n *\n * @deprecated As of version 8, RxJS will no longer support altering the\n * context of next functions provided as part of an observer to Subscribe. Instead,\n * you will have access to a subscription or a signal or token that will allow you to do things like\n * unsubscribe and test closed status. Will be removed in v8.\n */\n useDeprecatedNextContext: boolean;\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetTimeoutFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearTimeoutFunction = (handle: TimerHandle) => void;\n\ninterface TimeoutProvider {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n delegate:\n | {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n }\n | undefined;\n}\n\nexport const timeoutProvider: TimeoutProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setTimeout(handler: () => void, timeout?: number, ...args) {\n const { delegate } = timeoutProvider;\n if (delegate?.setTimeout) {\n return delegate.setTimeout(handler, timeout, ...args);\n }\n return setTimeout(handler, timeout, ...args);\n },\n clearTimeout(handle) {\n const { delegate } = timeoutProvider;\n return (delegate?.clearTimeout || clearTimeout)(handle as any);\n },\n delegate: undefined,\n};\n", "import { config } from '../config';\nimport { timeoutProvider } from '../scheduler/timeoutProvider';\n\n/**\n * Handles an error on another job either with the user-configured {@link onUnhandledError},\n * or by throwing it on that new job so it can be picked up by `window.onerror`, `process.on('error')`, etc.\n *\n * This should be called whenever there is an error that is out-of-band with the subscription\n * or when an error hits a terminal boundary of the subscription and no error handler was provided.\n *\n * @param err the error to report\n */\nexport function reportUnhandledError(err: any) {\n timeoutProvider.setTimeout(() => {\n const { onUnhandledError } = config;\n if (onUnhandledError) {\n // Execute the user-configured error handler.\n onUnhandledError(err);\n } else {\n // Throw so it is picked up by the runtime's uncaught error mechanism.\n throw err;\n }\n });\n}\n", "/* tslint:disable:no-empty */\nexport function noop() { }\n", "import { CompleteNotification, NextNotification, ErrorNotification } from './types';\n\n/**\n * A completion object optimized for memory use and created to be the\n * same \"shape\" as other notifications in v8.\n * @internal\n */\nexport const COMPLETE_NOTIFICATION = (() => createNotification('C', undefined, undefined) as CompleteNotification)();\n\n/**\n * Internal use only. Creates an optimized error notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function errorNotification(error: any): ErrorNotification {\n return createNotification('E', undefined, error) as any;\n}\n\n/**\n * Internal use only. Creates an optimized next notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function nextNotification(value: T) {\n return createNotification('N', value, undefined) as NextNotification;\n}\n\n/**\n * Ensures that all notifications created internally have the same \"shape\" in v8.\n *\n * TODO: This is only exported to support a crazy legacy test in `groupBy`.\n * @internal\n */\nexport function createNotification(kind: 'N' | 'E' | 'C', value: any, error: any) {\n return {\n kind,\n value,\n error,\n };\n}\n", "import { config } from '../config';\n\nlet context: { errorThrown: boolean; error: any } | null = null;\n\n/**\n * Handles dealing with errors for super-gross mode. Creates a context, in which\n * any synchronously thrown errors will be passed to {@link captureError}. Which\n * will record the error such that it will be rethrown after the call back is complete.\n * TODO: Remove in v8\n * @param cb An immediately executed function.\n */\nexport function errorContext(cb: () => void) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n const isRoot = !context;\n if (isRoot) {\n context = { errorThrown: false, error: null };\n }\n cb();\n if (isRoot) {\n const { errorThrown, error } = context!;\n context = null;\n if (errorThrown) {\n throw error;\n }\n }\n } else {\n // This is the general non-deprecated path for everyone that\n // isn't crazy enough to use super-gross mode (useDeprecatedSynchronousErrorHandling)\n cb();\n }\n}\n\n/**\n * Captures errors only in super-gross mode.\n * @param err the error to capture\n */\nexport function captureError(err: any) {\n if (config.useDeprecatedSynchronousErrorHandling && context) {\n context.errorThrown = true;\n context.error = err;\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { Observer, ObservableNotification } from './types';\nimport { isSubscription, Subscription } from './Subscription';\nimport { config } from './config';\nimport { reportUnhandledError } from './util/reportUnhandledError';\nimport { noop } from './util/noop';\nimport { nextNotification, errorNotification, COMPLETE_NOTIFICATION } from './NotificationFactories';\nimport { timeoutProvider } from './scheduler/timeoutProvider';\nimport { captureError } from './util/errorContext';\n\n/**\n * Implements the {@link Observer} interface and extends the\n * {@link Subscription} class. While the {@link Observer} is the public API for\n * consuming the values of an {@link Observable}, all Observers get converted to\n * a Subscriber, in order to provide Subscription-like capabilities such as\n * `unsubscribe`. Subscriber is a common type in RxJS, and crucial for\n * implementing operators, but it is rarely used as a public API.\n *\n * @class Subscriber\n */\nexport class Subscriber extends Subscription implements Observer {\n /**\n * A static factory for a Subscriber, given a (potentially partial) definition\n * of an Observer.\n * @param next The `next` callback of an Observer.\n * @param error The `error` callback of an\n * Observer.\n * @param complete The `complete` callback of an\n * Observer.\n * @return A Subscriber wrapping the (partially defined)\n * Observer represented by the given arguments.\n * @nocollapse\n * @deprecated Do not use. Will be removed in v8. There is no replacement for this\n * method, and there is no reason to be creating instances of `Subscriber` directly.\n * If you have a specific use case, please file an issue.\n */\n static create(next?: (x?: T) => void, error?: (e?: any) => void, complete?: () => void): Subscriber {\n return new SafeSubscriber(next, error, complete);\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected isStopped: boolean = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected destination: Subscriber | Observer; // this `any` is the escape hatch to erase extra type param (e.g. R)\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * There is no reason to directly create an instance of Subscriber. This type is exported for typings reasons.\n */\n constructor(destination?: Subscriber | Observer) {\n super();\n if (destination) {\n this.destination = destination;\n // Automatically chain subscriptions together here.\n // if destination is a Subscription, then it is a Subscriber.\n if (isSubscription(destination)) {\n destination.add(this);\n }\n } else {\n this.destination = EMPTY_OBSERVER;\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `next` from\n * the Observable, with a value. The Observable may call this method 0 or more\n * times.\n * @param {T} [value] The `next` value.\n * @return {void}\n */\n next(value?: T): void {\n if (this.isStopped) {\n handleStoppedNotification(nextNotification(value), this);\n } else {\n this._next(value!);\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `error` from\n * the Observable, with an attached `Error`. Notifies the Observer that\n * the Observable has experienced an error condition.\n * @param {any} [err] The `error` exception.\n * @return {void}\n */\n error(err?: any): void {\n if (this.isStopped) {\n handleStoppedNotification(errorNotification(err), this);\n } else {\n this.isStopped = true;\n this._error(err);\n }\n }\n\n /**\n * The {@link Observer} callback to receive a valueless notification of type\n * `complete` from the Observable. Notifies the Observer that the Observable\n * has finished sending push-based notifications.\n * @return {void}\n */\n complete(): void {\n if (this.isStopped) {\n handleStoppedNotification(COMPLETE_NOTIFICATION, this);\n } else {\n this.isStopped = true;\n this._complete();\n }\n }\n\n unsubscribe(): void {\n if (!this.closed) {\n this.isStopped = true;\n super.unsubscribe();\n this.destination = null!;\n }\n }\n\n protected _next(value: T): void {\n this.destination.next(value);\n }\n\n protected _error(err: any): void {\n try {\n this.destination.error(err);\n } finally {\n this.unsubscribe();\n }\n }\n\n protected _complete(): void {\n try {\n this.destination.complete();\n } finally {\n this.unsubscribe();\n }\n }\n}\n\n/**\n * This bind is captured here because we want to be able to have\n * compatibility with monoid libraries that tend to use a method named\n * `bind`. In particular, a library called Monio requires this.\n */\nconst _bind = Function.prototype.bind;\n\nfunction bind any>(fn: Fn, thisArg: any): Fn {\n return _bind.call(fn, thisArg);\n}\n\n/**\n * Internal optimization only, DO NOT EXPOSE.\n * @internal\n */\nclass ConsumerObserver implements Observer {\n constructor(private partialObserver: Partial>) {}\n\n next(value: T): void {\n const { partialObserver } = this;\n if (partialObserver.next) {\n try {\n partialObserver.next(value);\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n\n error(err: any): void {\n const { partialObserver } = this;\n if (partialObserver.error) {\n try {\n partialObserver.error(err);\n } catch (error) {\n handleUnhandledError(error);\n }\n } else {\n handleUnhandledError(err);\n }\n }\n\n complete(): void {\n const { partialObserver } = this;\n if (partialObserver.complete) {\n try {\n partialObserver.complete();\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n}\n\nexport class SafeSubscriber extends Subscriber {\n constructor(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((e?: any) => void) | null,\n complete?: (() => void) | null\n ) {\n super();\n\n let partialObserver: Partial>;\n if (isFunction(observerOrNext) || !observerOrNext) {\n // The first argument is a function, not an observer. The next\n // two arguments *could* be observers, or they could be empty.\n partialObserver = {\n next: (observerOrNext ?? undefined) as (((value: T) => void) | undefined),\n error: error ?? undefined,\n complete: complete ?? undefined,\n };\n } else {\n // The first argument is a partial observer.\n let context: any;\n if (this && config.useDeprecatedNextContext) {\n // This is a deprecated path that made `this.unsubscribe()` available in\n // next handler functions passed to subscribe. This only exists behind a flag\n // now, as it is *very* slow.\n context = Object.create(observerOrNext);\n context.unsubscribe = () => this.unsubscribe();\n partialObserver = {\n next: observerOrNext.next && bind(observerOrNext.next, context),\n error: observerOrNext.error && bind(observerOrNext.error, context),\n complete: observerOrNext.complete && bind(observerOrNext.complete, context),\n };\n } else {\n // The \"normal\" path. Just use the partial observer directly.\n partialObserver = observerOrNext;\n }\n }\n\n // Wrap the partial observer to ensure it's a full observer, and\n // make sure proper error handling is accounted for.\n this.destination = new ConsumerObserver(partialObserver);\n }\n}\n\nfunction handleUnhandledError(error: any) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n captureError(error);\n } else {\n // Ideal path, we report this as an unhandled error,\n // which is thrown on a new call stack.\n reportUnhandledError(error);\n }\n}\n\n/**\n * An error handler used when no error handler was supplied\n * to the SafeSubscriber -- meaning no error handler was supplied\n * do the `subscribe` call on our observable.\n * @param err The error to handle\n */\nfunction defaultErrorHandler(err: any) {\n throw err;\n}\n\n/**\n * A handler for notifications that cannot be sent to a stopped subscriber.\n * @param notification The notification being sent\n * @param subscriber The stopped subscriber\n */\nfunction handleStoppedNotification(notification: ObservableNotification, subscriber: Subscriber) {\n const { onStoppedNotification } = config;\n onStoppedNotification && timeoutProvider.setTimeout(() => onStoppedNotification(notification, subscriber));\n}\n\n/**\n * The observer used as a stub for subscriptions where the user did not\n * pass any arguments to `subscribe`. Comes with the default error handling\n * behavior.\n */\nexport const EMPTY_OBSERVER: Readonly> & { closed: true } = {\n closed: true,\n next: noop,\n error: defaultErrorHandler,\n complete: noop,\n};\n", "/**\n * Symbol.observable or a string \"@@observable\". Used for interop\n *\n * @deprecated We will no longer be exporting this symbol in upcoming versions of RxJS.\n * Instead polyfill and use Symbol.observable directly *or* use https://www.npmjs.com/package/symbol-observable\n */\nexport const observable: string | symbol = (() => (typeof Symbol === 'function' && Symbol.observable) || '@@observable')();\n", "/**\n * This function takes one parameter and just returns it. Simply put,\n * this is like `(x: T): T => x`.\n *\n * ## Examples\n *\n * This is useful in some cases when using things like `mergeMap`\n *\n * ```ts\n * import { interval, take, map, range, mergeMap, identity } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(5));\n *\n * const result$ = source$.pipe(\n * map(i => range(i)),\n * mergeMap(identity) // same as mergeMap(x => x)\n * );\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * Or when you want to selectively apply an operator\n *\n * ```ts\n * import { interval, take, identity } from 'rxjs';\n *\n * const shouldLimit = () => Math.random() < 0.5;\n *\n * const source$ = interval(1000);\n *\n * const result$ = source$.pipe(shouldLimit() ? take(5) : identity);\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * @param x Any value that is returned by this function\n * @returns The value passed as the first parameter to this function\n */\nexport function identity(x: T): T {\n return x;\n}\n", "import { identity } from './identity';\nimport { UnaryFunction } from '../types';\n\nexport function pipe(): typeof identity;\nexport function pipe(fn1: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction, fn3: UnaryFunction): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction,\n ...fns: UnaryFunction[]\n): UnaryFunction;\n\n/**\n * pipe() can be called on one or more functions, each of which can take one argument (\"UnaryFunction\")\n * and uses it to return a value.\n * It returns a function that takes one argument, passes it to the first UnaryFunction, and then\n * passes the result to the next one, passes that result to the next one, and so on. \n */\nexport function pipe(...fns: Array>): UnaryFunction {\n return pipeFromArray(fns);\n}\n\n/** @internal */\nexport function pipeFromArray(fns: Array>): UnaryFunction {\n if (fns.length === 0) {\n return identity as UnaryFunction;\n }\n\n if (fns.length === 1) {\n return fns[0];\n }\n\n return function piped(input: T): R {\n return fns.reduce((prev: any, fn: UnaryFunction) => fn(prev), input as any);\n };\n}\n", "import { Operator } from './Operator';\nimport { SafeSubscriber, Subscriber } from './Subscriber';\nimport { isSubscription, Subscription } from './Subscription';\nimport { TeardownLogic, OperatorFunction, Subscribable, Observer } from './types';\nimport { observable as Symbol_observable } from './symbol/observable';\nimport { pipeFromArray } from './util/pipe';\nimport { config } from './config';\nimport { isFunction } from './util/isFunction';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A representation of any set of values over any amount of time. This is the most basic building block\n * of RxJS.\n *\n * @class Observable\n */\nexport class Observable implements Subscribable {\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n source: Observable | undefined;\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n operator: Operator | undefined;\n\n /**\n * @constructor\n * @param {Function} subscribe the function that is called when the Observable is\n * initially subscribed to. This function is given a Subscriber, to which new values\n * can be `next`ed, or an `error` method can be called to raise an error, or\n * `complete` can be called to notify of a successful completion.\n */\n constructor(subscribe?: (this: Observable, subscriber: Subscriber) => TeardownLogic) {\n if (subscribe) {\n this._subscribe = subscribe;\n }\n }\n\n // HACK: Since TypeScript inherits static properties too, we have to\n // fight against TypeScript here so Subject can have a different static create signature\n /**\n * Creates a new Observable by calling the Observable constructor\n * @owner Observable\n * @method create\n * @param {Function} subscribe? the subscriber function to be passed to the Observable constructor\n * @return {Observable} a new observable\n * @nocollapse\n * @deprecated Use `new Observable()` instead. Will be removed in v8.\n */\n static create: (...args: any[]) => any = (subscribe?: (subscriber: Subscriber) => TeardownLogic) => {\n return new Observable(subscribe);\n };\n\n /**\n * Creates a new Observable, with this Observable instance as the source, and the passed\n * operator defined as the new observable's operator.\n * @method lift\n * @param operator the operator defining the operation to take on the observable\n * @return a new observable with the Operator applied\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * If you have implemented an operator using `lift`, it is recommended that you create an\n * operator by simply returning `new Observable()` directly. See \"Creating new operators from\n * scratch\" section here: https://rxjs.dev/guide/operators\n */\n lift(operator?: Operator): Observable {\n const observable = new Observable();\n observable.source = this;\n observable.operator = operator;\n return observable;\n }\n\n subscribe(observerOrNext?: Partial> | ((value: T) => void)): Subscription;\n /** @deprecated Instead of passing separate callback arguments, use an observer argument. Signatures taking separate callback arguments will be removed in v8. Details: https://rxjs.dev/deprecations/subscribe-arguments */\n subscribe(next?: ((value: T) => void) | null, error?: ((error: any) => void) | null, complete?: (() => void) | null): Subscription;\n /**\n * Invokes an execution of an Observable and registers Observer handlers for notifications it will emit.\n *\n * Use it when you have all these Observables, but still nothing is happening.\n *\n * `subscribe` is not a regular operator, but a method that calls Observable's internal `subscribe` function. It\n * might be for example a function that you passed to Observable's constructor, but most of the time it is\n * a library implementation, which defines what will be emitted by an Observable, and when it be will emitted. This means\n * that calling `subscribe` is actually the moment when Observable starts its work, not when it is created, as it is often\n * the thought.\n *\n * Apart from starting the execution of an Observable, this method allows you to listen for values\n * that an Observable emits, as well as for when it completes or errors. You can achieve this in two\n * of the following ways.\n *\n * The first way is creating an object that implements {@link Observer} interface. It should have methods\n * defined by that interface, but note that it should be just a regular JavaScript object, which you can create\n * yourself in any way you want (ES6 class, classic function constructor, object literal etc.). In particular, do\n * not attempt to use any RxJS implementation details to create Observers - you don't need them. Remember also\n * that your object does not have to implement all methods. If you find yourself creating a method that doesn't\n * do anything, you can simply omit it. Note however, if the `error` method is not provided and an error happens,\n * it will be thrown asynchronously. Errors thrown asynchronously cannot be caught using `try`/`catch`. Instead,\n * use the {@link onUnhandledError} configuration option or use a runtime handler (like `window.onerror` or\n * `process.on('error)`) to be notified of unhandled errors. Because of this, it's recommended that you provide\n * an `error` method to avoid missing thrown errors.\n *\n * The second way is to give up on Observer object altogether and simply provide callback functions in place of its methods.\n * This means you can provide three functions as arguments to `subscribe`, where the first function is equivalent\n * of a `next` method, the second of an `error` method and the third of a `complete` method. Just as in case of an Observer,\n * if you do not need to listen for something, you can omit a function by passing `undefined` or `null`,\n * since `subscribe` recognizes these functions by where they were placed in function call. When it comes\n * to the `error` function, as with an Observer, if not provided, errors emitted by an Observable will be thrown asynchronously.\n *\n * You can, however, subscribe with no parameters at all. This may be the case where you're not interested in terminal events\n * and you also handled emissions internally by using operators (e.g. using `tap`).\n *\n * Whichever style of calling `subscribe` you use, in both cases it returns a Subscription object.\n * This object allows you to call `unsubscribe` on it, which in turn will stop the work that an Observable does and will clean\n * up all resources that an Observable used. Note that cancelling a subscription will not call `complete` callback\n * provided to `subscribe` function, which is reserved for a regular completion signal that comes from an Observable.\n *\n * Remember that callbacks provided to `subscribe` are not guaranteed to be called asynchronously.\n * It is an Observable itself that decides when these functions will be called. For example {@link of}\n * by default emits all its values synchronously. Always check documentation for how given Observable\n * will behave when subscribed and if its default behavior can be modified with a `scheduler`.\n *\n * #### Examples\n *\n * Subscribe with an {@link guide/observer Observer}\n *\n * ```ts\n * import { of } from 'rxjs';\n *\n * const sumObserver = {\n * sum: 0,\n * next(value) {\n * console.log('Adding: ' + value);\n * this.sum = this.sum + value;\n * },\n * error() {\n * // We actually could just remove this method,\n * // since we do not really care about errors right now.\n * },\n * complete() {\n * console.log('Sum equals: ' + this.sum);\n * }\n * };\n *\n * of(1, 2, 3) // Synchronously emits 1, 2, 3 and then completes.\n * .subscribe(sumObserver);\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Subscribe with functions ({@link deprecations/subscribe-arguments deprecated})\n *\n * ```ts\n * import { of } from 'rxjs'\n *\n * let sum = 0;\n *\n * of(1, 2, 3).subscribe(\n * value => {\n * console.log('Adding: ' + value);\n * sum = sum + value;\n * },\n * undefined,\n * () => console.log('Sum equals: ' + sum)\n * );\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Cancel a subscription\n *\n * ```ts\n * import { interval } from 'rxjs';\n *\n * const subscription = interval(1000).subscribe({\n * next(num) {\n * console.log(num)\n * },\n * complete() {\n * // Will not be called, even when cancelling subscription.\n * console.log('completed!');\n * }\n * });\n *\n * setTimeout(() => {\n * subscription.unsubscribe();\n * console.log('unsubscribed!');\n * }, 2500);\n *\n * // Logs:\n * // 0 after 1s\n * // 1 after 2s\n * // 'unsubscribed!' after 2.5s\n * ```\n *\n * @param {Observer|Function} observerOrNext (optional) Either an observer with methods to be called,\n * or the first of three possible handlers, which is the handler for each value emitted from the subscribed\n * Observable.\n * @param {Function} error (optional) A handler for a terminal event resulting from an error. If no error handler is provided,\n * the error will be thrown asynchronously as unhandled.\n * @param {Function} complete (optional) A handler for a terminal event resulting from successful completion.\n * @return {Subscription} a subscription reference to the registered handlers\n * @method subscribe\n */\n subscribe(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((error: any) => void) | null,\n complete?: (() => void) | null\n ): Subscription {\n const subscriber = isSubscriber(observerOrNext) ? observerOrNext : new SafeSubscriber(observerOrNext, error, complete);\n\n errorContext(() => {\n const { operator, source } = this;\n subscriber.add(\n operator\n ? // We're dealing with a subscription in the\n // operator chain to one of our lifted operators.\n operator.call(subscriber, source)\n : source\n ? // If `source` has a value, but `operator` does not, something that\n // had intimate knowledge of our API, like our `Subject`, must have\n // set it. We're going to just call `_subscribe` directly.\n this._subscribe(subscriber)\n : // In all other cases, we're likely wrapping a user-provided initializer\n // function, so we need to catch errors and handle them appropriately.\n this._trySubscribe(subscriber)\n );\n });\n\n return subscriber;\n }\n\n /** @internal */\n protected _trySubscribe(sink: Subscriber): TeardownLogic {\n try {\n return this._subscribe(sink);\n } catch (err) {\n // We don't need to return anything in this case,\n // because it's just going to try to `add()` to a subscription\n // above.\n sink.error(err);\n }\n }\n\n /**\n * Used as a NON-CANCELLABLE means of subscribing to an observable, for use with\n * APIs that expect promises, like `async/await`. You cannot unsubscribe from this.\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * #### Example\n *\n * ```ts\n * import { interval, take } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(4));\n *\n * async function getTotal() {\n * let total = 0;\n *\n * await source$.forEach(value => {\n * total += value;\n * console.log('observable -> ' + value);\n * });\n *\n * return total;\n * }\n *\n * getTotal().then(\n * total => console.log('Total: ' + total)\n * );\n *\n * // Expected:\n * // 'observable -> 0'\n * // 'observable -> 1'\n * // 'observable -> 2'\n * // 'observable -> 3'\n * // 'Total: 6'\n * ```\n *\n * @param next a handler for each value emitted by the observable\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n */\n forEach(next: (value: T) => void): Promise;\n\n /**\n * @param next a handler for each value emitted by the observable\n * @param promiseCtor a constructor function used to instantiate the Promise\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n * @deprecated Passing a Promise constructor will no longer be available\n * in upcoming versions of RxJS. This is because it adds weight to the library, for very\n * little benefit. If you need this functionality, it is recommended that you either\n * polyfill Promise, or you create an adapter to convert the returned native promise\n * to whatever promise implementation you wanted. Will be removed in v8.\n */\n forEach(next: (value: T) => void, promiseCtor: PromiseConstructorLike): Promise;\n\n forEach(next: (value: T) => void, promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n const subscriber = new SafeSubscriber({\n next: (value) => {\n try {\n next(value);\n } catch (err) {\n reject(err);\n subscriber.unsubscribe();\n }\n },\n error: reject,\n complete: resolve,\n });\n this.subscribe(subscriber);\n }) as Promise;\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): TeardownLogic {\n return this.source?.subscribe(subscriber);\n }\n\n /**\n * An interop point defined by the es7-observable spec https://github.com/zenparsing/es-observable\n * @method Symbol.observable\n * @return {Observable} this instance of the observable\n */\n [Symbol_observable]() {\n return this;\n }\n\n /* tslint:disable:max-line-length */\n pipe(): Observable;\n pipe(op1: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction,\n ...operations: OperatorFunction[]\n ): Observable;\n /* tslint:enable:max-line-length */\n\n /**\n * Used to stitch together functional operators into a chain.\n * @method pipe\n * @return {Observable} the Observable result of all of the operators having\n * been called in the order they were passed in.\n *\n * ## Example\n *\n * ```ts\n * import { interval, filter, map, scan } from 'rxjs';\n *\n * interval(1000)\n * .pipe(\n * filter(x => x % 2 === 0),\n * map(x => x + x),\n * scan((acc, x) => acc + x)\n * )\n * .subscribe(x => console.log(x));\n * ```\n */\n pipe(...operations: OperatorFunction[]): Observable {\n return pipeFromArray(operations)(this);\n }\n\n /* tslint:disable:max-line-length */\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: typeof Promise): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: PromiseConstructorLike): Promise;\n /* tslint:enable:max-line-length */\n\n /**\n * Subscribe to this Observable and get a Promise resolving on\n * `complete` with the last emission (if any).\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * @method toPromise\n * @param [promiseCtor] a constructor function used to instantiate\n * the Promise\n * @return A Promise that resolves with the last value emit, or\n * rejects on an error. If there were no emissions, Promise\n * resolves with undefined.\n * @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise\n */\n toPromise(promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n let value: T | undefined;\n this.subscribe(\n (x: T) => (value = x),\n (err: any) => reject(err),\n () => resolve(value)\n );\n }) as Promise;\n }\n}\n\n/**\n * Decides between a passed promise constructor from consuming code,\n * A default configured promise constructor, and the native promise\n * constructor and returns it. If nothing can be found, it will throw\n * an error.\n * @param promiseCtor The optional promise constructor to passed by consuming code\n */\nfunction getPromiseCtor(promiseCtor: PromiseConstructorLike | undefined) {\n return promiseCtor ?? config.Promise ?? Promise;\n}\n\nfunction isObserver(value: any): value is Observer {\n return value && isFunction(value.next) && isFunction(value.error) && isFunction(value.complete);\n}\n\nfunction isSubscriber(value: any): value is Subscriber {\n return (value && value instanceof Subscriber) || (isObserver(value) && isSubscription(value));\n}\n", "import { Observable } from '../Observable';\nimport { Subscriber } from '../Subscriber';\nimport { OperatorFunction } from '../types';\nimport { isFunction } from './isFunction';\n\n/**\n * Used to determine if an object is an Observable with a lift function.\n */\nexport function hasLift(source: any): source is { lift: InstanceType['lift'] } {\n return isFunction(source?.lift);\n}\n\n/**\n * Creates an `OperatorFunction`. Used to define operators throughout the library in a concise way.\n * @param init The logic to connect the liftedSource to the subscriber at the moment of subscription.\n */\nexport function operate(\n init: (liftedSource: Observable, subscriber: Subscriber) => (() => void) | void\n): OperatorFunction {\n return (source: Observable) => {\n if (hasLift(source)) {\n return source.lift(function (this: Subscriber, liftedSource: Observable) {\n try {\n return init(liftedSource, this);\n } catch (err) {\n this.error(err);\n }\n });\n }\n throw new TypeError('Unable to lift unknown Observable type');\n };\n}\n", "import { Subscriber } from '../Subscriber';\n\n/**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional teardown logic here. This will only be called on teardown if the\n * subscriber itself is not already closed. This is called after all other teardown logic is executed.\n */\nexport function createOperatorSubscriber(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n onFinalize?: () => void\n): Subscriber {\n return new OperatorSubscriber(destination, onNext, onComplete, onError, onFinalize);\n}\n\n/**\n * A generic helper for allowing operators to be created with a Subscriber and\n * use closures to capture necessary state from the operator function itself.\n */\nexport class OperatorSubscriber extends Subscriber {\n /**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional finalization logic here. This will only be called on finalization if the\n * subscriber itself is not already closed. This is called after all other finalization logic is executed.\n * @param shouldUnsubscribe An optional check to see if an unsubscribe call should truly unsubscribe.\n * NOTE: This currently **ONLY** exists to support the strange behavior of {@link groupBy}, where unsubscription\n * to the resulting observable does not actually disconnect from the source if there are active subscriptions\n * to any grouped observable. (DO NOT EXPOSE OR USE EXTERNALLY!!!)\n */\n constructor(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n private onFinalize?: () => void,\n private shouldUnsubscribe?: () => boolean\n ) {\n // It's important - for performance reasons - that all of this class's\n // members are initialized and that they are always initialized in the same\n // order. This will ensure that all OperatorSubscriber instances have the\n // same hidden class in V8. This, in turn, will help keep the number of\n // hidden classes involved in property accesses within the base class as\n // low as possible. If the number of hidden classes involved exceeds four,\n // the property accesses will become megamorphic and performance penalties\n // will be incurred - i.e. inline caches won't be used.\n //\n // The reasons for ensuring all instances have the same hidden class are\n // further discussed in this blog post from Benedikt Meurer:\n // https://benediktmeurer.de/2018/03/23/impact-of-polymorphism-on-component-based-frameworks-like-react/\n super(destination);\n this._next = onNext\n ? function (this: OperatorSubscriber, value: T) {\n try {\n onNext(value);\n } catch (err) {\n destination.error(err);\n }\n }\n : super._next;\n this._error = onError\n ? function (this: OperatorSubscriber, err: any) {\n try {\n onError(err);\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._error;\n this._complete = onComplete\n ? function (this: OperatorSubscriber) {\n try {\n onComplete();\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._complete;\n }\n\n unsubscribe() {\n if (!this.shouldUnsubscribe || this.shouldUnsubscribe()) {\n const { closed } = this;\n super.unsubscribe();\n // Execute additional teardown if we have any and we didn't already do so.\n !closed && this.onFinalize?.();\n }\n }\n}\n", "import { Subscription } from '../Subscription';\n\ninterface AnimationFrameProvider {\n schedule(callback: FrameRequestCallback): Subscription;\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n delegate:\n | {\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n }\n | undefined;\n}\n\nexport const animationFrameProvider: AnimationFrameProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n schedule(callback) {\n let request = requestAnimationFrame;\n let cancel: typeof cancelAnimationFrame | undefined = cancelAnimationFrame;\n const { delegate } = animationFrameProvider;\n if (delegate) {\n request = delegate.requestAnimationFrame;\n cancel = delegate.cancelAnimationFrame;\n }\n const handle = request((timestamp) => {\n // Clear the cancel function. The request has been fulfilled, so\n // attempting to cancel the request upon unsubscription would be\n // pointless.\n cancel = undefined;\n callback(timestamp);\n });\n return new Subscription(() => cancel?.(handle));\n },\n requestAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.requestAnimationFrame || requestAnimationFrame)(...args);\n },\n cancelAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.cancelAnimationFrame || cancelAnimationFrame)(...args);\n },\n delegate: undefined,\n};\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface ObjectUnsubscribedError extends Error {}\n\nexport interface ObjectUnsubscribedErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (): ObjectUnsubscribedError;\n}\n\n/**\n * An error thrown when an action is invalid because the object has been\n * unsubscribed.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n *\n * @class ObjectUnsubscribedError\n */\nexport const ObjectUnsubscribedError: ObjectUnsubscribedErrorCtor = createErrorClass(\n (_super) =>\n function ObjectUnsubscribedErrorImpl(this: any) {\n _super(this);\n this.name = 'ObjectUnsubscribedError';\n this.message = 'object unsubscribed';\n }\n);\n", "import { Operator } from './Operator';\nimport { Observable } from './Observable';\nimport { Subscriber } from './Subscriber';\nimport { Subscription, EMPTY_SUBSCRIPTION } from './Subscription';\nimport { Observer, SubscriptionLike, TeardownLogic } from './types';\nimport { ObjectUnsubscribedError } from './util/ObjectUnsubscribedError';\nimport { arrRemove } from './util/arrRemove';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A Subject is a special type of Observable that allows values to be\n * multicasted to many Observers. Subjects are like EventEmitters.\n *\n * Every Subject is an Observable and an Observer. You can subscribe to a\n * Subject, and you can call next to feed values as well as error and complete.\n */\nexport class Subject extends Observable implements SubscriptionLike {\n closed = false;\n\n private currentObservers: Observer[] | null = null;\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n observers: Observer[] = [];\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n isStopped = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n hasError = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n thrownError: any = null;\n\n /**\n * Creates a \"subject\" by basically gluing an observer to an observable.\n *\n * @nocollapse\n * @deprecated Recommended you do not use. Will be removed at some point in the future. Plans for replacement still under discussion.\n */\n static create: (...args: any[]) => any = (destination: Observer, source: Observable): AnonymousSubject => {\n return new AnonymousSubject(destination, source);\n };\n\n constructor() {\n // NOTE: This must be here to obscure Observable's constructor.\n super();\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n lift(operator: Operator): Observable {\n const subject = new AnonymousSubject(this, this);\n subject.operator = operator as any;\n return subject as any;\n }\n\n /** @internal */\n protected _throwIfClosed() {\n if (this.closed) {\n throw new ObjectUnsubscribedError();\n }\n }\n\n next(value: T) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n if (!this.currentObservers) {\n this.currentObservers = Array.from(this.observers);\n }\n for (const observer of this.currentObservers) {\n observer.next(value);\n }\n }\n });\n }\n\n error(err: any) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.hasError = this.isStopped = true;\n this.thrownError = err;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.error(err);\n }\n }\n });\n }\n\n complete() {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.isStopped = true;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.complete();\n }\n }\n });\n }\n\n unsubscribe() {\n this.isStopped = this.closed = true;\n this.observers = this.currentObservers = null!;\n }\n\n get observed() {\n return this.observers?.length > 0;\n }\n\n /** @internal */\n protected _trySubscribe(subscriber: Subscriber): TeardownLogic {\n this._throwIfClosed();\n return super._trySubscribe(subscriber);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._checkFinalizedStatuses(subscriber);\n return this._innerSubscribe(subscriber);\n }\n\n /** @internal */\n protected _innerSubscribe(subscriber: Subscriber) {\n const { hasError, isStopped, observers } = this;\n if (hasError || isStopped) {\n return EMPTY_SUBSCRIPTION;\n }\n this.currentObservers = null;\n observers.push(subscriber);\n return new Subscription(() => {\n this.currentObservers = null;\n arrRemove(observers, subscriber);\n });\n }\n\n /** @internal */\n protected _checkFinalizedStatuses(subscriber: Subscriber) {\n const { hasError, thrownError, isStopped } = this;\n if (hasError) {\n subscriber.error(thrownError);\n } else if (isStopped) {\n subscriber.complete();\n }\n }\n\n /**\n * Creates a new Observable with this Subject as the source. You can do this\n * to create custom Observer-side logic of the Subject and conceal it from\n * code that uses the Observable.\n * @return {Observable} Observable that the Subject casts to\n */\n asObservable(): Observable {\n const observable: any = new Observable();\n observable.source = this;\n return observable;\n }\n}\n\n/**\n * @class AnonymousSubject\n */\nexport class AnonymousSubject extends Subject {\n constructor(\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n public destination?: Observer,\n source?: Observable\n ) {\n super();\n this.source = source;\n }\n\n next(value: T) {\n this.destination?.next?.(value);\n }\n\n error(err: any) {\n this.destination?.error?.(err);\n }\n\n complete() {\n this.destination?.complete?.();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n return this.source?.subscribe(subscriber) ?? EMPTY_SUBSCRIPTION;\n }\n}\n", "import { Subject } from './Subject';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\n\n/**\n * A variant of Subject that requires an initial value and emits its current\n * value whenever it is subscribed to.\n *\n * @class BehaviorSubject\n */\nexport class BehaviorSubject extends Subject {\n constructor(private _value: T) {\n super();\n }\n\n get value(): T {\n return this.getValue();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n const subscription = super._subscribe(subscriber);\n !subscription.closed && subscriber.next(this._value);\n return subscription;\n }\n\n getValue(): T {\n const { hasError, thrownError, _value } = this;\n if (hasError) {\n throw thrownError;\n }\n this._throwIfClosed();\n return _value;\n }\n\n next(value: T): void {\n super.next((this._value = value));\n }\n}\n", "import { TimestampProvider } from '../types';\n\ninterface DateTimestampProvider extends TimestampProvider {\n delegate: TimestampProvider | undefined;\n}\n\nexport const dateTimestampProvider: DateTimestampProvider = {\n now() {\n // Use the variable rather than `this` so that the function can be called\n // without being bound to the provider.\n return (dateTimestampProvider.delegate || Date).now();\n },\n delegate: undefined,\n};\n", "import { Subject } from './Subject';\nimport { TimestampProvider } from './types';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * A variant of {@link Subject} that \"replays\" old values to new subscribers by emitting them when they first subscribe.\n *\n * `ReplaySubject` has an internal buffer that will store a specified number of values that it has observed. Like `Subject`,\n * `ReplaySubject` \"observes\" values by having them passed to its `next` method. When it observes a value, it will store that\n * value for a time determined by the configuration of the `ReplaySubject`, as passed to its constructor.\n *\n * When a new subscriber subscribes to the `ReplaySubject` instance, it will synchronously emit all values in its buffer in\n * a First-In-First-Out (FIFO) manner. The `ReplaySubject` will also complete, if it has observed completion; and it will\n * error if it has observed an error.\n *\n * There are two main configuration items to be concerned with:\n *\n * 1. `bufferSize` - This will determine how many items are stored in the buffer, defaults to infinite.\n * 2. `windowTime` - The amount of time to hold a value in the buffer before removing it from the buffer.\n *\n * Both configurations may exist simultaneously. So if you would like to buffer a maximum of 3 values, as long as the values\n * are less than 2 seconds old, you could do so with a `new ReplaySubject(3, 2000)`.\n *\n * ### Differences with BehaviorSubject\n *\n * `BehaviorSubject` is similar to `new ReplaySubject(1)`, with a couple of exceptions:\n *\n * 1. `BehaviorSubject` comes \"primed\" with a single value upon construction.\n * 2. `ReplaySubject` will replay values, even after observing an error, where `BehaviorSubject` will not.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n * @see {@link shareReplay}\n */\nexport class ReplaySubject extends Subject {\n private _buffer: (T | number)[] = [];\n private _infiniteTimeWindow = true;\n\n /**\n * @param bufferSize The size of the buffer to replay on subscription\n * @param windowTime The amount of time the buffered items will stay buffered\n * @param timestampProvider An object with a `now()` method that provides the current timestamp. This is used to\n * calculate the amount of time something has been buffered.\n */\n constructor(\n private _bufferSize = Infinity,\n private _windowTime = Infinity,\n private _timestampProvider: TimestampProvider = dateTimestampProvider\n ) {\n super();\n this._infiniteTimeWindow = _windowTime === Infinity;\n this._bufferSize = Math.max(1, _bufferSize);\n this._windowTime = Math.max(1, _windowTime);\n }\n\n next(value: T): void {\n const { isStopped, _buffer, _infiniteTimeWindow, _timestampProvider, _windowTime } = this;\n if (!isStopped) {\n _buffer.push(value);\n !_infiniteTimeWindow && _buffer.push(_timestampProvider.now() + _windowTime);\n }\n this._trimBuffer();\n super.next(value);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._trimBuffer();\n\n const subscription = this._innerSubscribe(subscriber);\n\n const { _infiniteTimeWindow, _buffer } = this;\n // We use a copy here, so reentrant code does not mutate our array while we're\n // emitting it to a new subscriber.\n const copy = _buffer.slice();\n for (let i = 0; i < copy.length && !subscriber.closed; i += _infiniteTimeWindow ? 1 : 2) {\n subscriber.next(copy[i] as T);\n }\n\n this._checkFinalizedStatuses(subscriber);\n\n return subscription;\n }\n\n private _trimBuffer() {\n const { _bufferSize, _timestampProvider, _buffer, _infiniteTimeWindow } = this;\n // If we don't have an infinite buffer size, and we're over the length,\n // use splice to truncate the old buffer values off. Note that we have to\n // double the size for instances where we're not using an infinite time window\n // because we're storing the values and the timestamps in the same array.\n const adjustedBufferSize = (_infiniteTimeWindow ? 1 : 2) * _bufferSize;\n _bufferSize < Infinity && adjustedBufferSize < _buffer.length && _buffer.splice(0, _buffer.length - adjustedBufferSize);\n\n // Now, if we're not in an infinite time window, remove all values where the time is\n // older than what is allowed.\n if (!_infiniteTimeWindow) {\n const now = _timestampProvider.now();\n let last = 0;\n // Search the array for the first timestamp that isn't expired and\n // truncate the buffer up to that point.\n for (let i = 1; i < _buffer.length && (_buffer[i] as number) <= now; i += 2) {\n last = i;\n }\n last && _buffer.splice(0, last + 1);\n }\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Subscription } from '../Subscription';\nimport { SchedulerAction } from '../types';\n\n/**\n * A unit of work to be executed in a `scheduler`. An action is typically\n * created from within a {@link SchedulerLike} and an RxJS user does not need to concern\n * themselves about creating and manipulating an Action.\n *\n * ```ts\n * class Action extends Subscription {\n * new (scheduler: Scheduler, work: (state?: T) => void);\n * schedule(state?: T, delay: number = 0): Subscription;\n * }\n * ```\n *\n * @class Action\n */\nexport class Action extends Subscription {\n constructor(scheduler: Scheduler, work: (this: SchedulerAction, state?: T) => void) {\n super();\n }\n /**\n * Schedules this action on its parent {@link SchedulerLike} for execution. May be passed\n * some context object, `state`. May happen at some point in the future,\n * according to the `delay` parameter, if specified.\n * @param {T} [state] Some contextual data that the `work` function uses when\n * called by the Scheduler.\n * @param {number} [delay] Time to wait before executing the work, where the\n * time unit is implicit and defined by the Scheduler.\n * @return {void}\n */\n public schedule(state?: T, delay: number = 0): Subscription {\n return this;\n }\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetIntervalFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearIntervalFunction = (handle: TimerHandle) => void;\n\ninterface IntervalProvider {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n delegate:\n | {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n }\n | undefined;\n}\n\nexport const intervalProvider: IntervalProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setInterval(handler: () => void, timeout?: number, ...args) {\n const { delegate } = intervalProvider;\n if (delegate?.setInterval) {\n return delegate.setInterval(handler, timeout, ...args);\n }\n return setInterval(handler, timeout, ...args);\n },\n clearInterval(handle) {\n const { delegate } = intervalProvider;\n return (delegate?.clearInterval || clearInterval)(handle as any);\n },\n delegate: undefined,\n};\n", "import { Action } from './Action';\nimport { SchedulerAction } from '../types';\nimport { Subscription } from '../Subscription';\nimport { AsyncScheduler } from './AsyncScheduler';\nimport { intervalProvider } from './intervalProvider';\nimport { arrRemove } from '../util/arrRemove';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncAction extends Action {\n public id: TimerHandle | undefined;\n public state?: T;\n // @ts-ignore: Property has no initializer and is not definitely assigned\n public delay: number;\n protected pending: boolean = false;\n\n constructor(protected scheduler: AsyncScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (this.closed) {\n return this;\n }\n\n // Always replace the current state with the new state.\n this.state = state;\n\n const id = this.id;\n const scheduler = this.scheduler;\n\n //\n // Important implementation note:\n //\n // Actions only execute once by default, unless rescheduled from within the\n // scheduled callback. This allows us to implement single and repeat\n // actions via the same code path, without adding API surface area, as well\n // as mimic traditional recursion but across asynchronous boundaries.\n //\n // However, JS runtimes and timers distinguish between intervals achieved by\n // serial `setTimeout` calls vs. a single `setInterval` call. An interval of\n // serial `setTimeout` calls can be individually delayed, which delays\n // scheduling the next `setTimeout`, and so on. `setInterval` attempts to\n // guarantee the interval callback will be invoked more precisely to the\n // interval period, regardless of load.\n //\n // Therefore, we use `setInterval` to schedule single and repeat actions.\n // If the action reschedules itself with the same delay, the interval is not\n // canceled. If the action doesn't reschedule, or reschedules with a\n // different delay, the interval will be canceled after scheduled callback\n // execution.\n //\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, delay);\n }\n\n // Set the pending flag indicating that this action has been scheduled, or\n // has recursively rescheduled itself.\n this.pending = true;\n\n this.delay = delay;\n // If this action has already an async Id, don't request a new one.\n this.id = this.id ?? this.requestAsyncId(scheduler, this.id, delay);\n\n return this;\n }\n\n protected requestAsyncId(scheduler: AsyncScheduler, _id?: TimerHandle, delay: number = 0): TimerHandle {\n return intervalProvider.setInterval(scheduler.flush.bind(scheduler, this), delay);\n }\n\n protected recycleAsyncId(_scheduler: AsyncScheduler, id?: TimerHandle, delay: number | null = 0): TimerHandle | undefined {\n // If this action is rescheduled with the same delay time, don't clear the interval id.\n if (delay != null && this.delay === delay && this.pending === false) {\n return id;\n }\n // Otherwise, if the action's delay time is different from the current delay,\n // or the action has been rescheduled before it's executed, clear the interval id\n if (id != null) {\n intervalProvider.clearInterval(id);\n }\n\n return undefined;\n }\n\n /**\n * Immediately executes this action and the `work` it contains.\n * @return {any}\n */\n public execute(state: T, delay: number): any {\n if (this.closed) {\n return new Error('executing a cancelled action');\n }\n\n this.pending = false;\n const error = this._execute(state, delay);\n if (error) {\n return error;\n } else if (this.pending === false && this.id != null) {\n // Dequeue if the action didn't reschedule itself. Don't call\n // unsubscribe(), because the action could reschedule later.\n // For example:\n // ```\n // scheduler.schedule(function doWork(counter) {\n // /* ... I'm a busy worker bee ... */\n // var originalAction = this;\n // /* wait 100ms before rescheduling the action */\n // setTimeout(function () {\n // originalAction.schedule(counter + 1);\n // }, 100);\n // }, 1000);\n // ```\n this.id = this.recycleAsyncId(this.scheduler, this.id, null);\n }\n }\n\n protected _execute(state: T, _delay: number): any {\n let errored: boolean = false;\n let errorValue: any;\n try {\n this.work(state);\n } catch (e) {\n errored = true;\n // HACK: Since code elsewhere is relying on the \"truthiness\" of the\n // return here, we can't have it return \"\" or 0 or false.\n // TODO: Clean this up when we refactor schedulers mid-version-8 or so.\n errorValue = e ? e : new Error('Scheduled action threw falsy error');\n }\n if (errored) {\n this.unsubscribe();\n return errorValue;\n }\n }\n\n unsubscribe() {\n if (!this.closed) {\n const { id, scheduler } = this;\n const { actions } = scheduler;\n\n this.work = this.state = this.scheduler = null!;\n this.pending = false;\n\n arrRemove(actions, this);\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, null);\n }\n\n this.delay = null!;\n super.unsubscribe();\n }\n }\n}\n", "import { Action } from './scheduler/Action';\nimport { Subscription } from './Subscription';\nimport { SchedulerLike, SchedulerAction } from './types';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * An execution context and a data structure to order tasks and schedule their\n * execution. Provides a notion of (potentially virtual) time, through the\n * `now()` getter method.\n *\n * Each unit of work in a Scheduler is called an `Action`.\n *\n * ```ts\n * class Scheduler {\n * now(): number;\n * schedule(work, delay?, state?): Subscription;\n * }\n * ```\n *\n * @class Scheduler\n * @deprecated Scheduler is an internal implementation detail of RxJS, and\n * should not be used directly. Rather, create your own class and implement\n * {@link SchedulerLike}. Will be made internal in v8.\n */\nexport class Scheduler implements SchedulerLike {\n public static now: () => number = dateTimestampProvider.now;\n\n constructor(private schedulerActionCtor: typeof Action, now: () => number = Scheduler.now) {\n this.now = now;\n }\n\n /**\n * A getter method that returns a number representing the current time\n * (at the time this function was called) according to the scheduler's own\n * internal clock.\n * @return {number} A number that represents the current time. May or may not\n * have a relation to wall-clock time. May or may not refer to a time unit\n * (e.g. milliseconds).\n */\n public now: () => number;\n\n /**\n * Schedules a function, `work`, for execution. May happen at some point in\n * the future, according to the `delay` parameter, if specified. May be passed\n * some context object, `state`, which will be passed to the `work` function.\n *\n * The given arguments will be processed an stored as an Action object in a\n * queue of actions.\n *\n * @param {function(state: ?T): ?Subscription} work A function representing a\n * task, or some unit of work to be executed by the Scheduler.\n * @param {number} [delay] Time to wait before executing the work, where the\n * time unit is implicit and defined by the Scheduler itself.\n * @param {T} [state] Some contextual data that the `work` function uses when\n * called by the Scheduler.\n * @return {Subscription} A subscription in order to be able to unsubscribe\n * the scheduled work.\n */\n public schedule(work: (this: SchedulerAction, state?: T) => void, delay: number = 0, state?: T): Subscription {\n return new this.schedulerActionCtor(this, work).schedule(state, delay);\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Action } from './Action';\nimport { AsyncAction } from './AsyncAction';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncScheduler extends Scheduler {\n public actions: Array> = [];\n /**\n * A flag to indicate whether the Scheduler is currently executing a batch of\n * queued actions.\n * @type {boolean}\n * @internal\n */\n public _active: boolean = false;\n /**\n * An internal ID used to track the latest asynchronous task such as those\n * coming from `setTimeout`, `setInterval`, `requestAnimationFrame`, and\n * others.\n * @type {any}\n * @internal\n */\n public _scheduled: TimerHandle | undefined;\n\n constructor(SchedulerAction: typeof Action, now: () => number = Scheduler.now) {\n super(SchedulerAction, now);\n }\n\n public flush(action: AsyncAction): void {\n const { actions } = this;\n\n if (this._active) {\n actions.push(action);\n return;\n }\n\n let error: any;\n this._active = true;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions.shift()!)); // exhaust the scheduler queue\n\n this._active = false;\n\n if (error) {\n while ((action = actions.shift()!)) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\n/**\n *\n * Async Scheduler\n *\n * Schedule task as if you used setTimeout(task, duration)\n *\n * `async` scheduler schedules tasks asynchronously, by putting them on the JavaScript\n * event loop queue. It is best used to delay tasks in time or to schedule tasks repeating\n * in intervals.\n *\n * If you just want to \"defer\" task, that is to perform it right after currently\n * executing synchronous code ends (commonly achieved by `setTimeout(deferredTask, 0)`),\n * better choice will be the {@link asapScheduler} scheduler.\n *\n * ## Examples\n * Use async scheduler to delay task\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * const task = () => console.log('it works!');\n *\n * asyncScheduler.schedule(task, 2000);\n *\n * // After 2 seconds logs:\n * // \"it works!\"\n * ```\n *\n * Use async scheduler to repeat task in intervals\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * function task(state) {\n * console.log(state);\n * this.schedule(state + 1, 1000); // `this` references currently executing Action,\n * // which we reschedule with new state and delay\n * }\n *\n * asyncScheduler.schedule(task, 3000, 0);\n *\n * // Logs:\n * // 0 after 3s\n * // 1 after 4s\n * // 2 after 5s\n * // 3 after 6s\n * ```\n */\n\nexport const asyncScheduler = new AsyncScheduler(AsyncAction);\n\n/**\n * @deprecated Renamed to {@link asyncScheduler}. Will be removed in v8.\n */\nexport const async = asyncScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { Subscription } from '../Subscription';\nimport { QueueScheduler } from './QueueScheduler';\nimport { SchedulerAction } from '../types';\nimport { TimerHandle } from './timerHandle';\n\nexport class QueueAction extends AsyncAction {\n constructor(protected scheduler: QueueScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (delay > 0) {\n return super.schedule(state, delay);\n }\n this.delay = delay;\n this.state = state;\n this.scheduler.flush(this);\n return this;\n }\n\n public execute(state: T, delay: number): any {\n return delay > 0 || this.closed ? super.execute(state, delay) : this._execute(state, delay);\n }\n\n protected requestAsyncId(scheduler: QueueScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n\n if ((delay != null && delay > 0) || (delay == null && this.delay > 0)) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n\n // Otherwise flush the scheduler starting with this action.\n scheduler.flush(this);\n\n // HACK: In the past, this was returning `void`. However, `void` isn't a valid\n // `TimerHandle`, and generally the return value here isn't really used. So the\n // compromise is to return `0` which is both \"falsy\" and a valid `TimerHandle`,\n // as opposed to refactoring every other instanceo of `requestAsyncId`.\n return 0;\n }\n}\n", "import { AsyncScheduler } from './AsyncScheduler';\n\nexport class QueueScheduler extends AsyncScheduler {\n}\n", "import { QueueAction } from './QueueAction';\nimport { QueueScheduler } from './QueueScheduler';\n\n/**\n *\n * Queue Scheduler\n *\n * Put every next task on a queue, instead of executing it immediately\n *\n * `queue` scheduler, when used with delay, behaves the same as {@link asyncScheduler} scheduler.\n *\n * When used without delay, it schedules given task synchronously - executes it right when\n * it is scheduled. However when called recursively, that is when inside the scheduled task,\n * another task is scheduled with queue scheduler, instead of executing immediately as well,\n * that task will be put on a queue and wait for current one to finish.\n *\n * This means that when you execute task with `queue` scheduler, you are sure it will end\n * before any other task scheduled with that scheduler will start.\n *\n * ## Examples\n * Schedule recursively first, then do something\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(() => {\n * queueScheduler.schedule(() => console.log('second')); // will not happen now, but will be put on a queue\n *\n * console.log('first');\n * });\n *\n * // Logs:\n * // \"first\"\n * // \"second\"\n * ```\n *\n * Reschedule itself recursively\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(function(state) {\n * if (state !== 0) {\n * console.log('before', state);\n * this.schedule(state - 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * console.log('after', state);\n * }\n * }, 0, 3);\n *\n * // In scheduler that runs recursively, you would expect:\n * // \"before\", 3\n * // \"before\", 2\n * // \"before\", 1\n * // \"after\", 1\n * // \"after\", 2\n * // \"after\", 3\n *\n * // But with queue it logs:\n * // \"before\", 3\n * // \"after\", 3\n * // \"before\", 2\n * // \"after\", 2\n * // \"before\", 1\n * // \"after\", 1\n * ```\n */\n\nexport const queueScheduler = new QueueScheduler(QueueAction);\n\n/**\n * @deprecated Renamed to {@link queueScheduler}. Will be removed in v8.\n */\nexport const queue = queueScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\nimport { SchedulerAction } from '../types';\nimport { animationFrameProvider } from './animationFrameProvider';\nimport { TimerHandle } from './timerHandle';\n\nexport class AnimationFrameAction extends AsyncAction {\n constructor(protected scheduler: AnimationFrameScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n protected requestAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay is greater than 0, request as an async action.\n if (delay !== null && delay > 0) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n // Push the action to the end of the scheduler queue.\n scheduler.actions.push(this);\n // If an animation frame has already been requested, don't request another\n // one. If an animation frame hasn't been requested yet, request one. Return\n // the current animation frame request id.\n return scheduler._scheduled || (scheduler._scheduled = animationFrameProvider.requestAnimationFrame(() => scheduler.flush(undefined)));\n }\n\n protected recycleAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle | undefined {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n if (delay != null ? delay > 0 : this.delay > 0) {\n return super.recycleAsyncId(scheduler, id, delay);\n }\n // If the scheduler queue has no remaining actions with the same async id,\n // cancel the requested animation frame and set the scheduled flag to\n // undefined so the next AnimationFrameAction will request its own.\n const { actions } = scheduler;\n if (id != null && actions[actions.length - 1]?.id !== id) {\n animationFrameProvider.cancelAnimationFrame(id as number);\n scheduler._scheduled = undefined;\n }\n // Return undefined so the action knows to request a new async id if it's rescheduled.\n return undefined;\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\nexport class AnimationFrameScheduler extends AsyncScheduler {\n public flush(action?: AsyncAction): void {\n this._active = true;\n // The async id that effects a call to flush is stored in _scheduled.\n // Before executing an action, it's necessary to check the action's async\n // id to determine whether it's supposed to be executed in the current\n // flush.\n // Previous implementations of this method used a count to determine this,\n // but that was unsound, as actions that are unsubscribed - i.e. cancelled -\n // are removed from the actions array and that can shift actions that are\n // scheduled to be executed in a subsequent flush into positions at which\n // they are executed within the current flush.\n const flushId = this._scheduled;\n this._scheduled = undefined;\n\n const { actions } = this;\n let error: any;\n action = action || actions.shift()!;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions[0]) && action.id === flushId && actions.shift());\n\n this._active = false;\n\n if (error) {\n while ((action = actions[0]) && action.id === flushId && actions.shift()) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AnimationFrameAction } from './AnimationFrameAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\n\n/**\n *\n * Animation Frame Scheduler\n *\n * Perform task when `window.requestAnimationFrame` would fire\n *\n * When `animationFrame` scheduler is used with delay, it will fall back to {@link asyncScheduler} scheduler\n * behaviour.\n *\n * Without delay, `animationFrame` scheduler can be used to create smooth browser animations.\n * It makes sure scheduled task will happen just before next browser content repaint,\n * thus performing animations as efficiently as possible.\n *\n * ## Example\n * Schedule div height animation\n * ```ts\n * // html:
\n * import { animationFrameScheduler } from 'rxjs';\n *\n * const div = document.querySelector('div');\n *\n * animationFrameScheduler.schedule(function(height) {\n * div.style.height = height + \"px\";\n *\n * this.schedule(height + 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * }, 0, 0);\n *\n * // You will see a div element growing in height\n * ```\n */\n\nexport const animationFrameScheduler = new AnimationFrameScheduler(AnimationFrameAction);\n\n/**\n * @deprecated Renamed to {@link animationFrameScheduler}. Will be removed in v8.\n */\nexport const animationFrame = animationFrameScheduler;\n", "import { Observable } from '../Observable';\nimport { SchedulerLike } from '../types';\n\n/**\n * A simple Observable that emits no items to the Observer and immediately\n * emits a complete notification.\n *\n * Just emits 'complete', and nothing else.\n *\n * ![](empty.png)\n *\n * A simple Observable that only emits the complete notification. It can be used\n * for composing with other Observables, such as in a {@link mergeMap}.\n *\n * ## Examples\n *\n * Log complete notification\n *\n * ```ts\n * import { EMPTY } from 'rxjs';\n *\n * EMPTY.subscribe({\n * next: () => console.log('Next'),\n * complete: () => console.log('Complete!')\n * });\n *\n * // Outputs\n * // Complete!\n * ```\n *\n * Emit the number 7, then complete\n *\n * ```ts\n * import { EMPTY, startWith } from 'rxjs';\n *\n * const result = EMPTY.pipe(startWith(7));\n * result.subscribe(x => console.log(x));\n *\n * // Outputs\n * // 7\n * ```\n *\n * Map and flatten only odd numbers to the sequence `'a'`, `'b'`, `'c'`\n *\n * ```ts\n * import { interval, mergeMap, of, EMPTY } from 'rxjs';\n *\n * const interval$ = interval(1000);\n * const result = interval$.pipe(\n * mergeMap(x => x % 2 === 1 ? of('a', 'b', 'c') : EMPTY),\n * );\n * result.subscribe(x => console.log(x));\n *\n * // Results in the following to the console:\n * // x is equal to the count on the interval, e.g. (0, 1, 2, 3, ...)\n * // x will occur every 1000ms\n * // if x % 2 is equal to 1, print a, b, c (each on its own)\n * // if x % 2 is not equal to 1, nothing will be output\n * ```\n *\n * @see {@link Observable}\n * @see {@link NEVER}\n * @see {@link of}\n * @see {@link throwError}\n */\nexport const EMPTY = new Observable((subscriber) => subscriber.complete());\n\n/**\n * @param scheduler A {@link SchedulerLike} to use for scheduling\n * the emission of the complete notification.\n * @deprecated Replaced with the {@link EMPTY} constant or {@link scheduled} (e.g. `scheduled([], scheduler)`). Will be removed in v8.\n */\nexport function empty(scheduler?: SchedulerLike) {\n return scheduler ? emptyScheduled(scheduler) : EMPTY;\n}\n\nfunction emptyScheduled(scheduler: SchedulerLike) {\n return new Observable((subscriber) => scheduler.schedule(() => subscriber.complete()));\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport function isScheduler(value: any): value is SchedulerLike {\n return value && isFunction(value.schedule);\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\nimport { isScheduler } from './isScheduler';\n\nfunction last(arr: T[]): T | undefined {\n return arr[arr.length - 1];\n}\n\nexport function popResultSelector(args: any[]): ((...args: unknown[]) => unknown) | undefined {\n return isFunction(last(args)) ? args.pop() : undefined;\n}\n\nexport function popScheduler(args: any[]): SchedulerLike | undefined {\n return isScheduler(last(args)) ? args.pop() : undefined;\n}\n\nexport function popNumber(args: any[], defaultValue: number): number {\n return typeof last(args) === 'number' ? args.pop()! : defaultValue;\n}\n", "export const isArrayLike = ((x: any): x is ArrayLike => x && typeof x.length === 'number' && typeof x !== 'function');", "import { isFunction } from \"./isFunction\";\n\n/**\n * Tests to see if the object is \"thennable\".\n * @param value the object to test\n */\nexport function isPromise(value: any): value is PromiseLike {\n return isFunction(value?.then);\n}\n", "import { InteropObservable } from '../types';\nimport { observable as Symbol_observable } from '../symbol/observable';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being Observable (but not necessary an Rx Observable) */\nexport function isInteropObservable(input: any): input is InteropObservable {\n return isFunction(input[Symbol_observable]);\n}\n", "import { isFunction } from './isFunction';\n\nexport function isAsyncIterable(obj: any): obj is AsyncIterable {\n return Symbol.asyncIterator && isFunction(obj?.[Symbol.asyncIterator]);\n}\n", "/**\n * Creates the TypeError to throw if an invalid object is passed to `from` or `scheduled`.\n * @param input The object that was passed.\n */\nexport function createInvalidObservableTypeError(input: any) {\n // TODO: We should create error codes that can be looked up, so this can be less verbose.\n return new TypeError(\n `You provided ${\n input !== null && typeof input === 'object' ? 'an invalid object' : `'${input}'`\n } where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.`\n );\n}\n", "export function getSymbolIterator(): symbol {\n if (typeof Symbol !== 'function' || !Symbol.iterator) {\n return '@@iterator' as any;\n }\n\n return Symbol.iterator;\n}\n\nexport const iterator = getSymbolIterator();\n", "import { iterator as Symbol_iterator } from '../symbol/iterator';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being an Iterable */\nexport function isIterable(input: any): input is Iterable {\n return isFunction(input?.[Symbol_iterator]);\n}\n", "import { ReadableStreamLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport async function* readableStreamLikeToAsyncGenerator(readableStream: ReadableStreamLike): AsyncGenerator {\n const reader = readableStream.getReader();\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) {\n return;\n }\n yield value!;\n }\n } finally {\n reader.releaseLock();\n }\n}\n\nexport function isReadableStreamLike(obj: any): obj is ReadableStreamLike {\n // We don't want to use instanceof checks because they would return\n // false for instances from another Realm, like an