diff --git a/Dockerfile b/Dockerfile index d6257b5..ed6a4b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,7 +33,7 @@ ENV PATH=$PATH:/root/.local/bin # Setup app WORKDIR /app COPY pyproject.toml poetry.lock ./ -RUN poetry install --no-root --no-directory --with peer +RUN poetry install --no-directory --with peer COPY . . RUN make web diff --git a/graphbook/custom_nodes.py b/graphbook/custom_nodes.py index 0fba930..4010d87 100644 --- a/graphbook/custom_nodes.py +++ b/graphbook/custom_nodes.py @@ -20,6 +20,7 @@ Split, SplitNotesByItems, SplitItemField, + Copy, ) from graphbook.resources import Resource, NumberResource, FunctionResource, ListResource, DictResource @@ -33,6 +34,7 @@ Split, SplitNotesByItems, SplitItemField, + Copy, ] BUILT_IN_RESOURCES = [Resource, NumberResource, FunctionResource, ListResource, DictResource] diff --git a/graphbook/dataloading.py b/graphbook/dataloading.py index 14cccd9..687125b 100644 --- a/graphbook/dataloading.py +++ b/graphbook/dataloading.py @@ -1,12 +1,11 @@ from typing import List, Dict, Tuple, Any import queue -import torch -from torch import Tensor +from torch import set_num_threads import torch.multiprocessing as mp import traceback from .utils import MP_WORKER_TIMEOUT -torch.set_num_threads(1) +set_num_threads(1) MAX_RESULT_QUEUE_SIZE = 32 @@ -15,12 +14,12 @@ def do_load( ) -> Tuple[bool, Any]: try: - item, index, note_id = work_queue.get(False) + item, index, note_id, params = work_queue.get(False) except queue.Empty: return True, None try: - output = load_fn(item) + output = load_fn(item, **params) result = (output, index) to_return = (result, note_id) except Exception as e: @@ -156,9 +155,15 @@ def dump_loop( class Dataloader: - def __init__(self, num_workers: int = 1): + def __init__(self, num_workers: int = 1, spawn_method: bool = False): self.num_workers = num_workers - self.manager = mp.Manager() + self.context = mp + if spawn_method: + print( + "Using spawn method is not recommended because it is more error prone. Try to avoid it as much as possible." + ) + self.context = mp.get_context("spawn") + self.manager = self.context.Manager() self._load_queues: Dict[int, mp.Queue] = self.manager.dict() self._dump_queues: Dict[int, mp.Queue] = self.manager.dict() self._load_result_queues: Dict[int, mp.Queue] = self.manager.dict() @@ -171,12 +176,13 @@ def __init__(self, num_workers: int = 1): self._pending_dump_results: List[PendingResult] = self.manager.list( [None for _ in range(num_workers)] ) + self._workers: List[mp.Process] = [] self._loaders: List[mp.Process] = [] self._dumpers: List[mp.Process] = [] self._worker_queue_cycle = 0 - self._close_event: mp.Event = mp.Event() - self._fail_event: mp.Event = mp.Event() + self._close_event: mp.Event = self.context.Event() + self._fail_event: mp.Event = self.context.Event() def _start_workers(self): if len(self._workers) > 0: @@ -184,7 +190,7 @@ def _start_workers(self): self._fail_event.clear() self._close_event.clear() for i in range(self.num_workers): - load_process = mp.Process( + load_process = self.context.Process( target=load_loop, args=( i, @@ -199,7 +205,7 @@ def _start_workers(self): ) load_process.daemon = True load_process.start() - dump_process = mp.Process( + dump_process = self.context.Process( target=dump_loop, args=( i, @@ -292,9 +298,6 @@ def get_all_sizes(self): return sz def clear(self, consumer_id: int | None = None): - # There's a weird issue where queue.empty() evaluates to True even though there are still items in the queue. - # So we instead close the queue because workers should be killed by now and will need to be restarted - # with new queues from the graph. def clear_queue(q: mp.Queue): while not q.empty(): try: @@ -336,9 +339,13 @@ def clear_queue(q: mp.Queue): if consumer_id in self._consumer_dump_fn: del self._consumer_dump_fn[consumer_id] - def put_load(self, items: list, note_id: int, consumer_id: int): + def put_load( + self, items: list, load_fn_params: dict, note_id: int, consumer_id: int + ): for i, item in enumerate(items): - self._load_queues[consumer_id].put((item, i, note_id), block=False) + self._load_queues[consumer_id].put( + (item, i, note_id, load_fn_params), block=False + ) def get_load(self, consumer_id): if consumer_id not in self._load_result_queues: @@ -351,11 +358,6 @@ def get_load(self, consumer_id): if result is None: return None, note_id out, index = result - # https://pytorch.org/docs/stable/multiprocessing.html#sharing-cuda-tensors - if isinstance(out, Tensor): - out_clone = out.clone() - del out - out = out_clone return (out, index), note_id except queue.Empty: return None @@ -395,9 +397,9 @@ def setup_global_dl(dataloader: Dataloader): workers = dataloader -def put_load(items: list, note_id: int, consumer_id: int): +def put_load(items: list, load_fn_params: dict, note_id: int, consumer_id: int): global workers - workers.put_load(items, note_id, consumer_id) + workers.put_load(items, load_fn_params, note_id, consumer_id) def get_load(consumer_id): diff --git a/graphbook/exports.py b/graphbook/exports.py index 4529e92..fb6e915 100644 --- a/graphbook/exports.py +++ b/graphbook/exports.py @@ -9,6 +9,7 @@ "Split": steps.Split, "SplitNotesByItems": steps.SplitNotesByItems, "SplitItemField": steps.SplitItemField, + "Copy": steps.Copy, "DumpJSONL": steps.DumpJSONL, "LoadJSONL": steps.LoadJSONL, } diff --git a/graphbook/main.py b/graphbook/main.py index b74ce5f..81f75e2 100644 --- a/graphbook/main.py +++ b/graphbook/main.py @@ -68,6 +68,11 @@ def get_args(): action="store_true", help="Do not create a sample workflow if the workflow directory does not exist", ) + parser.add_argument( + "--spawn", + action="store_true", + help="Use the spawn start method for multiprocessing", + ) return parser.parse_args() diff --git a/graphbook/processing/web_processor.py b/graphbook/processing/web_processor.py index 87b9836..923b304 100644 --- a/graphbook/processing/web_processor.py +++ b/graphbook/processing/web_processor.py @@ -40,6 +40,7 @@ def __init__( custom_nodes_path: str, close_event: mp.Event, pause_event: mp.Event, + spawn_method: bool, num_workers: int = 1, ): self.cmd_queue = cmd_queue @@ -53,7 +54,7 @@ def __init__( self.custom_nodes_path = custom_nodes_path self.num_workers = num_workers self.steps = {} - self.dataloader = Dataloader(self.num_workers) + self.dataloader = Dataloader(self.num_workers, spawn_method) setup_global_dl(self.dataloader) self.state_client = ProcessorStateClient( server_request_conn, diff --git a/graphbook/state.py b/graphbook/state.py index 4be7d56..78e5070 100644 --- a/graphbook/state.py +++ b/graphbook/state.py @@ -125,7 +125,7 @@ def _update_custom_nodes(self) -> dict: if issubclass(obj, Resource): self.nodes["resources"][name] = obj updated_nodes["resources"][name] = True - + for name, cls in get_steps().items(): self.nodes["steps"][name] = cls updated_nodes["steps"][name] = True @@ -158,6 +158,7 @@ def __init__(self, custom_nodes_path: str, view_manager_queue: mp.Queue): self._node_catalog = NodeCatalog(custom_nodes_path) self._updated_nodes: Dict[str, Dict[str, bool]] = {} self._step_states: Dict[str, Set[StepState]] = {} + self._step_graph = {"child": {}, "parent": {}} def update_state(self, graph: dict, graph_resources: dict): nodes, is_updated = self._node_catalog.get_nodes() @@ -214,9 +215,10 @@ def set_resource_value(resource_id, resource_data): set_resource_value(resource_id, resource_data) # Next, create all steps - steps = {} - queues = {} - step_states = {} + steps: Dict[str, Step] = {} + queues: Dict[str, MultiConsumerStateDictionaryQueue] = {} + step_states: Dict[str, Set[StepState]] = {} + step_graph = {"child": {}, "parent": {}} logger_param_pool = {} for step_id, step_data in graph.items(): step_name = step_data["name"] @@ -240,6 +242,8 @@ def set_resource_value(resource_id, resource_data): queues[step_id] = self._queues[step_id] step_states[step_id] = self._step_states[step_id] step_states[step_id].discard(StepState.EXECUTED_THIS_RUN) + step_graph["parent"][step_id] = self._step_graph["parent"][step_id] + step_graph["child"][step_id] = self._step_graph["child"][step_id] logger_param_pool[id(self._steps[step_id])] = (step_id, step_name) else: try: @@ -252,11 +256,15 @@ def set_resource_value(resource_id, resource_data): step_states[step_id] = set() logger_param_pool[id(step)] = (step_id, step_name) + # Remove old consumers from parents previous_obj = self._steps.get(step_id) if previous_obj is not None: - for parent in previous_obj.parents: - if parent.id in self._queues: - self._queues[parent.id].remove_consumer(id(previous_obj)) + parent_ids = self._step_graph["parent"][previous_obj.id] + for parent_id in parent_ids: + if parent_id in self._queues: + self._queues[parent_id].remove_consumer(id(previous_obj)) + step_graph["parent"][step_id] = set() + step_graph["child"][step_id] = set() # Next, connect the steps for step_id, step_data in graph.items(): @@ -265,27 +273,27 @@ def set_resource_value(resource_id, resource_data): node = input["node"] slot = input["slot"] parent_node = steps[node] - if parent_node not in child_node.parents: - parent_node.set_child(child_node, slot) + step_graph["parent"][child_node.id].add(parent_node.id) + step_graph["child"][parent_node.id].add(child_node.id) # Note: Two objects with non-overlapping lifetimes may have the same id() value. # But in this case, the below child_node object is not overlapping because at # this point, any previous nodes in the graph are still in self._steps queues[parent_node.id].add_consumer(id(child_node), slot) + + # Remove consumers from parents that are not children for step_id in steps: parent_node = steps[step_id] children_ids = [ - id(child) - for label_steps in parent_node.children.values() - for child in label_steps + id(steps[child_id]) for child_id in step_graph["child"][step_id] ] - queues[step_id].remove_except(children_ids) + queues[step_id].remove_all_except(children_ids) def get_parent_iterator(step_id): - step = steps[step_id] p_index = 0 + parents = list(self._step_graph["parent"][step_id]) while True: - yield step.parents[p_index] - p_index = (p_index + 1) % len(step.parents) + yield parents[p_index] + p_index = (p_index + 1) % len(parents) self._parent_iterators = { step_id: get_parent_iterator(step_id) for step_id in steps @@ -300,6 +308,7 @@ def get_parent_iterator(step_id): self._queues = queues self._resource_values = resource_values self._step_states = step_states + self._step_graph = step_graph def create_parent_subgraph(self, step_id: str): new_steps = {} @@ -311,9 +320,8 @@ def create_parent_subgraph(self, step_id: str): continue new_steps[step_id] = self._steps[step_id] - step = self._steps[step_id] - for input in step.parents: - q.append(input.id) + for parent_id in self._step_graph["parent"][step_id]: + q.append(parent_id) return new_steps def get_processing_steps(self, step_id: str = None): @@ -330,9 +338,9 @@ def dfs(step_id): return visited.add(step_id) step = steps[step_id] - for child in step.children.values(): - for c in child: - dfs(c.id) + children = self._step_graph["child"][step_id] + for child_id in children: + dfs(child_id) ordered_steps.append(step) for step_id in steps: @@ -374,12 +382,12 @@ def clear_outputs(self, node_id: str | None = None): del self._resource_values[node_id], self._dict_resources[node_id] def get_input(self, step: Step) -> Note: - num_parents = len(step.parents) + num_parents = len(self._step_graph["parent"][step.id]) i = 0 while i < num_parents: - next_parent = next(self._parent_iterators[step.id]) + next_parent_id = next(self._parent_iterators[step.id]) try: - next_input = self._queues[next_parent.id].dequeue(id(step)) + next_input = self._queues[next_parent_id].dequeue(id(step)) return next_input except StopIteration: i += 1 @@ -402,7 +410,7 @@ def get_output_note(self, step_id: str, pin_id: str, index: int) -> dict: note = internal_list[index] entry.update(data=note.items) return entry - + def handle_prompt_response(self, step_id: str, response: dict) -> bool: step = self._steps.get(step_id) if not isinstance(step, PromptStep): @@ -444,7 +452,7 @@ def remove_consumer(self, consumer_id: int): del self._consumer_idx[consumer_id] del self._consumer_subs[consumer_id] - def remove_except(self, consumer_ids: List[int]): + def remove_all_except(self, consumer_ids: List[int]): self._consumer_idx = { k: v for k, v in self._consumer_idx.items() if k in consumer_ids } diff --git a/graphbook/steps/__init__.py b/graphbook/steps/__init__.py index f2a2bab..5e03d66 100644 --- a/graphbook/steps/__init__.py +++ b/graphbook/steps/__init__.py @@ -9,6 +9,7 @@ Split, SplitNotesByItems, SplitItemField, + Copy ) from .io import LoadJSONL, DumpJSONL @@ -23,6 +24,7 @@ "Split", "SplitNotesByItems", "SplitItemField", + "Copy", "LoadJSONL", "DumpJSONL", ] diff --git a/graphbook/steps/base.py b/graphbook/steps/base.py index 61288f7..8f83f24 100644 --- a/graphbook/steps/base.py +++ b/graphbook/steps/base.py @@ -11,6 +11,7 @@ import graphbook.dataloading as dataloader import warnings import traceback +import copy warnings.simplefilter("default", DeprecationWarning) @@ -26,7 +27,6 @@ class Step: def __init__(self, item_key=None): self.id = None self.item_key = item_key - self.parents = [] self.children = {"out": []} def set_child(self, child: Step, slot_name: str = "out"): @@ -37,23 +37,11 @@ def set_child(self, child: Step, slot_name: str = "out"): child (Step): child step slot_name (str): slot to bind the child to """ - if self not in child.parents: - child.parents.append(self) if slot_name not in self.children: self.children[slot_name] = [] if child not in self.children[slot_name]: self.children[slot_name].append(child) - def remove_children(self): - """ - Removes all children steps - """ - for children in self.children.values(): - for child in children: - if self in child.parents: - child.parents.remove(self) - self.children = {} - def log(self, message: str, type: str = "info"): """ Logs a message @@ -307,8 +295,9 @@ def __init__(self, batch_size, item_key): self.num_loaded_notes = {} self.dumped_item_holders = NoteItemHolders() self.accumulated_items = [[], [], []] - self._c = 0 - self._d = {} + self.load_fn_params = {} + self.dump_fn_params = {} + self.parallelized_load = self.load_fn != BatchStep.load_fn def in_q(self, note: Note | None): """ @@ -324,16 +313,21 @@ def in_q(self, note: Note | None): if items is None: raise ValueError(f"Item key {self.item_key} not found in Note.") - # Load - if hasattr(self, "load_fn"): - note_id = id(note) - if not is_batchable(items): - items = [items] + if not is_batchable(items): + items = [items] - if len(items) > 0: - dataloader.put_load(items, note_id, id(self)) + if len(items) > 0: + # Load + if self.parallelized_load: + note_id = id(note) + dataloader.put_load(items, self.load_fn_params, note_id, id(self)) self.loaded_notes[note_id] = note self.num_loaded_notes[note_id] = len(items) + else: + acc_items, acc_notes, _ = self.accumulated_items + for item in items: + acc_items.append(item) + acc_notes.append(note) def on_clear(self): self.loaded_notes = {} @@ -341,7 +335,30 @@ def on_clear(self): self.dumped_item_holders = NoteItemHolders() self.accumulated_items = [[], [], []] + def get_batch_sync(self, flush: bool = False) -> StepData: + items, notes, _ = self.accumulated_items + if len(items) == 0: + return None + if len(items) < self.batch_size: + if not flush: + return None + + b_items, b_notes, b_completed = [], [], [] + for _ in range(self.batch_size): + item = items.pop(0) + note = notes.pop(0) + b_items.append(item) + b_notes.append(note) + if len(notes) == 0 or note is not notes[0]: + b_completed.append(note) + + batch = (b_items, b_notes, b_completed) + return batch + def get_batch(self, flush: bool = False) -> StepData: + if not self.parallelized_load: + return self.get_batch_sync() + items, notes, completed = self.accumulated_items next_in = dataloader.get_load(id(self)) if next_in is not None: @@ -412,16 +429,19 @@ def load_fn(**args): def handle_batch(self, batch: StepData): loaded, notes, completed = batch - outputs = [l[0] for l in loaded] - indexes = [l[1] for l in loaded] - - items = [] - for note, index in zip(notes, indexes): - item = note.items[self.item_key] - if is_batchable(item): - items.append(item[index]) - else: - items.append(item) + if self.parallelized_load: + outputs = [l[0] for l in loaded] + indexes = [l[1] for l in loaded] + items = [] + for note, index in zip(notes, indexes): + item = note.items[self.item_key] + if is_batchable(item): + items.append(item[index]) + else: + items.append(item) + else: + outputs = None + items = loaded data_dump = self.on_item_batch(outputs, items, notes) if data_dump is not None: @@ -515,6 +535,7 @@ class PromptStep(AsyncStep): This is useful for interactive workflows where data labeling, model evaluation, or any other human input is required. Once the prompt is handled, the execution lifecycle of the Step will proceed, normally. """ + def __init__(self): super().__init__() self._is_awaiting_response = False @@ -551,7 +572,7 @@ def get_prompt(self, note: Note) -> dict: By default, it will return a boolean prompt. If None is returned, the prompt will be skipped on this note. A list of available prompts can be found in ``graphbook.prompts``. - + Args: note (Note): The Note input to display to the user """ @@ -686,3 +707,20 @@ def on_after_item(self, note: Note) -> StepOutput: note.items[self.b_key] = b_items if self.should_delete_original: del note.items[self.item_key] + +class Copy(Step): + """ + Copies the incoming notes to output slots A and B. + The original version will be forwarded to A and an indentical copy will be forwarded to B. + The copy is made with `copy.deepcopy()`. + """ + + RequiresInput = True + Parameters = {} + Category = "Util" + Outputs = ["A", "B"] + def __init__(self): + super().__init__() + + def forward_note(self, note: Note) -> StepOutput: + return {"A": [note], "B": [copy.deepcopy(note)]} diff --git a/graphbook/web.py b/graphbook/web.py index 0f33311..4d79e58 100644 --- a/graphbook/web.py +++ b/graphbook/web.py @@ -554,6 +554,7 @@ async def start(): custom_nodes_path, close_event, pause_event, + args.spawn, args.num_workers, ) try: diff --git a/poetry.lock b/poetry.lock index fbb20d7..27d0466 100644 --- a/poetry.lock +++ b/poetry.lock @@ -139,13 +139,13 @@ frozenlist = ">=1.1.0" [[package]] name = "alabaster" -version = "1.0.0" +version = "0.7.16" description = "A light, configurable Sphinx theme" optional = false -python-versions = ">=3.10" +python-versions = ">=3.9" files = [ - {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"}, - {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, + {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, + {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] [[package]] @@ -540,6 +540,29 @@ files = [ {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] +[[package]] +name = "importlib-metadata" +version = "8.5.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, + {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -793,83 +816,74 @@ typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} [[package]] name = "networkx" -version = "3.4.1" +version = "3.2.1" description = "Python package for creating and manipulating graphs and networks" optional = false -python-versions = ">=3.10" +python-versions = ">=3.9" files = [ - {file = "networkx-3.4.1-py3-none-any.whl", hash = "sha256:e30a87b48c9a6a7cc220e732bffefaee585bdb166d13377734446ce1a0620eed"}, - {file = "networkx-3.4.1.tar.gz", hash = "sha256:f9df45e85b78f5bd010993e897b4f1fdb242c11e015b101bd951e5c0e29982d8"}, + {file = "networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2"}, + {file = "networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6"}, ] [package.extras] -default = ["matplotlib (>=3.7)", "numpy (>=1.24)", "pandas (>=2.0)", "scipy (>=1.10,!=1.11.0,!=1.11.1)"] -developer = ["changelist (==0.5)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] -doc = ["intersphinx-registry", "myst-nb (>=1.1)", "numpydoc (>=1.8.0)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.15)", "sphinx (>=7.3)", "sphinx-gallery (>=0.16)", "texext (>=0.6.7)"] -example = ["cairocffi (>=1.7)", "contextily (>=1.6)", "igraph (>=0.11)", "momepy (>=0.7.2)", "osmnx (>=1.9)", "scikit-learn (>=1.5)", "seaborn (>=0.13)"] -extra = ["lxml (>=4.6)", "pydot (>=3.0.1)", "pygraphviz (>=1.14)", "sympy (>=1.10)"] +default = ["matplotlib (>=3.5)", "numpy (>=1.22)", "pandas (>=1.4)", "scipy (>=1.9,!=1.11.0,!=1.11.1)"] +developer = ["changelist (==0.4)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] +doc = ["nb2plots (>=0.7)", "nbconvert (<7.9)", "numpydoc (>=1.6)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.14)", "sphinx (>=7)", "sphinx-gallery (>=0.14)", "texext (>=0.6.7)"] +extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.11)", "sympy (>=1.10)"] test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] [[package]] name = "numpy" -version = "2.1.2" +version = "2.0.2" description = "Fundamental package for array computing in Python" optional = false -python-versions = ">=3.10" -files = [ - {file = "numpy-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30d53720b726ec36a7f88dc873f0eec8447fbc93d93a8f079dfac2629598d6ee"}, - {file = "numpy-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8d3ca0a72dd8846eb6f7dfe8f19088060fcb76931ed592d29128e0219652884"}, - {file = "numpy-2.1.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:fc44e3c68ff00fd991b59092a54350e6e4911152682b4782f68070985aa9e648"}, - {file = "numpy-2.1.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:7c1c60328bd964b53f8b835df69ae8198659e2b9302ff9ebb7de4e5a5994db3d"}, - {file = "numpy-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cdb606a7478f9ad91c6283e238544451e3a95f30fb5467fbf715964341a8a86"}, - {file = "numpy-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d666cb72687559689e9906197e3bec7b736764df6a2e58ee265e360663e9baf7"}, - {file = "numpy-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c6eef7a2dbd0abfb0d9eaf78b73017dbfd0b54051102ff4e6a7b2980d5ac1a03"}, - {file = "numpy-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:12edb90831ff481f7ef5f6bc6431a9d74dc0e5ff401559a71e5e4611d4f2d466"}, - {file = "numpy-2.1.2-cp310-cp310-win32.whl", hash = "sha256:a65acfdb9c6ebb8368490dbafe83c03c7e277b37e6857f0caeadbbc56e12f4fb"}, - {file = "numpy-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:860ec6e63e2c5c2ee5e9121808145c7bf86c96cca9ad396c0bd3e0f2798ccbe2"}, - {file = "numpy-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b42a1a511c81cc78cbc4539675713bbcf9d9c3913386243ceff0e9429ca892fe"}, - {file = "numpy-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:faa88bc527d0f097abdc2c663cddf37c05a1c2f113716601555249805cf573f1"}, - {file = "numpy-2.1.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:c82af4b2ddd2ee72d1fc0c6695048d457e00b3582ccde72d8a1c991b808bb20f"}, - {file = "numpy-2.1.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:13602b3174432a35b16c4cfb5de9a12d229727c3dd47a6ce35111f2ebdf66ff4"}, - {file = "numpy-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ebec5fd716c5a5b3d8dfcc439be82a8407b7b24b230d0ad28a81b61c2f4659a"}, - {file = "numpy-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2b49c3c0804e8ecb05d59af8386ec2f74877f7ca8fd9c1e00be2672e4d399b1"}, - {file = "numpy-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2cbba4b30bf31ddbe97f1c7205ef976909a93a66bb1583e983adbd155ba72ac2"}, - {file = "numpy-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8e00ea6fc82e8a804433d3e9cedaa1051a1422cb6e443011590c14d2dea59146"}, - {file = "numpy-2.1.2-cp311-cp311-win32.whl", hash = "sha256:5006b13a06e0b38d561fab5ccc37581f23c9511879be7693bd33c7cd15ca227c"}, - {file = "numpy-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:f1eb068ead09f4994dec71c24b2844f1e4e4e013b9629f812f292f04bd1510d9"}, - {file = "numpy-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7bf0a4f9f15b32b5ba53147369e94296f5fffb783db5aacc1be15b4bf72f43b"}, - {file = "numpy-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b1d0fcae4f0949f215d4632be684a539859b295e2d0cb14f78ec231915d644db"}, - {file = "numpy-2.1.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f751ed0a2f250541e19dfca9f1eafa31a392c71c832b6bb9e113b10d050cb0f1"}, - {file = "numpy-2.1.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:bd33f82e95ba7ad632bc57837ee99dba3d7e006536200c4e9124089e1bf42426"}, - {file = "numpy-2.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b8cde4f11f0a975d1fd59373b32e2f5a562ade7cde4f85b7137f3de8fbb29a0"}, - {file = "numpy-2.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d95f286b8244b3649b477ac066c6906fbb2905f8ac19b170e2175d3d799f4df"}, - {file = "numpy-2.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ab4754d432e3ac42d33a269c8567413bdb541689b02d93788af4131018cbf366"}, - {file = "numpy-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e585c8ae871fd38ac50598f4763d73ec5497b0de9a0ab4ef5b69f01c6a046142"}, - {file = "numpy-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9c6c754df29ce6a89ed23afb25550d1c2d5fdb9901d9c67a16e0b16eaf7e2550"}, - {file = "numpy-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:456e3b11cb79ac9946c822a56346ec80275eaf2950314b249b512896c0d2505e"}, - {file = "numpy-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a84498e0d0a1174f2b3ed769b67b656aa5460c92c9554039e11f20a05650f00d"}, - {file = "numpy-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4d6ec0d4222e8ffdab1744da2560f07856421b367928026fb540e1945f2eeeaf"}, - {file = "numpy-2.1.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:259ec80d54999cc34cd1eb8ded513cb053c3bf4829152a2e00de2371bd406f5e"}, - {file = "numpy-2.1.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:675c741d4739af2dc20cd6c6a5c4b7355c728167845e3c6b0e824e4e5d36a6c3"}, - {file = "numpy-2.1.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b2d4e667895cc55e3ff2b56077e4c8a5604361fc21a042845ea3ad67465aa8"}, - {file = "numpy-2.1.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43cca367bf94a14aca50b89e9bc2061683116cfe864e56740e083392f533ce7a"}, - {file = "numpy-2.1.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:76322dcdb16fccf2ac56f99048af32259dcc488d9b7e25b51e5eca5147a3fb98"}, - {file = "numpy-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:32e16a03138cabe0cb28e1007ee82264296ac0983714094380b408097a418cfe"}, - {file = "numpy-2.1.2-cp313-cp313-win32.whl", hash = "sha256:242b39d00e4944431a3cd2db2f5377e15b5785920421993770cddb89992c3f3a"}, - {file = "numpy-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f2ded8d9b6f68cc26f8425eda5d3877b47343e68ca23d0d0846f4d312ecaa445"}, - {file = "numpy-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ffef621c14ebb0188a8633348504a35c13680d6da93ab5cb86f4e54b7e922b5"}, - {file = "numpy-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ad369ed238b1959dfbade9018a740fb9392c5ac4f9b5173f420bd4f37ba1f7a0"}, - {file = "numpy-2.1.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d82075752f40c0ddf57e6e02673a17f6cb0f8eb3f587f63ca1eaab5594da5b17"}, - {file = "numpy-2.1.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:1600068c262af1ca9580a527d43dc9d959b0b1d8e56f8a05d830eea39b7c8af6"}, - {file = "numpy-2.1.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a26ae94658d3ba3781d5e103ac07a876b3e9b29db53f68ed7df432fd033358a8"}, - {file = "numpy-2.1.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13311c2db4c5f7609b462bc0f43d3c465424d25c626d95040f073e30f7570e35"}, - {file = "numpy-2.1.2-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:2abbf905a0b568706391ec6fa15161fad0fb5d8b68d73c461b3c1bab6064dd62"}, - {file = "numpy-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ef444c57d664d35cac4e18c298c47d7b504c66b17c2ea91312e979fcfbdfb08a"}, - {file = "numpy-2.1.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:bdd407c40483463898b84490770199d5714dcc9dd9b792f6c6caccc523c00952"}, - {file = "numpy-2.1.2-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:da65fb46d4cbb75cb417cddf6ba5e7582eb7bb0b47db4b99c9fe5787ce5d91f5"}, - {file = "numpy-2.1.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c193d0b0238638e6fc5f10f1b074a6993cb13b0b431f64079a509d63d3aa8b7"}, - {file = "numpy-2.1.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a7d80b2e904faa63068ead63107189164ca443b42dd1930299e0d1cb041cec2e"}, - {file = "numpy-2.1.2.tar.gz", hash = "sha256:13532a088217fa624c99b843eeb54640de23b3414b14aa66d023805eb731066c"}, +python-versions = ">=3.9" +files = [ + {file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"}, + {file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"}, + {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66"}, + {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b"}, + {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd"}, + {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318"}, + {file = "numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8"}, + {file = "numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326"}, + {file = "numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97"}, + {file = "numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a"}, + {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669"}, + {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951"}, + {file = "numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9"}, + {file = "numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15"}, + {file = "numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4"}, + {file = "numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c"}, + {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692"}, + {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a"}, + {file = "numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c"}, + {file = "numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded"}, + {file = "numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5"}, + {file = "numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729"}, + {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1"}, + {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd"}, + {file = "numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d"}, + {file = "numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d"}, + {file = "numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa"}, + {file = "numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385"}, + {file = "numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78"}, ] [[package]] @@ -1436,37 +1450,73 @@ files = [ [[package]] name = "sphinx" -version = "8.1.3" +version = "7.3.7" description = "Python documentation generator" optional = false -python-versions = ">=3.10" +python-versions = ">=3.9" files = [ - {file = "sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2"}, - {file = "sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927"}, + {file = "sphinx-7.3.7-py3-none-any.whl", hash = "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3"}, + {file = "sphinx-7.3.7.tar.gz", hash = "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc"}, ] [package.dependencies] -alabaster = ">=0.7.14" +alabaster = ">=0.7.14,<0.8.0" +babel = ">=2.9" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +docutils = ">=0.18.1,<0.22" +imagesize = ">=1.3" +Jinja2 = ">=3.0" +packaging = ">=21.0" +Pygments = ">=2.14" +requests = ">=2.25.0" +snowballstemmer = ">=2.0" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = ">=1.1.9" +tomli = {version = ">=2", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=3.5.0)", "importlib_metadata", "mypy (==1.9.0)", "pytest (>=6.0)", "ruff (==0.3.7)", "sphinx-lint", "tomli", "types-docutils", "types-requests"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=6.0)", "setuptools (>=67.0)"] + +[[package]] +name = "sphinx" +version = "7.4.7" +description = "Python documentation generator" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, + {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, +] + +[package.dependencies] +alabaster = ">=0.7.14,<0.8.0" babel = ">=2.13" colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} docutils = ">=0.20,<0.22" imagesize = ">=1.3" +importlib-metadata = {version = ">=6.0", markers = "python_version < \"3.10\""} Jinja2 = ">=3.1" packaging = ">=23.0" Pygments = ">=2.17" requests = ">=2.30.0" snowballstemmer = ">=2.2" -sphinxcontrib-applehelp = ">=1.0.7" -sphinxcontrib-devhelp = ">=1.0.6" -sphinxcontrib-htmlhelp = ">=2.0.6" -sphinxcontrib-jsmath = ">=1.0.1" -sphinxcontrib-qthelp = ">=1.0.6" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" sphinxcontrib-serializinghtml = ">=1.1.9" tomli = {version = ">=2", markers = "python_version < \"3.11\""} [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=6.0)", "mypy (==1.11.1)", "pyright (==1.1.384)", "pytest (>=6.0)", "ruff (==0.6.9)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.18.0.20240506)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241005)", "types-requests (==2.32.0.20240914)", "types-urllib3 (==1.26.25.14)"] +lint = ["flake8 (>=6.0)", "importlib-metadata (>=6.0)", "mypy (==1.10.1)", "pytest (>=6.0)", "ruff (==0.5.2)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-docutils (==0.21.0.20240711)", "types-requests (>=2.30.0)"] test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] [[package]] @@ -1511,6 +1561,21 @@ sphinx = ">=1.2" [package.extras] dev = ["build", "flake8", "pre-commit", "pytest", "sphinx", "tox"] +[[package]] +name = "sphinxawesome-theme" +version = "5.2.0" +description = "An awesome theme for the Sphinx documentation generator" +optional = false +python-versions = "<4.0,>=3.8" +files = [ + {file = "sphinxawesome_theme-5.2.0-py3-none-any.whl", hash = "sha256:56f1462f4ec4a5d66017819de6b7965a394d4157117bf12f029e7818d6789525"}, + {file = "sphinxawesome_theme-5.2.0.tar.gz", hash = "sha256:c24f1e5c0b9e475380d16fc5f1f3bfd84955817da0905d95f7e20809d7cd3c5d"}, +] + +[package.dependencies] +beautifulsoup4 = ">=4.9.1,<5.0.0" +sphinx = {version = ">=7.2,<7.4", markers = "python_version >= \"3.9\" and python_version < \"3.13\""} + [[package]] name = "sphinxawesome-theme" version = "5.3.1" @@ -1524,7 +1589,7 @@ files = [ [package.dependencies] beautifulsoup4 = ">=4.9.1,<5.0.0" -sphinx = {version = ">=8,<9", markers = "python_version >= \"3.10\" and python_version < \"3.13\""} +sphinx = {version = ">=7.2,<7.5", markers = "python_version >= \"3.9\" and python_version < \"3.10\""} [[package]] name = "sphinxcontrib-applehelp" @@ -1960,7 +2025,26 @@ idna = ">=2.0" multidict = ">=4.0" propcache = ">=0.2.0" +[[package]] +name = "zipp" +version = "3.21.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +files = [ + {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, + {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + [metadata] lock-version = "2.0" -python-versions = "^3.10" -content-hash = "7bc615ac38e7a1ead10b0383e91d6645483ef1d19549b5c26cc2ecccfb0d1ac8" +python-versions = "^3.9" +content-hash = "995d2711b1e344a73c17b451b854978d78807fea634198f012edb3c0019218f5" diff --git a/pyproject.toml b/pyproject.toml index 514e32c..5b02cc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "graphbook" -version = "0.9.0" +version = "0.9.1" authors = ["Richard Franklin "] description = "An extensible ML workflow framework built for data scientists and ML engineers." keywords = ["ml", "workflow", "framework", "pytorch", "data science", "machine learning", "ai"] @@ -23,7 +23,7 @@ repository = "https://github.com/graphbookai/graphbook" graphbook = "graphbook.main:main" [tool.poetry.dependencies] -python = "^3.10" +python = "^3.9" aiohttp = "^3.9.4" pillow = "^10.3.0" watchdog = "^4.0.0" diff --git a/web/package.json b/web/package.json index d21172b..244a039 100644 --- a/web/package.json +++ b/web/package.json @@ -1,7 +1,7 @@ { "name": "graphbook-web", "private": true, - "version": "0.9.0", + "version": "0.9.1", "type": "module", "scripts": { "dev": "vite", diff --git a/web/src/components/ContextMenu.tsx b/web/src/components/ContextMenu.tsx index 54a4c27..4c883a6 100644 --- a/web/src/components/ContextMenu.tsx +++ b/web/src/components/ContextMenu.tsx @@ -30,18 +30,21 @@ export function NodeContextMenu({ nodeId, top, left, close }) { name: 'Duplicate', disabled: () => runState !== 'stopped', action: () => { - const { addNodes } = reactFlowInstance; + const { setNodes } = reactFlowInstance; const position = { x: node.position.x + 50, y: node.position.y + 50, }; - addNodes({ - ...node, - selected: false, - dragging: false, - id: `${node.id}-copy`, - position, + setNodes(nodes => { + const copy = { ...node } as any; + delete copy.id; + return Graph.addNode({ + ...copy, + selected: false, + dragging: false, + position + }, nodes); }); } }, diff --git a/web/src/components/Nodes/Node.tsx b/web/src/components/Nodes/Node.tsx index 914d63e..a62869b 100644 --- a/web/src/components/Nodes/Node.tsx +++ b/web/src/components/Nodes/Node.tsx @@ -1,6 +1,6 @@ import React, { CSSProperties, useCallback, useEffect, useMemo, useState } from 'react'; import { Handle, Position, useNodes, useEdges, useReactFlow, useOnSelectionChange } from 'reactflow'; -import { Card, Collapse, Badge, Flex, Button, Image, Tabs, theme, Space, Tooltip } from 'antd'; +import { Card, Collapse, Badge, Flex, Button, Image, Tabs, theme, Space } from 'antd'; import { SearchOutlined, FileTextOutlined, CaretRightOutlined, FileImageOutlined, CodeOutlined } from '@ant-design/icons'; import { Widget, isWidgetType } from './widgets/Widgets'; import { Graph } from '../../graph'; @@ -13,6 +13,7 @@ import { useNotification } from '../../hooks/Notification'; import { useSettings } from '../../hooks/Settings'; import { SerializationErrorMessages } from '../Errors'; import { Prompt } from './widgets/Prompts'; +import { RemovableTooltip } from '../Tooltip'; import ReactJson from '@microlink/react-json-view'; import type { LogEntry, Parameter, ImageRef } from '../../utils'; @@ -38,6 +39,7 @@ export function WorkflowStep({ id, data, selected }) { const notification = useNotification(); const API = useAPI(); const filename = useFilename(); + const [settings, _] = useSettings(); useAPINodeMessage('stats', id, filename, (msg) => { setRecordCount(msg.queue_size || {}); @@ -132,9 +134,9 @@ export function WorkflowStep({ id, data, selected }) { position={Position.Left} id={parameterName} /> - + {parameterName} - + ); } diff --git a/web/src/components/Nodes/widgets/Widgets.tsx b/web/src/components/Nodes/widgets/Widgets.tsx index 75abfae..49fb084 100644 --- a/web/src/components/Nodes/widgets/Widgets.tsx +++ b/web/src/components/Nodes/widgets/Widgets.tsx @@ -1,4 +1,4 @@ -import { Switch, Typography, theme, Flex, Button, Radio, Tooltip, Select as ASelect } from 'antd'; +import { Switch, Typography, theme, Flex, Button, Radio, Select as ASelect } from 'antd'; import { PlusOutlined, MinusOutlined } from '@ant-design/icons'; import React, { useCallback, useState, useMemo } from 'react'; import CodeMirror from '@uiw/react-codemirror'; @@ -8,6 +8,7 @@ import { bbedit } from '@uiw/codemirror-theme-bbedit'; import { Graph } from '../../../graph'; import { useReactFlow } from 'reactflow'; import { usePluginWidgets } from '../../../hooks/Plugins'; +import { RemovableTooltip } from '../../Tooltip'; const { Text } = Typography; const { useToken } = theme; @@ -88,9 +89,9 @@ export function BooleanWidget({ name, def, onChange, style, ...props }) { const { required, description } = props; return ( - + {name} - + {input} ); @@ -167,9 +168,9 @@ export function ListWidget({ name, def, onChange, type, ...props }) { return ( - + {name} - + {def && def.map((item, i) => { return ( @@ -279,9 +280,9 @@ export function DictWidget({ name, def, onChange, ...props }) { return ( - + {name} - + {value && value.map((item, i) => { return ( @@ -414,9 +415,9 @@ function InputNumber({ onChange, label, value, placeholder, description, require return (
{label && - + {label} - + } onInputFocus(true)} @@ -470,9 +471,9 @@ function Input({ onChange, label, value, placeholder, style, description, requir return (
{label && - + {label} - + } onInputFocus(true)} diff --git a/web/src/components/Settings.tsx b/web/src/components/Settings.tsx index 9905b1a..53d562f 100644 --- a/web/src/components/Settings.tsx +++ b/web/src/components/Settings.tsx @@ -50,6 +50,10 @@ export default function Settings() { setClientSetting('quickviewImageHeight', value); }, []); + const disableTooltips = useCallback((event) => { + setClientSetting('disableTooltips', event.target.checked); + }, []); + return (
@@ -62,6 +66,7 @@ export default function Settings() { uncheckedText="Light" onChange={(checked) => { setClientSetting('theme', checked ? "Dark" : "Light") }} /> + Disable Tooltips (improves UI performance) Media Server Settings diff --git a/web/src/components/Tooltip.tsx b/web/src/components/Tooltip.tsx new file mode 100644 index 0000000..a6a348d --- /dev/null +++ b/web/src/components/Tooltip.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { Tooltip } from "antd"; +import { useSettings } from "../hooks/Settings"; + +export function RemovableTooltip({ title, children }: {title: string | undefined, children: any}) { + const [settings] = useSettings(); + if (settings.disableTooltips) { + return children; + } + + return {children}; +} diff --git a/web/src/hooks/Settings.ts b/web/src/hooks/Settings.ts index a6cedbd..5814dcb 100644 --- a/web/src/hooks/Settings.ts +++ b/web/src/hooks/Settings.ts @@ -3,6 +3,7 @@ import { useState, useEffect, useCallback } from "react"; const MONITOR_DATA_COLUMNS = ['stats', 'logs', 'notes', 'images']; let settings = { theme: "Light", + disableTooltips: false, graphServerHost: "localhost:8005", mediaServerHost: "localhost:8006", useExternalMediaServer: false, diff --git a/web/src/utils.ts b/web/src/utils.ts index 03f8663..58032fe 100644 --- a/web/src/utils.ts +++ b/web/src/utils.ts @@ -96,6 +96,11 @@ export const evalDragData = async (reactFlowInstance: ReactFlowInstance, API: Se position: dropPosition, ...data.node }; + Object.values(node.data.parameters).forEach((p) => { + if (p.default !== undefined) { + p.value = p.default; + } + }); setNodes(Graph.addNode(node, nodes)); } else if (data.text) { const id = uniqueIdFrom(nodes);