diff --git a/.github/workflows/pytests.yml b/.github/workflows/pytests.yml index ff2dc32..e736e83 100644 --- a/.github/workflows/pytests.yml +++ b/.github/workflows/pytests.yml @@ -2,7 +2,7 @@ name: Pytest on: push: - branches: [ main ] + branches: [ main, ete4 ] pull_request: branches: [ main ] diff --git a/har2tree/har2tree.py b/har2tree/har2tree.py index 2e27b64..bd47c71 100644 --- a/har2tree/har2tree.py +++ b/har2tree/har2tree.py @@ -312,20 +312,20 @@ def __init__(self, har_path: Path, capture_uuid: str): # Generate cookies lookup tables # All the initial cookies sent with the initial request given to splash self.initial_cookies: dict[str, dict[str, Any]] = {} - if hasattr(self._nodes_list[0], 'cookies_sent'): + if 'cookies_sent' in self._nodes_list[0].features: self.initial_cookies = {key: cookie for key, cookie in self._nodes_list[0].cookies_sent.items()} # Dictionary of all cookies received during the capture self.cookies_received: dict[str, list[tuple[str, URLNode, bool]]] = defaultdict(list) for n in self._nodes_list: - if hasattr(n, 'cookies_received'): + if 'cookies_received' in n.features: for domain, c_received, is_3rd_party in n.cookies_received: self.cookies_received[c_received].append((domain, n, is_3rd_party)) # Dictionary of all cookies sent during the capture self.cookies_sent: dict[str, list[URLNode]] = defaultdict(list) for n in self._nodes_list: - if hasattr(n, 'cookies_sent'): + if 'cookies_sent' in n.features: for c_sent in n.cookies_sent.keys(): self.cookies_sent[c_sent].append(n) @@ -341,7 +341,7 @@ def __init__(self, har_path: Path, capture_uuid: str): self.locally_created_not_sent: dict[str, dict[str, Any]] = self.locally_created.copy() # Cross reference the source of the cookie for n in self._nodes_list: - if hasattr(n, 'cookies_sent'): + if 'cookies_sent' in n.features: for c_sent in n.cookies_sent: # Remove cookie from list if sent during the capture. self.locally_created_not_sent.pop(c_sent, None) @@ -358,7 +358,7 @@ def __init__(self, har_path: Path, capture_uuid: str): # Add context if urls are found in external_ressources for n in self._nodes_list: - if hasattr(n, 'external_ressources'): + if 'external_ressources' in n.features: for type_ressource, urls in n.external_ressources.items(): for url in urls: if url not in self.all_url_requests: @@ -400,7 +400,7 @@ def __init__(self, har_path: Path, capture_uuid: str): @property def initial_referer(self) -> str | None: '''The referer passed to the first URL in the tree''' - if hasattr(self.url_tree, 'referer'): + if 'referer' in self.url_tree.features: return self.url_tree.referer return None @@ -441,7 +441,7 @@ def stats(self) -> dict[str, Any]: @property def redirects(self) -> list[str]: """List of redirects for this tree""" - return [a.name for a in reversed(self.rendered_node.get_ancestors())] + [self.rendered_node.name] + return [a.name for a in reversed(list(self.rendered_node.ancestors()))] + [self.rendered_node.name] @property def root_referer(self) -> str | None: @@ -471,7 +471,7 @@ def build_all_hashes(self, algorithm: str='sha1') -> dict[str, list[URLNode]]: h = hashlib.new(algorithm) h.update(urlnode.body.getbuffer()) to_return[h.hexdigest()].append(urlnode) - if hasattr(urlnode, 'embedded_ressources'): + if 'embedded_ressources' in urlnode.features: for _mimetype, blobs in urlnode.embedded_ressources.items(): for blob in blobs: h = hashlib.new(algorithm) @@ -514,22 +514,22 @@ def _load_url_entries(self) -> None: n = URLNode(capture_uuid=self.har.capture_uuid, name=unquote_plus(url_entry['request']['url'])) n.load_har_entry(url_entry, list(self.all_url_requests.keys())) - if hasattr(n, 'redirect_url'): + if 'redirect_url' in n.features: self.all_redirects.append(n.redirect_url) - if hasattr(n, 'initiator_url'): + if 'initiator_url' in n.features: # The HAR file was created by chrome/chromium and we got the _initiator key self.all_initiator_url[n.initiator_url].append(n.name) if url_entry['startedDateTime'] in self.har.pages_start_times: for page in self.har.pages_start_times[url_entry['startedDateTime']]: - if hasattr(n, 'pageref') and page['id'] == n.pageref: + if 'pageref' in n.features and page['id'] == n.pageref: # This node is the root entry of a page. Can be used as a fallback when we build the tree self.pages_root[n.pageref] = n.uuid break # NOTE 2021-05-28: Ignore referer for first entry - if hasattr(n, 'referer') and i > 0: + if 'referer' in n.features and i > 0: # NOTE 2021-05-14: referer to self are a real thing: url -> POST to self if n.name != n.referer or ('method' in n.request and n.request['method'] == 'POST'): self.all_referer[n.referer].append(n.name) @@ -543,7 +543,7 @@ def _load_url_entries(self) -> None: for page in pages: if page['id'] not in self.pages_root: for node in self._nodes_list: - if not hasattr(node, 'pageref'): + if 'pageref' not in node.features: # 2022-11-19: No pageref for this node in the HAR file, # this is weird but we need it as a fallback. node.add_feature('pageref', page['id']) @@ -553,17 +553,16 @@ def _load_url_entries(self) -> None: def get_host_node_by_uuid(self, uuid: str) -> HostNode: """Returns the node with this UUID from the HostNode tree""" - return self.hostname_tree.search_nodes(uuid=uuid)[0] + return self.hostname_tree.get_first_by_feature('uuid', uuid, expect_missing=False) def get_url_node_by_uuid(self, uuid: str) -> URLNode: """Returns the node with this UUID from the URLNode tree""" - return self.url_tree.search_nodes(uuid=uuid)[0] + return self.url_tree.get_first_by_feature('uuid', uuid, expect_missing=False) @property def rendered_node(self) -> URLNode: - node = self.url_tree.search_nodes(name=self.har.final_redirect) - if node: - return node[0] + if node := self.url_tree.get_first_by_feature('name', self.har.final_redirect, expect_missing=True): + return node if self.har.final_redirect: self.logger.warning(f'Final redirect URL from adress bar not in tree: {self.har.final_redirect}') @@ -572,7 +571,7 @@ def rendered_node(self) -> URLNode: pass # Just try to get the best guess: first node after JS/HTTP redirects curnode = self.url_tree - while hasattr(curnode, 'redirect') and curnode.redirect: + while 'redirect' in curnode.features and curnode.redirect: for child in curnode.children: if child.name == curnode.redirect_url: curnode = child @@ -612,7 +611,7 @@ def make_hostname_tree(self, root_nodes_url: URLNode | list[URLNode], root_node_ child_node_hostname.add_url(child_node_url) - if not child_node_url.is_leaf(): + if not child_node_url.is_leaf: sub_roots[child_node_hostname].append(child_node_url) for child_node_hostname, child_nodes_url in sub_roots.items(): @@ -652,13 +651,13 @@ def make_tree(self) -> URLNode: @trace_make_subtree_fallback def _make_subtree_fallback(self, node: URLNode, dev_debug: bool=False) -> None: - if hasattr(node, 'referer'): + if 'referer' in node.features: # 2022-04-28: the node has a referer, but for some reason, it could't be attached to the tree # Probable reason: the referer is a part of the URL (hostname) # FIXME: this is a very dirty fix, but I'm not sure we can do it any better if (referer_hostname := urlparse(node.referer).hostname): # the referer has a hostname - if (nodes_with_hostname := self.url_tree.search_nodes(hostname=referer_hostname)): + if nodes_with_hostname := list(self.url_tree.search_nodes(hostname=referer_hostname)): # the hostname has at least a node in the tree for node_with_hostname in nodes_with_hostname: if not node_with_hostname.empty_response: @@ -681,14 +680,14 @@ def _make_subtree_fallback(self, node: URLNode, dev_debug: bool=False) -> None: if dev_debug: self.logger.warning(f'Failed to attach URLNode in the normal process, attaching node to page {node.pageref} - Node: {page_root_node.uuid} - {page_root_node.name}.') self._make_subtree(page_root_node, [node]) - elif self.url_tree.search_nodes(name=self.har.final_redirect): + elif final_node := self.url_tree.get_first_by_feature('name', self.har.final_redirect, expect_missing=True): # Generally, when we have a bunch of redirects, they do not branch out before the final landing page # *but* it is not always the case: some intermediary redirects will have calls to 3rd party pages. # Hopefully, this last case was taken care of in the branch above. # In this branch, we get the landing page after the redirects (if any), and attach the node to it. if dev_debug: self.logger.warning(f'Failed to attach URLNode in the normal process, attaching node to final redirect: {self.har.final_redirect}.') - self._make_subtree(self.url_tree.search_nodes(name=self.har.final_redirect)[0], [node]) + self._make_subtree(final_node, [node]) else: # No luck, the node is root for this pageref, let's attach it to the prior page in the list, or the very first node (tree root) page_before = self.har.har['log']['pages'][0] @@ -739,7 +738,7 @@ def _make_subtree(self, root: URLNode, nodes_to_attach: list[URLNode] | None=Non for unode in unodes: # NOTE: as we're calling the method recursively, a node containing URLs in its external_ressources will attach # the the subnodes to itself, even if the subnodes have a different referer. It will often be correct, but not always. - if hasattr(unode, 'redirect') and not hasattr(unode, 'redirect_to_nothing'): + if 'redirect' in unode.features and 'redirect_to_nothing' not in unode.features: # If the subnode has a redirect URL set, we get all the requests matching this URL # One may think the entry related to this redirect URL has a referer to the parent. One would be wrong. # URL 1 has a referer, and redirects to URL 2. URL 2 has the same referer as URL 1. @@ -776,7 +775,9 @@ def _make_subtree(self, root: URLNode, nodes_to_attach: list[URLNode] | None=Non # The URL (unode.name) is in the list of known urls initiating calls for u in self.all_initiator_url[unode.name]: matching_urls = [url_node for url_node in self.all_url_requests[u] - if url_node in self._nodes_list and hasattr(url_node, 'initiator_url') and url_node.initiator_url == unode.name] + if url_node in self._nodes_list + and 'initiator_url' in url_node.features + and url_node.initiator_url == unode.name] self._nodes_list = [node for node in self._nodes_list if node not in matching_urls] if dev_debug: self.logger.warning(f'Found via initiator from {unode.name} to {matching_urls}.') @@ -793,14 +794,16 @@ def _make_subtree(self, root: URLNode, nodes_to_attach: list[URLNode] | None=Non matching_urls = [] for u in self.all_referer[ref]: matching_urls += [url_node for url_node in self.all_url_requests[u] - if url_node in self._nodes_list and hasattr(url_node, 'referer') and url_node.referer == ref] + if url_node in self._nodes_list + and 'referer' in url_node.features + and url_node.referer == ref] self._nodes_list = [node for node in self._nodes_list if node not in matching_urls] if dev_debug: self.logger.warning(f'Found via referer from {unode.name} to {matching_urls}.') # 2022-04-27: build subtrees recursively *after* we find all the best referer matches self._make_subtree(unode, matching_urls) - if hasattr(unode, 'external_ressources'): + if 'external_ressources' in unode.features: # the url loads external things, and some of them have no referer.... for external_tag, links in unode.external_ressources.items(): for link in links: diff --git a/har2tree/nodes.py b/har2tree/nodes.py index ad6fcd2..13b4fcd 100644 --- a/har2tree/nodes.py +++ b/har2tree/nodes.py @@ -16,12 +16,12 @@ from functools import lru_cache from io import BytesIO from pathlib import Path -from typing import MutableMapping, Any +from typing import MutableMapping, Any, overload, Literal from urllib.parse import unquote_plus, urlparse, urljoin import filetype # type: ignore from bs4 import BeautifulSoup -from ete3 import TreeNode # type: ignore +from ete4 import Tree # type: ignore from publicsuffixlist import PublicSuffixList # type: ignore from w3lib.html import strip_html5_whitespace from w3lib.url import canonicalize_url, safe_url_string @@ -37,15 +37,53 @@ def get_public_suffix_list() -> PublicSuffixList: return PublicSuffixList() -class HarTreeNode(TreeNode): # type: ignore[misc] +class HarTreeNode(Tree): # type: ignore[misc] - def __init__(self, capture_uuid: str, **kwargs: Any): + def __init__(self, capture_uuid: str, name: str | None=None): """Node dumpable in json to display with d3js""" - super().__init__(**kwargs) + super().__init__() logger = logging.getLogger(f'{__name__}.{self.__class__.__name__}') self.logger = Har2TreeLogAdapter(logger, {'uuid': capture_uuid}) self.add_feature('uuid', str(uuid.uuid4())) - self.features_to_skip = {'dist', 'support'} + if name: + self.add_feature('name', name) + self.features_to_skip: set[str] = set() + + def add_feature(self, feature_name: str, feature_value: Any) -> None: + self.add_prop(feature_name, feature_value) + + def __setattr__(self, attribute: str, value: Any) -> None: + if hasattr(self, 'props') and self.props and attribute in self.props: + self.props[attribute] = value + else: + super().__setattr__(attribute, value) + + def __getattr__(self, attribute: str) -> Any: + try: + return super().__getattribute__(attribute) + except AttributeError: + # reproduce ete3. + return self.props[attribute] + + @property + def features(self) -> set[str]: + return set(self.props.keys()) + + @overload + def get_first_by_feature(self, feature_name: str, value: str, /, *, expect_missing: Literal[True]=True) -> HarTreeNode | None: + ... + + @overload + def get_first_by_feature(self, feature_name: str, value: str, /, *, expect_missing: Literal[False]) -> HarTreeNode: + ... + + def get_first_by_feature(self, feature_name: str, value: str, /, *, expect_missing: bool=False) -> HarTreeNode | None: + try: + return next(self.search_nodes(**{feature_name: value})) + except StopIteration: + if expect_missing: + return None + raise Har2TreeError(f'Unable to find feature "{feature_name}": "{value}"') def to_dict(self) -> MutableMapping[str, Any]: """Make a dict that can then be dumped in json. @@ -54,7 +92,7 @@ def to_dict(self) -> MutableMapping[str, Any]: for feature in self.features: if feature in self.features_to_skip: continue - to_return[feature] = getattr(self, feature) + to_return[feature] = self.props[feature] for child in self.children: to_return['children'].append(child) @@ -67,12 +105,11 @@ def to_json(self) -> str: class URLNode(HarTreeNode): - start_time: datetime - def __init__(self, capture_uuid: str, **kwargs: Any): + def __init__(self, capture_uuid: str, name: str): """Node of the URL Tree""" - super().__init__(capture_uuid=capture_uuid, **kwargs) + super().__init__(capture_uuid=capture_uuid, name=name) # Do not add the body in the json dump self.features_to_skip.add('body') self.features_to_skip.add('url_split') @@ -85,13 +122,13 @@ def add_rendered_features(self, all_requests: list[str], rendered_html: BytesIO if rendered_html: self.add_feature('rendered_html', rendered_html) rendered_external, rendered_embedded = find_external_ressources(rendered_html.getvalue(), self.name, all_requests) - if hasattr(self, 'external_ressources'): + if 'external_ressources' in self.features: # for the external ressources, the keys are always the same self.external_ressources: dict[str, list[str]] = {initiator_type: urls + rendered_external[initiator_type] for initiator_type, urls in self.external_ressources.items()} else: self.add_feature('external_ressources', rendered_external) - if hasattr(self, 'embedded_ressources'): + if 'embedded_ressources' in self.features: # for the embedded ressources, the keys are the mimetypes, they may not overlap mimetypes = list(self.embedded_ressources.keys()) + list(rendered_embedded.keys()) self.embedded_ressources: dict[str, list[tuple[str, BytesIO]]] = {mimetype: self.embedded_ressources.get(mimetype, []) + rendered_embedded.get(mimetype, []) for mimetype in mimetypes} @@ -155,7 +192,7 @@ def load_har_entry(self, har_entry: MutableMapping[str, Any], all_requests: list self.add_feature('time', timedelta(milliseconds=har_entry['time'])) self.add_feature('time_content_received', self.start_time + self.time) # Instant the response is fully received (and the processing of the content by the browser can start) - if hasattr(self, 'file_on_disk'): + if 'file_on_disk' in self.features: # TODO: Do something better? hostname is the feature name used for the aggregated tree # so we need that unless we want to change the JS self.add_feature('hostname', str(Path(self.url_split.path).parent)) @@ -175,7 +212,7 @@ def load_har_entry(self, har_entry: MutableMapping[str, Any], all_requests: list # Not an IP pass - if not hasattr(self, 'hostname_is_ip'): + if 'hostname_is_ip' not in self.features or not self.hostname_is_ip: try: # attempt to decode if the hostname is idna encoded idna_decoded = self.hostname.encode().decode('idna') @@ -184,7 +221,7 @@ def load_har_entry(self, har_entry: MutableMapping[str, Any], all_requests: list except UnicodeError: pass - if not hasattr(self, 'hostname_is_ip') and not hasattr(self, 'file_on_disk'): + if 'hostname_is_ip' not in self.features and 'file_on_disk' not in self.features: tld = get_public_suffix_list().publicsuffix(self.hostname) if tld: self.add_feature('known_tld', tld) @@ -331,6 +368,7 @@ def load_har_entry(self, har_entry: MutableMapping[str, Any], all_requests: list if not self.response['content'].get('text') or self.response['content']['text'] == '': # If the content of the response is empty, skip. self.add_feature('empty_response', True) + self.add_feature('mimetype', '') else: self.add_feature('empty_response', False) if self.response['content'].get('encoding') == 'base64': @@ -347,12 +385,12 @@ def load_har_entry(self, har_entry: MutableMapping[str, Any], all_requests: list if mt not in ["application/octet-stream", "x-unknown"]: self.add_feature('mimetype', mt) - if not hasattr(self, 'mimetype'): + if 'mimetype' not in self.features: # try to guess something better if kind := filetype.guess(self.body.getvalue()): self.add_feature('mimetype', kind.mime) - if not hasattr(self, 'mimetype'): + if 'mimetype' not in self.features: self.add_feature('mimetype', '') external_ressources, embedded_ressources = find_external_ressources(self.body.getvalue(), self.name, all_requests) @@ -383,8 +421,6 @@ def load_har_entry(self, har_entry: MutableMapping[str, Any], all_requests: list self.add_feature('redirect', True) self.add_feature('redirect_url', self.external_ressources['meta_refresh'][0]) - # FIXME: Deprecated, use generic_type directly. Keep for now for backward compat - self.add_feature(self.generic_type, True) if self.generic_type == 'unknown_mimetype': if self.mimetype not in ['x-unknown']: self.logger.warning(f'Unknown mimetype: {self.mimetype}') @@ -531,7 +567,7 @@ def _sanitize(maybe_url: str) -> str | None: return None return href - if not hasattr(self, 'rendered_html') or not self.rendered_html: + if 'rendered_html' not in self.features or not self.rendered_html: raise Har2TreeError('Not the node of a page rendered, invalid request.') urls: set[str] = set() soup = BeautifulSoup(self.rendered_html.getvalue(), "lxml") @@ -562,9 +598,9 @@ def _sanitize(maybe_url: str) -> str | None: class HostNode(HarTreeNode): - def __init__(self, capture_uuid: str, **kwargs: Any): + def __init__(self, capture_uuid: str, name: str | None =None): """Node of the Hostname Tree""" - super().__init__(capture_uuid=capture_uuid, **kwargs) + super().__init__(capture_uuid=capture_uuid, name=name) # Do not add the URLs in the json dump self.features_to_skip.add('urls') @@ -631,10 +667,10 @@ def add_url(self, url: URLNode) -> None: """Add a URL node to the Host node, initialize/update the features""" if not self.name: self.add_feature('name', url.hostname) - if hasattr(url, 'idna'): + if 'idna' in url.features: self.add_feature('idna', url.idna) - if hasattr(url, 'hostname_is_ip') and url.hostname_is_ip: + if 'hostname_is_ip' in url.features and url.hostname_is_ip: self.add_feature('hostname_is_ip', True) self.urls.append(url) @@ -642,46 +678,48 @@ def add_url(self, url: URLNode) -> None: # Add to URLNode a reference to the HostNode UUID url.add_feature('hostnode_uuid', self.uuid) - if hasattr(url, 'rendered_html') or hasattr(url, 'downloaded_filename'): + if 'rendered_html' in url.features or 'downloaded_filename' in url.features: self.contains_rendered_urlnode = True - if hasattr(url, 'downloaded_filename'): + if 'downloaded_filename' in url.features: self.add_feature('downloaded_filename', url.downloaded_filename) - if hasattr(url, 'cookies_sent'): + if 'cookies_sent' in url.features: # Keep a set of cookies sent: different URLs will send the same cookie self.cookies_sent.update(set(url.cookies_sent.keys())) - if hasattr(url, 'cookies_received'): + if 'cookies_received' in url.features: # Keep a set of cookies received: different URLs will receive the same cookie self.cookies_received.update({(domain, cookie, is_3rd_party) for domain, cookie, is_3rd_party in url.cookies_received}) - if hasattr(url, 'js'): - self.js += 1 - if hasattr(url, 'redirect'): + if 'redirect' in url.features: self.redirect += 1 - if hasattr(url, 'redirect_to_nothing'): + if 'redirect_to_nothing' in url.features: self.redirect_to_nothing += 1 - if hasattr(url, 'image'): + if 'iframe' in url.features: + self.iframe += 1 + if url.generic_type == 'js': + self.js += 1 + elif url.generic_type == 'image': self.image += 1 - if hasattr(url, 'css'): + elif url.generic_type == 'css': self.css += 1 - if hasattr(url, 'json'): + elif url.generic_type == 'json': self.json += 1 - if hasattr(url, 'html'): + elif url.generic_type == 'html': self.html += 1 - if hasattr(url, 'font'): + elif url.generic_type == 'font': self.font += 1 - if hasattr(url, 'octet_stream'): + elif url.generic_type == 'octet_stream': self.octet_stream += 1 - if hasattr(url, 'text'): + elif url.generic_type == 'text': self.text += 1 - if hasattr(url, 'pdf'): + elif url.generic_type == 'pdf': self.pdf += 1 # FIXME: need icon - if hasattr(url, 'video') or hasattr(url, 'livestream') or hasattr(url, 'audio') or hasattr(url, 'flash'): + elif url.generic_type in {'video', 'livestream', 'audio', 'flash'}: self.video += 1 - if hasattr(url, 'unknown_mimetype') or hasattr(url, 'unset_mimetype'): + elif url.generic_type in {'unknown_mimetype', 'unset_mimetype'}: self.unknown_mimetype += 1 - if hasattr(url, 'iframe'): - self.iframe += 1 + else: + self.logger.warning(f'Unexpected generic type: {url.generic_type}') if url.name.startswith('http://'): self.http_content = True diff --git a/poetry.lock b/poetry.lock index 9a1e533..520b400 100644 --- a/poetry.lock +++ b/poetry.lock @@ -262,6 +262,109 @@ webencodings = "*" [package.extras] css = ["tinycss2 (>=1.1.0,<1.3)"] +[[package]] +name = "bottle" +version = "0.12.25" +description = "Fast and simple WSGI-framework for small web-applications." +optional = false +python-versions = "*" +files = [ + {file = "bottle-0.12.25-py3-none-any.whl", hash = "sha256:d6f15f9d422670b7c073d63bd8d287b135388da187a0f3e3c19293626ce034ea"}, + {file = "bottle-0.12.25.tar.gz", hash = "sha256:e1a9c94970ae6d710b3fb4526294dfeb86f2cb4a81eff3a4b98dc40fb0e5e021"}, +] + +[[package]] +name = "brotli" +version = "1.1.0" +description = "Python bindings for the Brotli compression library" +optional = false +python-versions = "*" +files = [ + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752"}, + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"}, + {file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"}, + {file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"}, + {file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"}, + {file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"}, + {file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"}, + {file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"}, + {file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"}, + {file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"}, + {file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"}, + {file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"}, + {file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"}, + {file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"}, + {file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"}, + {file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"}, + {file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"}, + {file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"}, + {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"}, +] + [[package]] name = "certifi" version = "2024.8.30" @@ -642,14 +745,32 @@ files = [ ] [[package]] -name = "ete3" -version = "3.1.3" +name = "ete4" +version = "4.0.0b2" description = "A Python Environment for (phylogenetic) Tree Exploration" optional = false -python-versions = "*" -files = [ - {file = "ete3-3.1.3.tar.gz", hash = "sha256:06a3b7fa8ed90187b076a8dbbe5b1b62acee94201d3c6e822f55f449601ef6f2"}, -] +python-versions = ">=3.7" +files = [] +develop = false + +[package.dependencies] +bottle = "*" +brotli = "*" +numpy = "*" +requests = "*" +scipy = "*" + +[package.extras] +doc = ["sphinx"] +test = ["pytest (>=6.0)"] +treediff = ["lap"] +treeview = ["pyqt6"] + +[package.source] +type = "git" +url = "https://github.com/etetoolkit/ete.git" +reference = "HEAD" +resolved_reference = "a88743b094db59b8ef0ae4cbdac53f92b5d32aa2" [[package]] name = "exceptiongroup" @@ -2596,6 +2717,86 @@ files = [ {file = "rpds_py-0.20.0.tar.gz", hash = "sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121"}, ] +[[package]] +name = "scipy" +version = "1.10.1" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = "<3.12,>=3.8" +files = [ + {file = "scipy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7354fd7527a4b0377ce55f286805b34e8c54b91be865bac273f527e1b839019"}, + {file = "scipy-1.10.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:4b3f429188c66603a1a5c549fb414e4d3bdc2a24792e061ffbd607d3d75fd84e"}, + {file = "scipy-1.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1553b5dcddd64ba9a0d95355e63fe6c3fc303a8fd77c7bc91e77d61363f7433f"}, + {file = "scipy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c0ff64b06b10e35215abce517252b375e580a6125fd5fdf6421b98efbefb2d2"}, + {file = "scipy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:fae8a7b898c42dffe3f7361c40d5952b6bf32d10c4569098d276b4c547905ee1"}, + {file = "scipy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f1564ea217e82c1bbe75ddf7285ba0709ecd503f048cb1236ae9995f64217bd"}, + {file = "scipy-1.10.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d925fa1c81b772882aa55bcc10bf88324dadb66ff85d548c71515f6689c6dac5"}, + {file = "scipy-1.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaea0a6be54462ec027de54fca511540980d1e9eea68b2d5c1dbfe084797be35"}, + {file = "scipy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15a35c4242ec5f292c3dd364a7c71a61be87a3d4ddcc693372813c0b73c9af1d"}, + {file = "scipy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:43b8e0bcb877faf0abfb613d51026cd5cc78918e9530e375727bf0625c82788f"}, + {file = "scipy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5678f88c68ea866ed9ebe3a989091088553ba12c6090244fdae3e467b1139c35"}, + {file = "scipy-1.10.1-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:39becb03541f9e58243f4197584286e339029e8908c46f7221abeea4b749fa88"}, + {file = "scipy-1.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bce5869c8d68cf383ce240e44c1d9ae7c06078a9396df68ce88a1230f93a30c1"}, + {file = "scipy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07c3457ce0b3ad5124f98a86533106b643dd811dd61b548e78cf4c8786652f6f"}, + {file = "scipy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:049a8bbf0ad95277ffba9b3b7d23e5369cc39e66406d60422c8cfef40ccc8415"}, + {file = "scipy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cd9f1027ff30d90618914a64ca9b1a77a431159df0e2a195d8a9e8a04c78abf9"}, + {file = "scipy-1.10.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:79c8e5a6c6ffaf3a2262ef1be1e108a035cf4f05c14df56057b64acc5bebffb6"}, + {file = "scipy-1.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51af417a000d2dbe1ec6c372dfe688e041a7084da4fdd350aeb139bd3fb55353"}, + {file = "scipy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b4735d6c28aad3cdcf52117e0e91d6b39acd4272f3f5cd9907c24ee931ad601"}, + {file = "scipy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ff7f37b1bf4417baca958d254e8e2875d0cc23aaadbe65b3d5b3077b0eb23ea"}, + {file = "scipy-1.10.1.tar.gz", hash = "sha256:2cf9dfb80a7b4589ba4c40ce7588986d6d5cebc5457cad2c2880f6bc2d42f3a5"}, +] + +[package.dependencies] +numpy = ">=1.19.5,<1.27.0" + +[package.extras] +dev = ["click", "doit (>=0.36.0)", "flake8", "mypy", "pycodestyle", "pydevtool", "rich-click", "typing_extensions"] +doc = ["matplotlib (>2)", "numpydoc", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-design (>=0.2.0)"] +test = ["asv", "gmpy2", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + +[[package]] +name = "scipy" +version = "1.13.1" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca"}, + {file = "scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f"}, + {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989"}, + {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f"}, + {file = "scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94"}, + {file = "scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54"}, + {file = "scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9"}, + {file = "scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326"}, + {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299"}, + {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa"}, + {file = "scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59"}, + {file = "scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b"}, + {file = "scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1"}, + {file = "scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d"}, + {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627"}, + {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884"}, + {file = "scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16"}, + {file = "scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949"}, + {file = "scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5"}, + {file = "scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24"}, + {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004"}, + {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d"}, + {file = "scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c"}, + {file = "scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2"}, + {file = "scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c"}, +] + +[package.dependencies] +numpy = ">=1.22.4,<2.3" + +[package.extras] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"] +doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.12.0)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0)", "sphinx-design (>=0.4.0)"] +test = ["array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + [[package]] name = "send2trash" version = "1.8.3" @@ -3222,4 +3423,4 @@ docs = ["Sphinx", "Sphinx", "Sphinx", "six"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<3.13" -content-hash = "b7c571f678db7f25a43fed11a73ac47902d2755eea9844365b511e35658cd980" +content-hash = "eca287f43d91516508d1221a7bab834100af808629cf0b1cfa296161f676d5af" diff --git a/pyproject.toml b/pyproject.toml index f5a2aaa..3a5835a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ classifiers = [ [tool.poetry.dependencies] python = ">=3.8,<3.13" -ete3 = "^3.1.3" +ete4 = { git = "https://github.com/etetoolkit/ete.git" } beautifulsoup4 = {version= "^4.12.3", extras = ["lxml", "charset_normalizer"]} publicsuffixlist = "^1.0.2.20240827" filetype = "^1.2.0" @@ -38,6 +38,10 @@ Sphinx = [ {version = "^8", python = ">=3.10", optional = true} ] charset-normalizer = "^3.3.2" +scipy = [ + { version = "<1.13", python = "<3.9" }, + { version = "^1.13.0", python = ">=3.9" } +] six = {version = "^1.16.0", optional = true} [tool.poetry.group.dev.dependencies] diff --git a/tests/simple_test.py b/tests/simple_test.py index 7efa76a..19715cd 100644 --- a/tests/simple_test.py +++ b/tests/simple_test.py @@ -131,7 +131,7 @@ def test_rebuild_url_partial_double_slash(self) -> None: self.assertEqual(rebuild_url_double_slash, 'https://www.youtube.com/watch?v=iwGFalTRHDA') def test_hostname_tree_features(self) -> None: - self.assertEqual(self.http_redirect_ct.root_hartree.hostname_tree.features, {'name', 'js', 'html', 'pdf', 'json', 'text', 'video', 'css', 'iframe', 'http_content', 'https_content', 'support', 'dist', 'octet_stream', 'font', 'redirect', + self.assertEqual(self.http_redirect_ct.root_hartree.hostname_tree.features, {'name', 'js', 'html', 'pdf', 'json', 'text', 'video', 'css', 'iframe', 'http_content', 'https_content', 'octet_stream', 'font', 'redirect', 'unknown_mimetype', 'contains_rendered_urlnode', 'urls', 'uuid', 'redirect_to_nothing', 'unset_mimetype', 'image'}) self.assertTrue('meta_refresh' in self.http_redirect_ct.root_hartree.url_tree.external_ressources) self.assertEqual(self.http_redirect_ct.root_hartree.url_tree.external_ressources['meta_refresh'][0], 'https://www.youtube.com/watch?v=iwGFalTRHDA') @@ -246,7 +246,7 @@ def test_third_party_cookies_received(self) -> None: def test_hostnode_to_json(self) -> None: # Easiest way to test the to_json method without having a huge string here is extracting one from a file - # This file is already cleaned, no UUIDs (see) + # This file is already cleaned, no UUIDs with open(self.test_dir / 'iframe' / 'to_json.json') as json_file: expected_dict = json.load(json_file)