From 77a17f5a76fb4b64012d0cbdddc0216dd2d08514 Mon Sep 17 00:00:00 2001 From: Cyril Matthey-Doret Date: Fri, 24 Nov 2023 13:50:22 +0100 Subject: [PATCH] perf(license): tf-idf based matching (#99) * refactor(utils): create gimie.utils.uri submodule * chore: add numpy + scipy to deps * feat: add tfidf vectorizer * test: unit tests for tfidf vectorizer * refactor(tfidf): more intuitive func names * fix(tfidf): correct ngrams tokenization for n>1, adjust doctests * ci: change python versions 3.8-3.10 -> 3.9-3.12 * chore: add scipy to deps * refactor(license): use tfidf in LicenseParser * chore: rm scancode from deps * feat: add pre-computed license tf-idf * feat: script to regen. tf-idf for all spdx licenses * test(license): update docstrings for tfidf * test(tfidf): rm test corpus from module, adapt doctest * refactor(license): only include osi-approved licenses in tfidf matrix * refactor(license): set min similarity to 0.9 * perf(tfidf): prune vectors to float16 ro reduce memory footprint * chore(license): black fmt * doc(license): mention tfidf in parser docstring * chore: rename test_tfidf.py * docs(tfidf): link to sklearn documentation * refactor(tfidf): reorder methods * refactor: rename utils text module * fix: gimie.utils.text_processing import paths --- .github/workflows/poetry-pytest.yml | 2 +- gimie/extractors/__init__.py | 2 +- gimie/parsers/license.py | 160 --- gimie/parsers/license/__init__.py | 132 ++ gimie/parsers/license/data/spdx_licenses.csv | 129 ++ gimie/parsers/license/data/tfidf_matrix.npz | Bin 0 -> 56400 bytes .../license/data/tfidf_vectorizer.json | 1 + gimie/project.py | 2 +- gimie/utils/text_processing.py | 318 +++++ gimie/{utils.py => utils/uri.py} | 0 poetry.lock | 1178 ++++------------- pyproject.toml | 9 +- scripts/generate_tfidf.py | 56 + tests/test_tfidf.py | 69 + 14 files changed, 968 insertions(+), 1090 deletions(-) delete mode 100644 gimie/parsers/license.py create mode 100644 gimie/parsers/license/__init__.py create mode 100644 gimie/parsers/license/data/spdx_licenses.csv create mode 100644 gimie/parsers/license/data/tfidf_matrix.npz create mode 100644 gimie/parsers/license/data/tfidf_vectorizer.json create mode 100644 gimie/utils/text_processing.py rename gimie/{utils.py => utils/uri.py} (100%) create mode 100644 scripts/generate_tfidf.py create mode 100644 tests/test_tfidf.py diff --git a/.github/workflows/poetry-pytest.yml b/.github/workflows/poetry-pytest.yml index 27c35fb4..d0c8fe2a 100644 --- a/.github/workflows/poetry-pytest.yml +++ b/.github/workflows/poetry-pytest.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: # https://github.com/actions/checkout - uses: actions/checkout@v4 diff --git a/gimie/extractors/__init__.py b/gimie/extractors/__init__.py index 2f5c7d9f..0c28bd45 100644 --- a/gimie/extractors/__init__.py +++ b/gimie/extractors/__init__.py @@ -21,7 +21,7 @@ from gimie.extractors.github import GithubExtractor from gimie.extractors.gitlab import GitlabExtractor from gimie.extractors.git import GitExtractor -from gimie.utils import validate_url +from gimie.utils.uri import validate_url GIT_PROVIDERS: Dict[str, Type[Extractor]] = { "git": GitExtractor, diff --git a/gimie/parsers/license.py b/gimie/parsers/license.py deleted file mode 100644 index a7811541..00000000 --- a/gimie/parsers/license.py +++ /dev/null @@ -1,160 +0,0 @@ -# Gimie -# Copyright 2022 - Swiss Data Science Center (SDSC) -# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and -# Eidgenössische Technische Hochschule Zürich (ETHZ). -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os -import re -import tempfile -from typing import Iterable, List, Optional, Set, Tuple, Union - -from rdflib.term import URIRef -from scancode.api import get_licenses -from spdx_license_list import LICENSES - -from gimie.graph.namespaces import SDO -from gimie.parsers.abstract import Parser, Property - -SPDX_IDS = list(LICENSES.keys()) - - -class LicenseParser(Parser): - """Parse LICENSE file(s) into schema:license.""" - - def __init__(self): - super().__init__() - - def parse(self, data: bytes) -> Set[Property]: - """Extracts an spdx URL from a license file and returns a - set with a single tuple . - If no matching URL is found, an empty set is returned. - """ - props = set() - license_url = get_license_url(data) - - if license_url: - props.add((SDO.license, URIRef(license_url))) - return props - - -def get_license_url(data: bytes) -> Optional[str]: - """Takes a license body as raw data, and matches its content - using the scancode API to get possible license matches. The best match is - then returned as a spdx license URL. - - Examples - -------- - >>> get_license_url(open('LICENSE', 'rb').read()) - 'https://spdx.org/licenses/Apache-2.0.html' - """ - temp_file = tempfile.NamedTemporaryFile(delete=False) - temp_file.write(data) - temp_file.close() - - license_detections = get_licenses(temp_file.name, include_text=True)[ - "license_detections" - ] - license_id = get_license_with_highest_coverage(license_detections) # type: ignore - if license_id is None: - return None - spdx_license_id = get_spdx_license_id(license_id) - os.remove(temp_file.name) - if spdx_license_id: - return f"https://spdx.org/licenses/{str(spdx_license_id)}.html" - - return None - - -def get_spdx_license_id( - license_id: str, - spdx_ids: Iterable[str] = SPDX_IDS, -) -> Optional[str]: - """Given a scancode API license ID also known as a license detection, returns the correctly capitalized - spdx id corresponding to it. - - Parameters - ---------- - license_id: - A license id to match with SPDX licenses. - spdx_ids: - An iterable of (reference) SPDX license ids. - - Examples - -------- - >>> get_spdx_license_id('apache-2.0') - 'Apache-2.0' - >>> get_spdx_license_id('gpl-3.0') - 'GPL-3.0' - """ - - lower_spdx_ids = {spdx.lower(): spdx for spdx in spdx_ids} - - return lower_spdx_ids.get(license_id.lower(), None) - - -def is_license_filename(filename: str) -> bool: - """Given an input filename, returns a boolean indicating whether the filename path looks like a license. - - Parameters - ---------- - filename: - A filename to check. - - Examples - -------- - >>> is_license_filename('LICENSE.txt') - True - >>> is_license_filename('LICENSE-APACHE') - True - >>> is_license_filename('README.md') - False - """ - if filename.startswith("."): - return False - pattern = r".*(license(s)?.*|lizenz|reus(e|ing).*|copy(ing)?.*)(\.(txt|md|rst))?$" - if re.match(pattern, filename, flags=re.IGNORECASE): - return True - return False - - -def get_license_with_highest_coverage( - license_detections: List[dict], -) -> Optional[str]: - """Filters a list of "license detections" (the output of scancode.api.get_licenses) - to return the one with the highest match percentage. - This is used to select among multiple license matches from a single file. - - Parameters - ---------- - license_detections: - A list of license detections, as returned by scancode.api.get_licenses. - - Examples - -------- - >>> from scancode.api import get_licenses - >>> license_detections = get_licenses('LICENSE')['license_detections'] - >>> get_license_with_highest_coverage(license_detections) - 'apache-2.0' - """ - highest_coverage = 0.0 - highest_license = None - - for detection in license_detections: - matches = detection["matches"] if "matches" in detection else [] - for match in matches: - match_coverage = match.get("score", 0) - if match_coverage > highest_coverage: - highest_coverage = match_coverage - highest_license = match.get("license_expression", None) - return highest_license diff --git a/gimie/parsers/license/__init__.py b/gimie/parsers/license/__init__.py new file mode 100644 index 00000000..c3e78c8c --- /dev/null +++ b/gimie/parsers/license/__init__.py @@ -0,0 +1,132 @@ +# Gimie +# Copyright 2022 - Swiss Data Science Center (SDSC) +# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and +# Eidgenössische Technische Hochschule Zürich (ETHZ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import csv +from io import BytesIO +import pkgutil +import re +from typing import List, Optional, Set + +import numpy as np +import scipy.sparse as sp +from rdflib.term import URIRef + +from gimie.graph.namespaces import SDO +from gimie.parsers.abstract import Parser, Property +from gimie.utils.text_processing import TfidfVectorizer + + +class LicenseParser(Parser): + """Parse LICENSE body into schema:license . + Uses tf-idf-based matching.""" + + def __init__(self): + super().__init__() + + def parse(self, data: bytes) -> Set[Property]: + """Extracts an spdx URL from a license file and returns a + set with a single tuple . + If no matching URL is found, an empty set is returned. + """ + props = set() + license_url = match_license(data) + + if license_url: + props.add((SDO.license, URIRef(license_url))) + return props + + +def match_license(data: bytes, min_similarity: float = 0.9) -> Optional[str]: + """Given a license file, returns the url of the most similar spdx license. + This is done using TF-IDF on the license text and getting the + closest match in the SPDX license corpus based on cosine similarity. + + Parameters + ---------- + data: + The license body as bytes. + + Examples + -------- + >>> match_license(open('LICENSE', 'rb').read()) + 'https://spdx.org/licenses/Apache-2.0.html' + """ + # Compute tfidf vector for input license + vectorizer = load_tfidf_vectorizer() + input_vec = vectorizer.transform([str(data)]) + + # Load ids and tfidf vectors for spdx licenses + spdx_licenses = load_spdx_ids() + spdx_vecs = load_tfidf_matrix() + # Compute cosine similarity between input_vec and spdx vectors + sim: np.ndarray = (input_vec * spdx_vecs.T).todense() + # Pick the most similar spdx vector + closest_idx = np.argmax(sim) + # If similarity is below threshold, return None + if sim[0, closest_idx] < min_similarity: + return None + closest_id = spdx_licenses[closest_idx] + return f"https://spdx.org/licenses/{closest_id}.html" + + +def load_tfidf_vectorizer() -> TfidfVectorizer: + """Load tfidf matrix and vectorizer from disk.""" + + data = pkgutil.get_data(__name__, "data/tfidf_vectorizer.json") + if data is None: + raise FileNotFoundError("Could not find tfidf_vectorizer.json") + return TfidfVectorizer.model_validate_json(data) + + +def load_spdx_ids() -> List[str]: + """Load spdx licenses from disk.""" + data = pkgutil.get_data(__name__, "data/spdx_licenses.csv") + if data is None: + raise FileNotFoundError("Could not find spdx_licenses.csv") + reader = csv.reader(data.decode().split("\n")) + return [l[0] for l in reader if l] + + +def load_tfidf_matrix() -> sp.csr_matrix: + """Load pre-computed tfidf matrix of spdx licenses from disk. + Matrix has dimensions (n_licenses, n_features).""" + data = pkgutil.get_data(__name__, "data/tfidf_matrix.npz") + if data is None: + raise FileNotFoundError("Could not find tfidf_matrix.npz") + return sp.load_npz(BytesIO(data)) + + +def is_license_filename(filename: str) -> bool: + """Given an input filename, returns a boolean indicating whether the filename path looks like a license. + + Parameters + ---------- + filename: + A filename to check. + + Examples + -------- + >>> is_license_filename('LICENSE-APACHE') + True + >>> is_license_filename('README.md') + False + """ + if filename.startswith("."): + return False + pattern = r".*(license(s)?.*|lizenz|reus(e|ing).*|copy(ing)?.*)(\.(txt|md|rst))?$" + if re.match(pattern, filename, flags=re.IGNORECASE): + return True + return False diff --git a/gimie/parsers/license/data/spdx_licenses.csv b/gimie/parsers/license/data/spdx_licenses.csv new file mode 100644 index 00000000..899ca7d2 --- /dev/null +++ b/gimie/parsers/license/data/spdx_licenses.csv @@ -0,0 +1,129 @@ +0BSD,643 +AAL,2529 +AFL-1.1,4676 +AFL-1.2,4950 +AFL-2.0,8986 +AFL-2.1,8947 +AFL-3.0,10315 +AGPL-3.0-only,34020 +AGPL-3.0-or-later,34020 +Apache-1.1,2514 +Apache-2.0,10280 +APL-1.0,46065 +APSL-1.0,19644 +APSL-1.1,20151 +APSL-1.2,19796 +APSL-2.0,20281 +Artistic-1.0,4854 +Artistic-1.0-cl8,5184 +Artistic-1.0-Perl,6060 +Artistic-2.0,8764 +BSD-1-Clause,1086 +BSD-2-Clause,1267 +BSD-2-Clause-Patent,2569 +BSD-3-Clause,1460 +BSD-3-Clause-LBNL,2388 +BSL-1.0,1338 +CAL-1.0,16121 +CAL-1.0-Combined-Work-Exception,16121 +CATOSL-1.1,19046 +CDDL-1.0,16419 +CECILL-2.1,21774 +CERN-OHL-P-2.0,8855 +CERN-OHL-S-2.0,13419 +CERN-OHL-W-2.0,14529 +CNRI-Python,3381 +CPAL-1.0,28141 +CPL-1.0,11653 +CUA-OPL-1.0,23381 +ECL-1.0,2425 +ECL-2.0,11111 +EFL-1.0,919 +EFL-2.0,924 +Entessa,2277 +EPL-1.0,11345 +EPL-2.0,13946 +EUDatagrid,3195 +EUPL-1.1,13231 +EUPL-1.2,13648 +Fair,245 +Frameworx-1.0,9771 +GPL-2.0-only,17337 +GPL-2.0-or-later,17337 +GPL-3.0-only,34509 +GPL-3.0-or-later,34509 +HPND,1187 +ICU,1597 +Intel,2078 +IPA,9093 +IPL-1.0,11409 +ISC,823 +Jam,195 +LGPL-2.0-only,24842 +LGPL-2.0-or-later,24842 +LGPL-2.1-only,25967 +LGPL-2.1-or-later,25967 +LGPL-3.0-only,41933 +LGPL-3.0-or-later,41933 +LiLiQ-P-1.1,6351 +LiLiQ-R-1.1,8392 +LiLiQ-Rplus-1.1,8043 +LPL-1.0,11948 +LPL-1.02,11824 +LPPL-1.3c,18575 +MirOS,888 +MIT,1078 +MIT-0,915 +MIT-Modern-Variant,917 +Motosoto,20187 +MPL-1.0,18272 +MPL-1.1,23669 +MPL-2.0,16727 +MPL-2.0-no-copyleft-exception,16727 +MS-PL,2663 +MS-RL,3058 +MulanPSL-2.0,6850 +Multics,2040 +NASA-1.3,13778 +Naumen,1953 +NCSA,1700 +NGPL,4703 +Nokia,21002 +NPOSL-3.0,11799 +NTP,714 +OCLC-2.0,11121 +OFL-1.1,4012 +OFL-1.1-no-RFN,4012 +OFL-1.1-RFN,4012 +OGTSL,5277 +OLDAP-2.8,2195 +OLFL-1.3,11401 +OSET-PL-2.1,19843 +OSL-1.0,8920 +OSL-2.0,9880 +OSL-2.1,9871 +OSL-3.0,10309 +PHP-3.0,2846 +PHP-3.01,2855 +PostgreSQL,1195 +Python-2.0,9411 +QPL-1.0,4364 +RPL-1.1,33931 +RPL-1.5,32009 +RPSL-1.0,30267 +RSCPL,21050 +SimPL-2.0,2529 +SISSL,14490 +Sleepycat,4995 +SPL-1.0,23398 +UCL-1.0,10556 +Unicode-DFS-2016,2857 +Unlicense,1211 +UPL-1.0,1833 +VSL-1.0,2065 +W3C,2701 +Watcom-1.0,20968 +Xnet,1250 +Zlib,838 +ZPL-2.0,2275 +ZPL-2.1,2100 diff --git a/gimie/parsers/license/data/tfidf_matrix.npz b/gimie/parsers/license/data/tfidf_matrix.npz new file mode 100644 index 0000000000000000000000000000000000000000..9722558f20cd05624300cb7049214d1eb3ef94ac GIT binary patch literal 56400 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 List[str]: + """Basic tokenizer. Removes punctuation, but not stop words. + + Parameters + ---------- + text: + Text to tokenize. + sep: + Token separator. + + Examples + -------- + >>> tokenize("Is this a test? Yes it is.") + ['is', 'this', 'a', 'test', 'yes', 'it', 'is'] + """ + text = text.lower() + text = re.sub(r"[\.|,|;|:|!|?|\n]", "", text) + return text.split(sep) + + +def extract_ngrams(tokens: List[str], size: int = 1) -> List[str]: + """Extract ngrams from a list of tokens. + + Parameters + ---------- + tokens: + List of tokens. + size: + Size of ngrams to extract. + + Examples + -------- + >>> extract_ngrams(["this", "is", "a", "test"], size=2) + ['this is', 'is a', 'a test'] + """ + return [ + " ".join(tokens[i : i + size]) + for i in range(0, len(tokens) - size + 1) + ] + + +def get_ngram_counts( + doc: str, ngram_range: Tuple[int, int] = (1, 1) +) -> Counter[str]: + """Get ngram counts for a document. The ngram range is inclusive. + + Parameters + ---------- + doc: + Document to extract ngrams from. + ngram_range: + Inclusive range of ngram sizes to extract. + + Examples + -------- + >>> get_ngram_counts("Red roses red.", ngram_range=(1, 2)) + Counter({'red': 2, 'roses': 1, 'red roses': 1, 'roses red': 1}) + """ + ngram_counts: Counter[str] = Counter() + tokens = tokenize(doc) + for size in range(ngram_range[0], ngram_range[1] + 1): + ngram_counts += Counter(extract_ngrams(tokens, size)) + return ngram_counts + + +def normalize_csr_rows(X: sp.csr_matrix, norm: str = "l1") -> sp.csr_matrix: + """Normalize rows of a CSR matrix in place. + + Parameters + ---------- + X: + CSR matrix to normalize. + norm: + Norm to use for normalization. Either "l1" or "l2". + + Examples + -------- + >>> X = sp.csr_matrix([[1, 2], [3, 4]], dtype=np.float64) + >>> normalize_csr_rows(X, norm="l1").toarray() + array([[0.33333333, 0.66666667], + [0.42857143, 0.57142857]]) + >>> normalize_csr_rows(X, norm="l2").toarray() + array([[0.4472136 , 0.89442719], + [0.6 , 0.8 ]]) + """ + norm_func = { + "l1": lambda x: np.abs(x).sum(), + "l2": lambda x: np.sqrt((x**2).sum()), + }[norm] + + for i in range(X.shape[0]): + if X[i].sum() == 0.0: + continue + + X[i, :] /= norm_func(X[i].data) + return X + + +@dataclass +class TfidfConfig: + """Configuration for TfidfVectorizer. + + For more information on tf-idf, see https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfTransformer.html + + Parameters + ---------- + max_features: + Maximum number of features to keep. If None, all features are kept. + ngram_range: + Inclusive range of ngram sizes to extract. + smooth_idf: + Smooth idf weights by adding a constant 1 to the numerator and denominator + of the idf as if an extra document was seen containing every term once, + preventing zero divisions. + vocabulary: + Vocabulary to use. If None, the vocabulary is inferred from the data. + norm: + Normalization to use for the tfidf matrix. Either "l1" or "l2". + sublinear_tf: + Apply sublinear tf scaling, i.e. replace tf with 1 + log(tf). + """ + + max_features: Optional[int] = None + ngram_range: Tuple[int, int] = (1, 1) + smooth_idf: bool = True + vocabulary: Optional[Dict[str, int]] = None + norm: Optional[Literal["l1", "l2"]] = None + sublinear_tf: bool = False + + +class TfidfVectorizer(BaseModel): + r"""A simple term frequency-inverse document frequency (tf-idf) vectorizer + that can be loaded from and serialized to JSON. + + This implementation replicates the behavior of scikit-learn's (as of 1.3.2), + but only supports a subset of its parameters. + + For more information on tf-idf, see https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfTransformer.html + + Parameters + ---------- + config: + Configuration for the vectorizer. + idf_vector: + Precomputed idf vector. If None, it is computed from the data. + vocabulary: + Vocabulary to use. If None, the vocabulary is inferred from the data. + + Examples + -------- + >>> docs = ["The quick brown fox", "jumps over", "the lazy dog."] + >>> vectorizer = TfidfVectorizer(config=TfidfConfig()) + >>> tfidf = vectorizer.fit_transform(docs) + >>> tfidf.shape + (3, 8) + """ + + config: TfidfConfig + idf_vector: List[float] = list() + vocabulary: Dict[str, int] = Field(default_factory=dict) + + def _get_idf_vector( + self, ngram_counts: List[Counter[str]], vocab: Dict[str, int] + ) -> List[float]: + """Compute the idf vector for the whole corpus from a list of + ngram counts from each document. + + Parameters + ---------- + ngram_counts: + List of ngram counts for each document. + vocab: + Vocabulary to use. Each ngram key has an integer value used as the + column index of the output matrix. + """ + idf_vector = np.zeros(len(vocab), dtype=np.float64) + for record in ngram_counts: + idf_vector[[vocab[t] for t in record.keys() if t in vocab]] += 1 + n_docs = len(ngram_counts) + int(self.config.smooth_idf) + idf_vector += int(self.config.smooth_idf) + idf_vector = 1 + np.log(n_docs / (idf_vector)) + return list(idf_vector) + + def _get_tf_matrix( + self, ngram_counts: List[Counter[str]], vocab: Dict[str, int] + ) -> sp.csr_matrix: + """Compute the term frequency matrix for the whole corpus from a + list of ngram counts from each document. + + Parameters + ---------- + ngram_counts: + List of ngram counts for each document (rows of the output matrix). + vocab: + Vocabulary to use. Each ngram key has an integer value used as the + column index of the output matrix. + """ + tf_matrix = sp.lil_matrix( + (len(ngram_counts), len(vocab)), dtype=np.float64 + ) + for idx, record in enumerate(ngram_counts): + pairs = record.items() + counts = [v for _, v in pairs] + tf_matrix[idx, [vocab[t] for t, _ in pairs]] = [c for c in counts] + tf_matrix = tf_matrix.tocsr() + if self.config.sublinear_tf: + # applies log in place + np.log(tf_matrix.data, tf_matrix.data) # type: ignore + tf_matrix.data += 1 # type: ignore + return tf_matrix + + def _get_tfidf( + self, ngram_counts: List[Counter[str]], vocab: Dict[str, int] + ) -> sp.csr_matrix: + """Compute the tfidf matrix over the whole corpus from a list of + ngram counts from each document. + + Parameters + ---------- + ngram_counts: + List of ngram counts for each document. + vocab: + Vocabulary to use. Each ngram key has an integer value used as the + column index of the output matrix. + """ + tf_matrix: sp.csr_matrix = self._get_tf_matrix( + ngram_counts, vocab=vocab + ) + + tfidf_matrix = tf_matrix.multiply(np.array(self.idf_vector)) # type: ignore + return tfidf_matrix.tocsr() # type: ignore + + def _get_vocabulary( + self, ngram_counts: Iterable[Counter[str]] + ) -> dict[str, int]: + """Get the vocabulary from a list of ngram counts. The vocabulary + is a mapping from ngrams to integer used as column indices in the + tfidf matrix. + + Parameters + ---------- + ngram_counts: + List of ngram counts for each document. + """ + counts_corpus = reduce(lambda x, y: x | y, ngram_counts).most_common() + if self.config.max_features is not None: + counts_corpus = counts_corpus[: self.config.max_features] + return { + t[0]: i + for i, t in enumerate(sorted(counts_corpus, key=lambda x: x[0])) + } + + def fit(self, data: Iterable[str]): + """Fit the vectorizer to a list of documents. + + Parameters + ---------- + data: + List of documents contents to fit the vectorizer to.""" + counts_records: List[Counter[str]] = [ + get_ngram_counts(doc, self.config.ngram_range) for doc in data + ] + vocab = self.config.vocabulary or self._get_vocabulary(counts_records) + self.idf_vector = self._get_idf_vector(counts_records, vocab=vocab) + self.vocabulary = vocab + + def transform(self, data: Iterable[str]) -> sp.csr_matrix: + """Transform a list of documents into a tfidf matrix. + The model must be fit before calling this method. + + Parameters + ---------- + data: + List of documents contents to transform. + """ + if not self.vocabulary: + raise ValueError("Vocabulary is empty. Call `fit` first.") + counts_records = [ + get_ngram_counts(doc, self.config.ngram_range) for doc in data + ] + counts_records = [ + Counter({k: v for k, v in doc.items() if k in self.vocabulary}) + for doc in counts_records + ] + tfidf = self._get_tfidf(counts_records, vocab=self.vocabulary) + if self.config.norm is not None: + return normalize_csr_rows(tfidf, norm=self.config.norm) + return tfidf + + def fit_transform(self, data: Iterable[str]) -> sp.csr_matrix: + """Fit the vectorizer to a list of documents and transform them + into a tfidf matrix. + + Parameters + ---------- + data: + List of documents contents to fit the vectorizer to and transform. + """ + self.fit(list(data)) + return self.transform(data) diff --git a/gimie/utils.py b/gimie/utils/uri.py similarity index 100% rename from gimie/utils.py rename to gimie/utils/uri.py diff --git a/poetry.lock b/poetry.lock index d234d903..8abc5c53 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,24 +13,17 @@ files = [ ] [[package]] -name = "attrs" -version = "23.1.0" -description = "Classes Without Boilerplate" +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, - {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, ] -[package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] - [[package]] name = "babel" version = "2.13.1" @@ -43,33 +36,14 @@ files = [ {file = "Babel-2.13.1.tar.gz", hash = "sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900"}, ] -[package.dependencies] -pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} -setuptools = {version = "*", markers = "python_version >= \"3.12\""} - [package.extras] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] -[[package]] -name = "banal" -version = "1.0.6" -description = "Commons of banal micro-functions for Python." -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "banal-1.0.6-py2.py3-none-any.whl", hash = "sha256:877aacb16b17f8fa4fd29a7c44515c5a23dc1a7b26078bc41dd34829117d85e1"}, - {file = "banal-1.0.6.tar.gz", hash = "sha256:2fe02c9305f53168441948f4a03dfbfa2eacc73db30db4a93309083cb0e250a5"}, -] - -[package.extras] -dev = ["mypy", "wheel"] - [[package]] name = "beautifulsoup4" version = "4.12.2" description = "Screen-scraping library" -category = "main" +category = "dev" optional = false python-versions = ">=3.6.0" files = [ @@ -84,21 +58,6 @@ soupsieve = ">1.2" html5lib = ["html5lib"] lxml = ["lxml"] -[[package]] -name = "binaryornot" -version = "0.4.4" -description = "Ultra-lightweight pure Python package to check if a file is binary or text." -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "binaryornot-0.4.4-py2.py3-none-any.whl", hash = "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4"}, - {file = "binaryornot-0.4.4.tar.gz", hash = "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061"}, -] - -[package.dependencies] -chardet = ">=3.0.2" - [[package]] name = "black" version = "22.12.0" @@ -135,18 +94,6 @@ d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] -[[package]] -name = "boolean-py" -version = "4.0" -description = "Define boolean algebras, create and parse boolean expressions and create custom boolean DSL." -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "boolean.py-4.0-py3-none-any.whl", hash = "sha256:2876f2051d7d6394a531d82dc6eb407faa0b01a0a0b3083817ccd7323b8d96bd"}, - {file = "boolean.py-4.0.tar.gz", hash = "sha256:17b9a181630e43dde1851d42bef546d616d5d9b4480357514597e78b203d06e4"}, -] - [[package]] name = "cachetools" version = "5.3.2" @@ -192,71 +139,6 @@ files = [ {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, ] -[[package]] -name = "cffi" -version = "1.16.0" -description = "Foreign Function Interface for Python calling C code." -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, - {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, - {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, - {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, - {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, - {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, - {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, - {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, - {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, - {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, - {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, - {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, - {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, - {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, - {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, -] - -[package.dependencies] -pycparser = "*" - [[package]] name = "cfgv" version = "3.4.0" @@ -269,18 +151,6 @@ files = [ {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] -[[package]] -name = "chardet" -version = "5.2.0" -description = "Universal encoding detector for Python 3" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, - {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, -] - [[package]] name = "charset-normalizer" version = "3.3.2" @@ -408,52 +278,6 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -[[package]] -name = "commoncode" -version = "31.0.3" -description = "Set of common utilities, originally split from ScanCode" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "commoncode-31.0.3-py3-none-any.whl", hash = "sha256:10bf51c8ba939fab81b2fbf7a689e84c9d8c049e8b5e9ae09b0c97f7b8541a1e"}, - {file = "commoncode-31.0.3.tar.gz", hash = "sha256:bab6b9e7f9bf89eb2a37a912626747075b1bb614875da156890efac159b29741"}, -] - -[package.dependencies] -attrs = ">=18.1,<20.1.0 || >20.1.0" -Beautifulsoup4 = ">=4.0.0" -click = ">=6.7,<7.0 || >7.0" -requests = ">=2.7.0" -saneyaml = ">=0.5.2" -text-unidecode = ">=1.0" - -[package.extras] -docs = ["Sphinx (>=5.0.2)", "doc8 (>=0.11.2)", "sphinx-reredirects (>=0.1.2)", "sphinx-rtd-theme (>=1.0.0)"] -testing = ["aboutcode-toolkit (>=7.0.2)", "black", "isort", "pycodestyle (>=2.8.0)", "pytest (>=6,!=7.0.0)", "pytest-xdist (>=2)", "twine"] - -[[package]] -name = "container-inspector" -version = "32.0.1" -description = "Docker, containers, rootfs and virtual machine related software composition analysis (SCA) utilities." -category = "main" -optional = false -python-versions = ">=3.7.*" -files = [ - {file = "container-inspector-32.0.1.tar.gz", hash = "sha256:6049eacf79f39c85c8dd7a6ce23a9466df4db7a7e69746fb203477080457c826"}, - {file = "container_inspector-32.0.1-py3-none-any.whl", hash = "sha256:e487ef243dc50055230072532dcb5504d0cc32ca3979ea3dee7e12d5ae5dc24c"}, -] - -[package.dependencies] -attrs = ">=18.1,<20.1.0 || >20.1.0" -click = ">=6.7,<7.0 || >7.0,<8.0.3 || >8.0.3" -commoncode = ">=30.2.0" -dockerfile-parse = "*" - -[package.extras] -docs = ["Sphinx (>=3.3.1)", "doc8 (>=0.8.1)", "sphinx-rtd-theme (>=0.5.0)"] -testing = ["aboutcode-toolkit (>=6.0.0)", "black", "pytest (>=6,!=7.0.0)", "pytest-xdist (>=2)", "twine"] - [[package]] name = "coverage" version = "6.5.0" @@ -540,72 +364,6 @@ requests = ">=1.0.0" [package.extras] yaml = ["PyYAML (>=3.10)"] -[[package]] -name = "cryptography" -version = "41.0.5" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797"}, - {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20"}, - {file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548"}, - {file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d"}, - {file = "cryptography-41.0.5-cp37-abi3-win32.whl", hash = "sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936"}, - {file = "cryptography-41.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84"}, - {file = "cryptography-41.0.5.tar.gz", hash = "sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7"}, -] - -[package.dependencies] -cffi = ">=1.12" - -[package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] -nox = ["nox"] -pep8test = ["black", "check-sdist", "mypy", "ruff"] -sdist = ["build"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] -test-randomorder = ["pytest-randomly"] - -[[package]] -name = "debian-inspector" -version = "31.0.0" -description = "Utilities to parse Debian package, copyright and control files." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "debian_inspector-31.0.0-py3-none-any.whl", hash = "sha256:db477fd2da64f832ad0d24797052055c44d530c79fa3df417b18cb00d26cfddc"}, - {file = "debian_inspector-31.0.0.tar.gz", hash = "sha256:46094f953464b269bb09855eadeee3c92cb6b487a0bfa26eba537b52cc3d6b47"}, -] - -[package.dependencies] -attrs = ">=19.2,<20.1.0 || >20.1.0" -chardet = ">=3.0.0" - -[package.extras] -docs = ["Sphinx (>=3.3.1)", "doc8 (>=0.8.1)", "sphinx-rtd-theme (>=0.5.0)", "sphinxcontrib-apidoc (>=0.3.0)"] -testing = ["aboutcode-toolkit (>=6.0.0)", "black", "commoncode", "pytest (>=6,!=7.0.0)", "pytest-xdist (>=2)"] - [[package]] name = "distlib" version = "0.3.7" @@ -618,18 +376,6 @@ files = [ {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, ] -[[package]] -name = "dockerfile-parse" -version = "2.0.1" -description = "Python library for Dockerfile manipulation" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "dockerfile-parse-2.0.1.tar.gz", hash = "sha256:3184ccdc513221983e503ac00e1aa504a2aa8f84e5de673c46b0b6eee99ec7bc"}, - {file = "dockerfile_parse-2.0.1-py2.py3-none-any.whl", hash = "sha256:bdffd126d2eb26acf1066acb54cb2e336682e1d72b974a40894fac76a4df17f6"}, -] - [[package]] name = "docopt" version = "0.6.2" @@ -653,26 +399,6 @@ files = [ {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, ] -[[package]] -name = "dparse2" -version = "0.7.0" -description = "A parser for Python dependency files" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "dparse2-0.7.0-py3-none-any.whl", hash = "sha256:2b935161700cdad4f27fa7ada85900756739be65ba3ef614ac4436e7ba929102"}, - {file = "dparse2-0.7.0.tar.gz", hash = "sha256:6bf6872aeaffedcac67ad0abb516630bad045dbdb58505b58d8f796ee91f0a73"}, -] - -[package.dependencies] -packvers = "*" -pyyaml = "*" -toml = "*" - -[package.extras] -pipenv = ["pipenv"] - [[package]] name = "exceptiongroup" version = "1.1.3" @@ -688,18 +414,6 @@ files = [ [package.extras] test = ["pytest (>=6)"] -[[package]] -name = "fasteners" -version = "0.19" -description = "A python package that provides useful locks" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "fasteners-0.19-py3-none-any.whl", hash = "sha256:758819cb5d94cdedf4e836988b74de396ceacb8e2794d21f82d131fd9ee77237"}, - {file = "fasteners-0.19.tar.gz", hash = "sha256:b4f37c3ac52d8a445af3a66bce57b33b5e90b97c696b7b984f530cf8f0ded09c"}, -] - [[package]] name = "filelock" version = "3.13.1" @@ -717,24 +431,6 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1 testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] typing = ["typing-extensions (>=4.8)"] -[[package]] -name = "fingerprints" -version = "1.2.3" -description = "A library to generate entity fingerprints." -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "fingerprints-1.2.3-py2.py3-none-any.whl", hash = "sha256:b8f83ad13dcdadce94903383db3b9b062b85a3a86f54f9e26d8faa97313f20bf"}, - {file = "fingerprints-1.2.3.tar.gz", hash = "sha256:1719f808ec8dd6c7b32c79129be3cc77dc2d2258008cd0236654862a86a78b97"}, -] - -[package.dependencies] -normality = ">=2.0.0,<=3.0.0" - -[package.extras] -dev = ["black", "bump2version", "mypy", "pytest", "pytest-cov", "pyyaml", "types-pyyaml"] - [[package]] name = "frozendict" version = "2.3.8" @@ -782,37 +478,6 @@ files = [ {file = "frozendict-2.3.8.tar.gz", hash = "sha256:5526559eca8f1780a4ee5146896f59afc31435313560208dd394a3a5e537d3ff"}, ] -[[package]] -name = "ftfy" -version = "6.1.1" -description = "Fixes mojibake and other problems with Unicode, after the fact" -category = "main" -optional = false -python-versions = ">=3.7,<4" -files = [ - {file = "ftfy-6.1.1-py3-none-any.whl", hash = "sha256:0ffd33fce16b54cccaec78d6ec73d95ad370e5df5a25255c8966a6147bd667ca"}, - {file = "ftfy-6.1.1.tar.gz", hash = "sha256:bfc2019f84fcd851419152320a6375604a0f1459c281b5b199b2cd0d2e727f8f"}, -] - -[package.dependencies] -wcwidth = ">=0.2.5" - -[[package]] -name = "gemfileparser2" -version = "0.9.3" -description = "Parse Ruby Gemfile, .gemspec and Cocoapod .podspec files using Python." -category = "main" -optional = false -python-versions = ">=3.6.*" -files = [ - {file = "gemfileparser2-0.9.3-py3-none-any.whl", hash = "sha256:6d19bd99a81dff98dafed4437f5194a383b4b22d6be1de2c92cb134a5a598152"}, - {file = "gemfileparser2-0.9.3.tar.gz", hash = "sha256:04528964e7f45b66f460d6ca2309eb9a8286bed3fc03a47d3eb52dee4602fc39"}, -] - -[package.extras] -docs = ["Sphinx (>=3.3.1)", "doc8 (>=0.8.1)", "sphinx-rtd-theme (>=0.5.0)"] -testing = ["aboutcode-toolkit (>=6.0.0)", "black", "pytest (>=6,!=7.0.0)", "pytest-xdist (>=2)"] - [[package]] name = "gitdb" version = "4.0.11" @@ -911,7 +576,7 @@ files = [ name = "importlib-metadata" version = "6.8.0" description = "Read metadata from Python packages" -category = "main" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -939,44 +604,6 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] -[[package]] -name = "intbitset" -version = "3.0.2" -description = "C-based extension implementing fast integer bit sets." -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "intbitset-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:768c6becf9a898355c044646c03f8ca71acfacf7de04f2aa43eb8f84ea619f17"}, - {file = "intbitset-3.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8f4d5b8666b2d3d69ec591ccbe16aad1f3673b8a2849ef26bb2d1f9f45ca9dc8"}, - {file = "intbitset-3.0.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:77772377d1d6aa4562a41ded600f1bc7804ee0506fc76ccf8fb2f4662df41371"}, - {file = "intbitset-3.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f779bcc0b267ce6d699d6004ae7a2ead5bea6a21c2362c9b03d69826bf4f7ed8"}, - {file = "intbitset-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:7643345a8ee1aaf228ec4b480964e1737b810f9c611d3c8960afe35d5e88daea"}, - {file = "intbitset-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:012cf6209431e0bce3dd7579fa62cff07e7b225c65bc17b5d03852c7d57c1e86"}, - {file = "intbitset-3.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2e2f20dfaeefa73a9d3cf60af32c69cbcaa6beb3ea86071cb8c827462a13623"}, - {file = "intbitset-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666d06ff5935ba5a98cbaa2f76b516c380b55e510a427c02f74f50d571d356f3"}, - {file = "intbitset-3.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ed3d4c0a67a3db24379b17d47a0a91ffd40eb700a7577b5df05246702dd39dab"}, - {file = "intbitset-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:1b10313835a204476f2ba253fb4e582be5a29601e1c2b7522dace97f22d0309e"}, - {file = "intbitset-3.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7c3a340ad58e6918c64abd9d26042178c98ff44e7ab3e7fc542df223d43a8e1e"}, - {file = "intbitset-3.0.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6802771924237ed645adffb1079d258a99475427bd9152c9bd36be295068a84"}, - {file = "intbitset-3.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ca2d76a4b7bf4b1b64f4b6cd462a231ae819af350db373c42f8e975500ebfc20"}, - {file = "intbitset-3.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b234fb045c97b3226fd8657c298804bd2c5a7af0cf6d42878d3b8a75cace02f1"}, - {file = "intbitset-3.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:65904b7aad560ecedb67f3b0c302dca2c48cbad7be171b79105e82a8bcfed62a"}, - {file = "intbitset-3.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5361fbc087a6c1e4b8a6fefdfd48c82744416b9444324090d1109966fe659ac3"}, - {file = "intbitset-3.0.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b33d76a5fbb9880187ab6d7ebe2399ee723dd7535f1526095bc4d2b7af9d255"}, - {file = "intbitset-3.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e303dc1ffcfb178b1aa05322d0b0ac4af66ad5dc42edf3054063dd66d99d437f"}, - {file = "intbitset-3.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:9cc0350936fc813721a0a40a046b87118405cdd78c4f546171c539e8facb713d"}, - {file = "intbitset-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c6cc457ed31adbdc0c6acb6ea38f63f01084a868a6260a756660f11c74aa6b2e"}, - {file = "intbitset-3.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f979f04ed1689402b1fca7054e212962b6c3b650c2cf12b80026d8c1843e3ce2"}, - {file = "intbitset-3.0.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a2ca7e7b95fb7c4bf35156f196117466984e56a10616ebba9a646bc6e228af58"}, - {file = "intbitset-3.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6236f8488e51de6efcf1720638b4583614dbcf5b542417b7e0e91f40f2131870"}, - {file = "intbitset-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:45c70f876e42a288be781ebd0456a80897044f4b6d0bff55f8e24077dc2eb1ae"}, - {file = "intbitset-3.0.2.tar.gz", hash = "sha256:a300b2d5a4989857ff1d0c3971624766a89a751e315aa080c07865031ae637a7"}, -] - -[package.extras] -tests = ["pytest", "pytest-xdist"] - [[package]] name = "isodate" version = "0.6.1" @@ -992,43 +619,11 @@ files = [ [package.dependencies] six = "*" -[[package]] -name = "jaraco-functools" -version = "3.9.0" -description = "Functools like those found in stdlib" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jaraco.functools-3.9.0-py3-none-any.whl", hash = "sha256:df2e2b0aadd2dfcee2d7e0d7d083d5a5b68f4c8621e6915ae9819a90de65dd44"}, - {file = "jaraco.functools-3.9.0.tar.gz", hash = "sha256:8b137b0feacc17fef4bacee04c011c9e86f2341099c870a1d12d3be37b32a638"}, -] - -[package.dependencies] -more-itertools = "*" -typing-extensions = {version = "*", markers = "python_version < \"3.11\""} - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["jaraco.classes", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] - -[[package]] -name = "javaproperties" -version = "0.8.1" -description = "Read & write Java .properties files" -category = "main" -optional = false -python-versions = "~=3.6" -files = [ - {file = "javaproperties-0.8.1-py3-none-any.whl", hash = "sha256:0e9b43334d6c1a9bffe34e2ece52588e21a7e099869bdaa481a5c6498774e18e"}, - {file = "javaproperties-0.8.1.tar.gz", hash = "sha256:9dcba389effe67d3f906bbdcc64b8ef2ee8eac00072406784ea636bb6ba56061"}, -] - [[package]] name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1042,25 +637,6 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] -[[package]] -name = "jsonstreams" -version = "0.6.0" -description = "A JSON streaming writer" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" -files = [ - {file = "jsonstreams-0.6.0-py2.py3-none-any.whl", hash = "sha256:b2e609c2bc17eec77fe26dae4d32556ba59dafbbff30c9a4909f2e19fa5bb000"}, - {file = "jsonstreams-0.6.0.tar.gz", hash = "sha256:721cda7391e9415b7b15cebd6cf92fc7f8788ca211eda7d64162a066ee45a72e"}, -] - -[package.dependencies] -six = "*" - -[package.extras] -recomended = ["simplejson"] -test = ["tox"] - [[package]] name = "lazy-object-proxy" version = "1.9.0" @@ -1107,25 +683,6 @@ files = [ {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, ] -[[package]] -name = "license-expression" -version = "30.1.1" -description = "license-expression is a comprehensive utility library to parse, compare, simplify and normalize license expressions (such as SPDX license expressions) using boolean logic." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "license-expression-30.1.1.tar.gz", hash = "sha256:42375df653ad85e6f5b4b0385138b2dbea1f5d66360783d8625c3e4f97f11f0c"}, - {file = "license_expression-30.1.1-py3-none-any.whl", hash = "sha256:8d7e5e2de0d04fc104a4f952c440e8f08a5ba63480a0dad015b294770b7e58ec"}, -] - -[package.dependencies] -"boolean.py" = ">=4.0" - -[package.extras] -docs = ["Sphinx (==5.1.0)", "doc8 (>=0.8.1)", "sphinx-rtd-theme (>=0.5.0)", "sphinxcontrib-apidoc (>=0.3.0)"] -testing = ["black", "isort", "pytest (>=6,!=7.0.0)", "pytest-xdist (>=2)", "twine"] - [[package]] name = "lizard" version = "1.17.10" @@ -1275,7 +832,7 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] name = "markupsafe" version = "2.1.3" description = "Safely add untrusted strings to HTML/XML markup." -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1384,18 +941,6 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] -[[package]] -name = "more-itertools" -version = "10.1.0" -description = "More routines for operating on iterables, beyond itertools" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "more-itertools-10.1.0.tar.gz", hash = "sha256:626c369fa0eb37bac0291bce8259b332fd59ac792fa5497b59837309cd5b114a"}, - {file = "more_itertools-10.1.0-py3-none-any.whl", hash = "sha256:64e0735fcfdc6f3464ea133afe8ea4483b1c5fe3a3d69852e6503b43a0b222e6"}, -] - [[package]] name = "mypy-extensions" version = "1.0.0" @@ -1451,27 +996,47 @@ files = [ setuptools = "*" [[package]] -name = "normality" -version = "2.5.0" -description = "Micro-library to normalize text strings" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "normality-2.5.0-py2.py3-none-any.whl", hash = "sha256:d9f48daf32e351e88b9e372787c1da437df9d0d818aec6e2834b02102378df62"}, - {file = "normality-2.5.0.tar.gz", hash = "sha256:a55133e972b81c4a3bf8b6dc419f262f94a4fd6f636297046f74d35c93abe153"}, +name = "numpy" +version = "1.26.1" +description = "Fundamental package for array computing in Python" +category = "main" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "numpy-1.26.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82e871307a6331b5f09efda3c22e03c095d957f04bf6bc1804f30048d0e5e7af"}, + {file = "numpy-1.26.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdd9ec98f0063d93baeb01aad472a1a0840dee302842a2746a7a8e92968f9575"}, + {file = "numpy-1.26.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d78f269e0c4fd365fc2992c00353e4530d274ba68f15e968d8bc3c69ce5f5244"}, + {file = "numpy-1.26.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ab9163ca8aeb7fd32fe93866490654d2f7dda4e61bc6297bf72ce07fdc02f67"}, + {file = "numpy-1.26.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:78ca54b2f9daffa5f323f34cdf21e1d9779a54073f0018a3094ab907938331a2"}, + {file = "numpy-1.26.1-cp310-cp310-win32.whl", hash = "sha256:d1cfc92db6af1fd37a7bb58e55c8383b4aa1ba23d012bdbba26b4bcca45ac297"}, + {file = "numpy-1.26.1-cp310-cp310-win_amd64.whl", hash = "sha256:d2984cb6caaf05294b8466966627e80bf6c7afd273279077679cb010acb0e5ab"}, + {file = "numpy-1.26.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cd7837b2b734ca72959a1caf3309457a318c934abef7a43a14bb984e574bbb9a"}, + {file = "numpy-1.26.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1c59c046c31a43310ad0199d6299e59f57a289e22f0f36951ced1c9eac3665b9"}, + {file = "numpy-1.26.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d58e8c51a7cf43090d124d5073bc29ab2755822181fcad978b12e144e5e5a4b3"}, + {file = "numpy-1.26.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6081aed64714a18c72b168a9276095ef9155dd7888b9e74b5987808f0dd0a974"}, + {file = "numpy-1.26.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:97e5d6a9f0702c2863aaabf19f0d1b6c2628fbe476438ce0b5ce06e83085064c"}, + {file = "numpy-1.26.1-cp311-cp311-win32.whl", hash = "sha256:b9d45d1dbb9de84894cc50efece5b09939752a2d75aab3a8b0cef6f3a35ecd6b"}, + {file = "numpy-1.26.1-cp311-cp311-win_amd64.whl", hash = "sha256:3649d566e2fc067597125428db15d60eb42a4e0897fc48d28cb75dc2e0454e53"}, + {file = "numpy-1.26.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1d1bd82d539607951cac963388534da3b7ea0e18b149a53cf883d8f699178c0f"}, + {file = "numpy-1.26.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:afd5ced4e5a96dac6725daeb5242a35494243f2239244fad10a90ce58b071d24"}, + {file = "numpy-1.26.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03fb25610ef560a6201ff06df4f8105292ba56e7cdd196ea350d123fc32e24e"}, + {file = "numpy-1.26.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcfaf015b79d1f9f9c9fd0731a907407dc3e45769262d657d754c3a028586124"}, + {file = "numpy-1.26.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e509cbc488c735b43b5ffea175235cec24bbc57b227ef1acc691725beb230d1c"}, + {file = "numpy-1.26.1-cp312-cp312-win32.whl", hash = "sha256:af22f3d8e228d84d1c0c44c1fbdeb80f97a15a0abe4f080960393a00db733b66"}, + {file = "numpy-1.26.1-cp312-cp312-win_amd64.whl", hash = "sha256:9f42284ebf91bdf32fafac29d29d4c07e5e9d1af862ea73686581773ef9e73a7"}, + {file = "numpy-1.26.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bb894accfd16b867d8643fc2ba6c8617c78ba2828051e9a69511644ce86ce83e"}, + {file = "numpy-1.26.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e44ccb93f30c75dfc0c3aa3ce38f33486a75ec9abadabd4e59f114994a9c4617"}, + {file = "numpy-1.26.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9696aa2e35cc41e398a6d42d147cf326f8f9d81befcb399bc1ed7ffea339b64e"}, + {file = "numpy-1.26.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5b411040beead47a228bde3b2241100454a6abde9df139ed087bd73fc0a4908"}, + {file = "numpy-1.26.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1e11668d6f756ca5ef534b5be8653d16c5352cbb210a5c2a79ff288e937010d5"}, + {file = "numpy-1.26.1-cp39-cp39-win32.whl", hash = "sha256:d1d2c6b7dd618c41e202c59c1413ef9b2c8e8a15f5039e344af64195459e3104"}, + {file = "numpy-1.26.1-cp39-cp39-win_amd64.whl", hash = "sha256:59227c981d43425ca5e5c01094d59eb14e8772ce6975d4b2fc1e106a833d5ae2"}, + {file = "numpy-1.26.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:06934e1a22c54636a059215d6da99e23286424f316fddd979f5071093b648668"}, + {file = "numpy-1.26.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76ff661a867d9272cd2a99eed002470f46dbe0943a5ffd140f49be84f68ffc42"}, + {file = "numpy-1.26.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:6965888d65d2848e8768824ca8288db0a81263c1efccec881cb35a0d805fcd2f"}, + {file = "numpy-1.26.1.tar.gz", hash = "sha256:c8c6c72d4a9f831f328efb1312642a1cafafaa88981d9ab76368d50d07d93cbe"}, ] -[package.dependencies] -banal = ">=1.0.1" -chardet = "*" -charset-normalizer = ">=2.0.0" -text-unidecode = "*" - -[package.extras] -dev = ["mypy", "pyicu (>=1.9.3)", "pytest", "types-chardet"] -icu = ["pyicu (>=1.9.3)"] - [[package]] name = "owlrl" version = "6.0.2" @@ -1487,24 +1052,6 @@ files = [ [package.dependencies] rdflib = ">=6.0.2" -[[package]] -name = "packageurl-python" -version = "0.11.2" -description = "A purl aka. Package URL parser and builder" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "packageurl-python-0.11.2.tar.gz", hash = "sha256:01fbf74a41ef85cf413f1ede529a1411f658bda66ed22d45d27280ad9ceba471"}, - {file = "packageurl_python-0.11.2-py3-none-any.whl", hash = "sha256:799acfe8d9e6e3534bbc19660be97d5b66754bc033e62c39f1e2f16323fcfa84"}, -] - -[package.extras] -build = ["wheel"] -lint = ["black", "isort", "mypy"] -sqlalchemy = ["sqlalchemy (>=2.0.0)"] -test = ["pytest"] - [[package]] name = "packaging" version = "23.2" @@ -1517,33 +1064,6 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] -[[package]] -name = "packvers" -version = "21.5" -description = "Core utilities for Python packages. Fork to support LegacyVersion" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "packvers-21.5-py3-none-any.whl", hash = "sha256:a05e4a2b0f2eecb49d2568bfe180168a99165ab5167aa791f82266e33740ac87"}, - {file = "packvers-21.5.tar.gz", hash = "sha256:2d2758fc09d2c325414354b8478d649f878b52c38598517fba51c8623526ca79"}, -] - -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" - -[[package]] -name = "parameter-expansion-patched" -version = "0.3.1" -description = "Shell parameter expansion in Python. Patched by co-maintainer for a PyPI release." -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "parameter-expansion-patched-0.3.1.tar.gz", hash = "sha256:ff5dbc89fbde582f3336562d196b710771e92baa7b6d59356a14b085a0b6740b"}, - {file = "parameter_expansion_patched-0.3.1-py3-none-any.whl", hash = "sha256:832f04bed2a81e32d9d233cbe27448a7a22edf9a744086dbd01066c41ad0f535"}, -] - [[package]] name = "pathspec" version = "0.11.2" @@ -1556,71 +1076,6 @@ files = [ {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, ] -[[package]] -name = "pdfminer-six" -version = "20221105" -description = "PDF parser and analyzer" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "pdfminer.six-20221105-py3-none-any.whl", hash = "sha256:1eaddd712d5b2732f8ac8486824533514f8ba12a0787b3d5fe1e686cd826532d"}, - {file = "pdfminer.six-20221105.tar.gz", hash = "sha256:8448ab7b939d18b64820478ecac5394f482d7a79f5f7eaa7703c6c959c175e1d"}, -] - -[package.dependencies] -charset-normalizer = ">=2.0.0" -cryptography = ">=36.0.0" - -[package.extras] -dev = ["black", "mypy (==0.931)", "nox", "pytest"] -docs = ["sphinx", "sphinx-argparse"] -image = ["Pillow"] - -[[package]] -name = "pefile" -version = "2023.2.7" -description = "Python PE parsing module" -category = "main" -optional = false -python-versions = ">=3.6.0" -files = [ - {file = "pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"}, - {file = "pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc"}, -] - -[[package]] -name = "pip-requirements-parser" -version = "32.0.1" -description = "pip requirements parser - a mostly correct pip requirements parsing library because it uses pip's own code." -category = "main" -optional = false -python-versions = ">=3.6.0" -files = [ - {file = "pip-requirements-parser-32.0.1.tar.gz", hash = "sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3"}, - {file = "pip_requirements_parser-32.0.1-py3-none-any.whl", hash = "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526"}, -] - -[package.dependencies] -packaging = "*" -pyparsing = "*" - -[package.extras] -docs = ["Sphinx (>=3.3.1)", "doc8 (>=0.8.1)", "sphinx-rtd-theme (>=0.5.0)"] -testing = ["aboutcode-toolkit (>=6.0.0)", "black", "pytest (>=6,!=7.0.0)", "pytest-xdist (>=2)"] - -[[package]] -name = "pkginfo2" -version = "30.0.0" -description = "Query metadatdata from sdists / bdists / installed packages. Safer fork of pkginfo to avoid doing arbitrary imports and eval()" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "pkginfo2-30.0.0-py3-none-any.whl", hash = "sha256:f1558f3ff71c99e8f362b6d079c15ef334dfce8ab2bc623a992341baeb1e7248"}, - {file = "pkginfo2-30.0.0.tar.gz", hash = "sha256:5e1afbeb156febb407a9b5c16b51c5b4737c529eeda2b1607e1e277cf260669c"}, -] - [[package]] name = "platformdirs" version = "3.11.0" @@ -1641,7 +1096,7 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co name = "pluggy" version = "1.3.0" description = "plugin and hook calling mechanisms for python" -category = "main" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1653,39 +1108,6 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "plugincode" -version = "32.0.0" -description = "plugincode is a library that provides plugin functionality for ScanCode toolkit." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "plugincode-32.0.0-py3-none-any.whl", hash = "sha256:344bb9943fcf4d6d05669c3c61efd4093fffa6a290fba7c5c11db15f2b51305e"}, - {file = "plugincode-32.0.0.tar.gz", hash = "sha256:4132d93b1755271c6e226c9da2e2044ff62ebcb873b5e958d66a8ddde9f345fa"}, -] - -[package.dependencies] -click = ">=6.7,<7.0 || >7.0" -commoncode = ">=31.0.0" -pluggy = ">=0.12.0" - -[package.extras] -docs = ["Sphinx (>=3.3.1)", "doc8 (>=0.8.1)", "sphinx-rtd-theme (>=0.5.0)"] -testing = ["aboutcode-toolkit (>=7.0.2)", "black", "isort", "pycodestyle (>=2.8.0)", "pytest (>=6,!=7.0.0)", "pytest-xdist (>=2)", "twine"] - -[[package]] -name = "ply" -version = "3.11" -description = "Python Lex & Yacc" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"}, - {file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"}, -] - [[package]] name = "pre-commit" version = "3.5.0" @@ -1724,67 +1146,144 @@ wcwidth = "*" tests = ["pytest", "pytest-cov", "pytest-lazy-fixture"] [[package]] -name = "publicsuffix2" -version = "2.20191221" -description = "Get a public suffix for a domain name using the Public Suffix List. Forked from and using the same API as the publicsuffix package." +name = "pydantic" +version = "2.4.2" +description = "Data validation using Python type hints" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "publicsuffix2-2.20191221-py2.py3-none-any.whl", hash = "sha256:786b5e36205b88758bd3518725ec8cfe7a8173f5269354641f581c6b80a99893"}, - {file = "publicsuffix2-2.20191221.tar.gz", hash = "sha256:00f8cc31aa8d0d5592a5ced19cccba7de428ebca985db26ac852d920ddd6fe7b"}, + {file = "pydantic-2.4.2-py3-none-any.whl", hash = "sha256:bc3ddf669d234f4220e6e1c4d96b061abe0998185a8d7855c0126782b7abc8c1"}, + {file = "pydantic-2.4.2.tar.gz", hash = "sha256:94f336138093a5d7f426aac732dcfe7ab4eb4da243c88f891d65deb4a2556ee7"}, ] -[[package]] -name = "pyahocorasick" -version = "2.0.0" -description = "pyahocorasick is a fast and memory efficient library for exact or approximate multi-pattern string search. With the ``ahocorasick.Automaton`` class, you can find multiple key string occurrences at once in some input text. You can use it as a plain dict-like Trie or convert a Trie to an automaton for efficient Aho-Corasick search. And pickle to disk for easy reuse of large automatons. Implemented in C and tested on Python 3.6+. Works on Linux, macOS and Windows. BSD-3-Cause license." -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "pyahocorasick-2.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e20e58e37ed21ce1007c3753ce462f878b7dc68ae5ff693832fdc390d6537aff"}, - {file = "pyahocorasick-2.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:04f209fb96b824474185e42a8cc099fd0580d504049359b5296632bbe404dd7d"}, - {file = "pyahocorasick-2.0.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cd731fbe632edd32c1e4fdf16dd85ceba9bf3bf9793ff70c573d20156e812a29"}, - {file = "pyahocorasick-2.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8f4b43156b200df99b197d856778f94b489a559a65b7948ebdea67f91e09b20f"}, - {file = "pyahocorasick-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:4086ba7bb0ab5060661543f9503faac1e4648468c20aa05a986d5af4ed1a423b"}, - {file = "pyahocorasick-2.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1a322cd6c0554caca957cbdf60c3a52b7e57c8e9fb2ac65e6a0f27e86b7e628b"}, - {file = "pyahocorasick-2.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4058b0c35175bedd0d097369d7ef752d03d1e5731d1fda439a7644d90fd4089a"}, - {file = "pyahocorasick-2.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:644a2bc91aaef019242a9530bbd832e31a2388deb2d54587d238cbace13898c4"}, - {file = "pyahocorasick-2.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f455e517ddef82fc6891d0ffb82f60070ccc13c425d431b82082e5bdd226ac1f"}, - {file = "pyahocorasick-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:a3b7059515bb2f6baed72ce20b30ff877a1f5b15acb4590e2af5db9fb2bb8314"}, - {file = "pyahocorasick-2.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8f3d68ad21aa63dae116d76c98f160454c199501c592e1cf45d013074ecf79f8"}, - {file = "pyahocorasick-2.0.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a44ab4e11a2c3f292506f2b3d5afefa19d01a3172d6209f4eb2827802a7c3630"}, - {file = "pyahocorasick-2.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:675d0f47b1369748ce8c3b746dba0496022523547aa80f330b45d7e7cf446f5f"}, - {file = "pyahocorasick-2.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:67c53f8709611754013541fa781e2dc2aa4653a3d472c8e4789ffa94d9482db3"}, - {file = "pyahocorasick-2.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fa1ef232f8e816b7fe637e2f60ebc74c231e50b0315c88c841e8fd16ceb31592"}, - {file = "pyahocorasick-2.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c706c28423816611cc683ad4082d59619a6bb2dbdff3a61d62d7f8b9d353529a"}, - {file = "pyahocorasick-2.0.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:67f2f85d78290c7de0c770e6c28d8868859950088781440485cffbfeed4c1f8d"}, - {file = "pyahocorasick-2.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9e0631784addce1aabe2bd4696b7b557b71b937b24ffe28a56877baf6c35c86b"}, - {file = "pyahocorasick-2.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:11adffb8ce4015e36f766afc77bf0c121af052f437049e2f87e10f1bcac43d25"}, - {file = "pyahocorasick-2.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9645f4b8f42b27adad6839f77baf3d48d2db50709a72fc35ce74017038aa5545"}, - {file = "pyahocorasick-2.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bd7adbd4a2088a74e8e78995a8b654c0ead0eb4a47e871c231c502e8c2deaa65"}, - {file = "pyahocorasick-2.0.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:11feb62defff120415a54fab7ae47e27453014614b5fcd732ce93c247c9ab770"}, - {file = "pyahocorasick-2.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5081c010c64117965b6bb40e19674bf08f71dea2ddde51795591e427ac261325"}, - {file = "pyahocorasick-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:bb1414ec05e134aca5fd037132de87ea28878573516f44772e90be407882b908"}, - {file = "pyahocorasick-2.0.0.tar.gz", hash = "sha256:2985cac6d99c0e9165617fe154b4db0b50c4c2819791c2ad5f0aac0c6a6e58c5"}, -] +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.10.1" +typing-extensions = ">=4.6.1" [package.extras] -testing = ["pytest", "setuptools", "twine", "wheel"] +email = ["email-validator (>=2.0.0)"] [[package]] -name = "pycparser" -version = "2.21" -description = "C parser in Python" +name = "pydantic-core" +version = "2.10.1" +description = "" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.7" files = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, + {file = "pydantic_core-2.10.1-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:d64728ee14e667ba27c66314b7d880b8eeb050e58ffc5fec3b7a109f8cddbd63"}, + {file = "pydantic_core-2.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:48525933fea744a3e7464c19bfede85df4aba79ce90c60b94d8b6e1eddd67096"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef337945bbd76cce390d1b2496ccf9f90b1c1242a3a7bc242ca4a9fc5993427a"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1392e0638af203cee360495fd2cfdd6054711f2db5175b6e9c3c461b76f5175"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0675ba5d22de54d07bccde38997e780044dcfa9a71aac9fd7d4d7a1d2e3e65f7"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:128552af70a64660f21cb0eb4876cbdadf1a1f9d5de820fed6421fa8de07c893"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f6e6aed5818c264412ac0598b581a002a9f050cb2637a84979859e70197aa9e"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ecaac27da855b8d73f92123e5f03612b04c5632fd0a476e469dfc47cd37d6b2e"}, + {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b3c01c2fb081fced3bbb3da78510693dc7121bb893a1f0f5f4b48013201f362e"}, + {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:92f675fefa977625105708492850bcbc1182bfc3e997f8eecb866d1927c98ae6"}, + {file = "pydantic_core-2.10.1-cp310-none-win32.whl", hash = "sha256:420a692b547736a8d8703c39ea935ab5d8f0d2573f8f123b0a294e49a73f214b"}, + {file = "pydantic_core-2.10.1-cp310-none-win_amd64.whl", hash = "sha256:0880e239827b4b5b3e2ce05e6b766a7414e5f5aedc4523be6b68cfbc7f61c5d0"}, + {file = "pydantic_core-2.10.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:073d4a470b195d2b2245d0343569aac7e979d3a0dcce6c7d2af6d8a920ad0bea"}, + {file = "pydantic_core-2.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:600d04a7b342363058b9190d4e929a8e2e715c5682a70cc37d5ded1e0dd370b4"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39215d809470f4c8d1881758575b2abfb80174a9e8daf8f33b1d4379357e417c"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eeb3d3d6b399ffe55f9a04e09e635554012f1980696d6b0aca3e6cf42a17a03b"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a7902bf75779bc12ccfc508bfb7a4c47063f748ea3de87135d433a4cca7a2f"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3625578b6010c65964d177626fde80cf60d7f2e297d56b925cb5cdeda6e9925a"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:caa48fc31fc7243e50188197b5f0c4228956f97b954f76da157aae7f67269ae8"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:07ec6d7d929ae9c68f716195ce15e745b3e8fa122fc67698ac6498d802ed0fa4"}, + {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e6f31a17acede6a8cd1ae2d123ce04d8cca74056c9d456075f4f6f85de055607"}, + {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d8f1ebca515a03e5654f88411420fea6380fc841d1bea08effb28184e3d4899f"}, + {file = "pydantic_core-2.10.1-cp311-none-win32.whl", hash = "sha256:6db2eb9654a85ada248afa5a6db5ff1cf0f7b16043a6b070adc4a5be68c716d6"}, + {file = "pydantic_core-2.10.1-cp311-none-win_amd64.whl", hash = "sha256:4a5be350f922430997f240d25f8219f93b0c81e15f7b30b868b2fddfc2d05f27"}, + {file = "pydantic_core-2.10.1-cp311-none-win_arm64.whl", hash = "sha256:5fdb39f67c779b183b0c853cd6b45f7db84b84e0571b3ef1c89cdb1dfc367325"}, + {file = "pydantic_core-2.10.1-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:b1f22a9ab44de5f082216270552aa54259db20189e68fc12484873d926426921"}, + {file = "pydantic_core-2.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8572cadbf4cfa95fb4187775b5ade2eaa93511f07947b38f4cd67cf10783b118"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db9a28c063c7c00844ae42a80203eb6d2d6bbb97070cfa00194dff40e6f545ab"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2a35baa428181cb2270a15864ec6286822d3576f2ed0f4cd7f0c1708472aff"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05560ab976012bf40f25d5225a58bfa649bb897b87192a36c6fef1ab132540d7"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6495008733c7521a89422d7a68efa0a0122c99a5861f06020ef5b1f51f9ba7c"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ac492c686defc8e6133e3a2d9eaf5261b3df26b8ae97450c1647286750b901"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8282bab177a9a3081fd3d0a0175a07a1e2bfb7fcbbd949519ea0980f8a07144d"}, + {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:aafdb89fdeb5fe165043896817eccd6434aee124d5ee9b354f92cd574ba5e78f"}, + {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f6defd966ca3b187ec6c366604e9296f585021d922e666b99c47e78738b5666c"}, + {file = "pydantic_core-2.10.1-cp312-none-win32.whl", hash = "sha256:7c4d1894fe112b0864c1fa75dffa045720a194b227bed12f4be7f6045b25209f"}, + {file = "pydantic_core-2.10.1-cp312-none-win_amd64.whl", hash = "sha256:5994985da903d0b8a08e4935c46ed8daf5be1cf217489e673910951dc533d430"}, + {file = "pydantic_core-2.10.1-cp312-none-win_arm64.whl", hash = "sha256:0d8a8adef23d86d8eceed3e32e9cca8879c7481c183f84ed1a8edc7df073af94"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:9badf8d45171d92387410b04639d73811b785b5161ecadabf056ea14d62d4ede"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:ebedb45b9feb7258fac0a268a3f6bec0a2ea4d9558f3d6f813f02ff3a6dc6698"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfe1090245c078720d250d19cb05d67e21a9cd7c257698ef139bc41cf6c27b4f"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e357571bb0efd65fd55f18db0a2fb0ed89d0bb1d41d906b138f088933ae618bb"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3dcd587b69bbf54fc04ca157c2323b8911033e827fffaecf0cafa5a892a0904"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c120c9ce3b163b985a3b966bb701114beb1da4b0468b9b236fc754783d85aa3"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15d6bca84ffc966cc9976b09a18cf9543ed4d4ecbd97e7086f9ce9327ea48891"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cabb9710f09d5d2e9e2748c3e3e20d991a4c5f96ed8f1132518f54ab2967221"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:82f55187a5bebae7d81d35b1e9aaea5e169d44819789837cdd4720d768c55d15"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1d40f55222b233e98e3921df7811c27567f0e1a4411b93d4c5c0f4ce131bc42f"}, + {file = "pydantic_core-2.10.1-cp37-none-win32.whl", hash = "sha256:14e09ff0b8fe6e46b93d36a878f6e4a3a98ba5303c76bb8e716f4878a3bee92c"}, + {file = "pydantic_core-2.10.1-cp37-none-win_amd64.whl", hash = "sha256:1396e81b83516b9d5c9e26a924fa69164156c148c717131f54f586485ac3c15e"}, + {file = "pydantic_core-2.10.1-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:6835451b57c1b467b95ffb03a38bb75b52fb4dc2762bb1d9dbed8de31ea7d0fc"}, + {file = "pydantic_core-2.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b00bc4619f60c853556b35f83731bd817f989cba3e97dc792bb8c97941b8053a"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa467fd300a6f046bdb248d40cd015b21b7576c168a6bb20aa22e595c8ffcdd"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d99277877daf2efe074eae6338453a4ed54a2d93fb4678ddfe1209a0c93a2468"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa7db7558607afeccb33c0e4bf1c9a9a835e26599e76af6fe2fcea45904083a6"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aad7bd686363d1ce4ee930ad39f14e1673248373f4a9d74d2b9554f06199fb58"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:443fed67d33aa85357464f297e3d26e570267d1af6fef1c21ca50921d2976302"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:042462d8d6ba707fd3ce9649e7bf268633a41018d6a998fb5fbacb7e928a183e"}, + {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ecdbde46235f3d560b18be0cb706c8e8ad1b965e5c13bbba7450c86064e96561"}, + {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ed550ed05540c03f0e69e6d74ad58d026de61b9eaebebbaaf8873e585cbb18de"}, + {file = "pydantic_core-2.10.1-cp38-none-win32.whl", hash = "sha256:8cdbbd92154db2fec4ec973d45c565e767ddc20aa6dbaf50142676484cbff8ee"}, + {file = "pydantic_core-2.10.1-cp38-none-win_amd64.whl", hash = "sha256:9f6f3e2598604956480f6c8aa24a3384dbf6509fe995d97f6ca6103bb8c2534e"}, + {file = "pydantic_core-2.10.1-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:655f8f4c8d6a5963c9a0687793da37b9b681d9ad06f29438a3b2326d4e6b7970"}, + {file = "pydantic_core-2.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e570ffeb2170e116a5b17e83f19911020ac79d19c96f320cbfa1fa96b470185b"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64322bfa13e44c6c30c518729ef08fda6026b96d5c0be724b3c4ae4da939f875"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:485a91abe3a07c3a8d1e082ba29254eea3e2bb13cbbd4351ea4e5a21912cc9b0"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7c2b8eb9fc872e68b46eeaf835e86bccc3a58ba57d0eedc109cbb14177be531"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5cb87bdc2e5f620693148b5f8f842d293cae46c5f15a1b1bf7ceeed324a740c"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25bd966103890ccfa028841a8f30cebcf5875eeac8c4bde4fe221364c92f0c9a"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f323306d0556351735b54acbf82904fe30a27b6a7147153cbe6e19aaaa2aa429"}, + {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0c27f38dc4fbf07b358b2bc90edf35e82d1703e22ff2efa4af4ad5de1b3833e7"}, + {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f1365e032a477c1430cfe0cf2856679529a2331426f8081172c4a74186f1d595"}, + {file = "pydantic_core-2.10.1-cp39-none-win32.whl", hash = "sha256:a1c311fd06ab3b10805abb72109f01a134019739bd3286b8ae1bc2fc4e50c07a"}, + {file = "pydantic_core-2.10.1-cp39-none-win_amd64.whl", hash = "sha256:ae8a8843b11dc0b03b57b52793e391f0122e740de3df1474814c700d2622950a"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d43002441932f9a9ea5d6f9efaa2e21458221a3a4b417a14027a1d530201ef1b"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fcb83175cc4936a5425dde3356f079ae03c0802bbdf8ff82c035f8a54b333521"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:962ed72424bf1f72334e2f1e61b68f16c0e596f024ca7ac5daf229f7c26e4208"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cf5bb4dd67f20f3bbc1209ef572a259027c49e5ff694fa56bed62959b41e1f9"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e544246b859f17373bed915182ab841b80849ed9cf23f1f07b73b7c58baee5fb"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c0877239307b7e69d025b73774e88e86ce82f6ba6adf98f41069d5b0b78bd1bf"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:53df009d1e1ba40f696f8995683e067e3967101d4bb4ea6f667931b7d4a01357"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a1254357f7e4c82e77c348dabf2d55f1d14d19d91ff025004775e70a6ef40ada"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:524ff0ca3baea164d6d93a32c58ac79eca9f6cf713586fdc0adb66a8cdeab96a"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f0ac9fb8608dbc6eaf17956bf623c9119b4db7dbb511650910a82e261e6600f"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:320f14bd4542a04ab23747ff2c8a778bde727158b606e2661349557f0770711e"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63974d168b6233b4ed6a0046296803cb13c56637a7b8106564ab575926572a55"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:417243bf599ba1f1fef2bb8c543ceb918676954734e2dcb82bf162ae9d7bd514"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dda81e5ec82485155a19d9624cfcca9be88a405e2857354e5b089c2a982144b2"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:14cfbb00959259e15d684505263d5a21732b31248a5dd4941f73a3be233865b9"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:631cb7415225954fdcc2a024119101946793e5923f6c4d73a5914d27eb3d3a05"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:bec7dd208a4182e99c5b6c501ce0b1f49de2802448d4056091f8e630b28e9a52"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:149b8a07712f45b332faee1a2258d8ef1fb4a36f88c0c17cb687f205c5dc6e7d"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d966c47f9dd73c2d32a809d2be529112d509321c5310ebf54076812e6ecd884"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7eb037106f5c6b3b0b864ad226b0b7ab58157124161d48e4b30c4a43fef8bc4b"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:154ea7c52e32dce13065dbb20a4a6f0cc012b4f667ac90d648d36b12007fa9f7"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e562617a45b5a9da5be4abe72b971d4f00bf8555eb29bb91ec2ef2be348cd132"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:f23b55eb5464468f9e0e9a9935ce3ed2a870608d5f534025cd5536bca25b1402"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:e9121b4009339b0f751955baf4543a0bfd6bc3f8188f8056b1a25a2d45099934"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:0523aeb76e03f753b58be33b26540880bac5aa54422e4462404c432230543f33"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e0e2959ef5d5b8dc9ef21e1a305a21a36e254e6a34432d00c72a92fdc5ecda5"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da01bec0a26befab4898ed83b362993c844b9a607a86add78604186297eb047e"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2e9072d71c1f6cfc79a36d4484c82823c560e6f5599c43c1ca6b5cdbd54f881"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f36a3489d9e28fe4b67be9992a23029c3cec0babc3bd9afb39f49844a8c721c5"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f64f82cc3443149292b32387086d02a6c7fb39b8781563e0ca7b8d7d9cf72bd7"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b4a6db486ac8e99ae696e09efc8b2b9fea67b63c8f88ba7a1a16c24a057a0776"}, + {file = "pydantic_core-2.10.1.tar.gz", hash = "sha256:0f8682dbdd2f67f8e1edddcbffcc29f60a6182b4901c367fc8c1c40d30bb0a82"}, ] +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pydriller" version = "2.5.1" @@ -1802,27 +1301,11 @@ lizard = "*" pytz = "*" types-pytz = "*" -[[package]] -name = "pygmars" -version = "0.8.0" -description = "Craft simple regex-based small language lexers and parsers. Build parsers from grammars and accept Pygments lexers as an input. Derived from NLTK." -category = "main" -optional = false -python-versions = ">=3.6.*" -files = [ - {file = "pygmars-0.8.0-py3-none-any.whl", hash = "sha256:704e23fa8f6ecc70204d0e24b4332e18bd6afc0fe32432aac51c1f5c1d8349a0"}, - {file = "pygmars-0.8.0.tar.gz", hash = "sha256:f434c885da52a0dc61a231ce40fb407ad7a92c0e4e4a6a97b51b49095136d35e"}, -] - -[package.extras] -docs = ["Sphinx (>=3.3.1)", "doc8 (>=0.8.1)", "sphinx-rtd-theme (>=0.5.0)"] -testing = ["aboutcode-toolkit (>=6.0.0)", "black", "pytest (>=6,!=7.0.0)", "pytest-xdist (>=2)"] - [[package]] name = "pygments" version = "2.16.1" description = "Pygments is a syntax highlighting package written in Python." -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1855,27 +1338,6 @@ cachetools = ["cachetools"] frozendict = ["frozendict"] requests = ["requests"] -[[package]] -name = "pymaven-patch" -version = "0.3.0" -description = "Python access to maven. nexB advanced patch." -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "pymaven-patch-0.3.0.tar.gz", hash = "sha256:d55b29bd4aeef3510904a12885eb6856b5bd48f3e29925a123461429f9ad85c0"}, - {file = "pymaven_patch-0.3.0-py2.py3-none-any.whl", hash = "sha256:db031d93aeff14703fbae6f5b4bca634088c15047b811e093e73e5ab77ffdb18"}, -] - -[package.dependencies] -lxml = ">=4.0.0,<5.0.0" -requests = ">=2.7.0,<3.0.0" -six = ">=1.10,<2.0" - -[package.extras] -development = ["detox", "epdb", "flake8", "isort", "yapf"] -test = ["mock", "pytest", "pytest-cov"] - [[package]] name = "pyparsing" version = "3.1.1" @@ -2005,7 +1467,7 @@ files = [ name = "pyyaml" version = "6.0.1" description = "YAML parser and emitter for Python" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2106,97 +1568,47 @@ socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] -name = "saneyaml" -version = "0.6.0" -description = "Read and write readable YAML safely preserving order and avoiding bad surprises with unwanted infered type conversions. This library is a PyYaml wrapper with sane behaviour to read and write readable YAML safely, typically when used for configuration." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "saneyaml-0.6.0-py3-none-any.whl", hash = "sha256:9fdf69e8fe30882221e4bf26eebd34b0fe645478e53ea10231aa49d8d949d8c7"}, - {file = "saneyaml-0.6.0.tar.gz", hash = "sha256:b2309f7836623cd6db932574ebbc43e3f65c743e7635179e64beecb0d1626e44"}, -] - -[package.dependencies] -PyYAML = "*" - -[package.extras] -docs = ["Sphinx (>=3.3.1)", "doc8 (>=0.8.1)", "sphinx-rtd-theme (>=0.5.0)"] -testing = ["aboutcode-toolkit (>=7.0.2)", "black", "isort", "pytest (>=6,!=7.0.0)", "pytest-xdist (>=2)", "twine"] - -[[package]] -name = "scancode-toolkit-mini" -version = "32.0.8" -description = "ScanCode is a tool to scan code for license, copyright, package and their documented dependencies and other interesting facts. scancode-toolkit-mini is a special build that does not come with pre-built binary dependencies by default. These are instead installed separately or with the extra_requires scancode-toolkit-mini[full]" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "scancode-toolkit-mini-32.0.8.tar.gz", hash = "sha256:d7a283d144c9755aefdab0cad9787f650ed27f67b2dbec221ed1d20acb7f5772"}, - {file = "scancode_toolkit_mini-32.0.8-cp310-none-any.whl", hash = "sha256:2bc0b343a9e01c59c8a9fd5a08ec2eae38b742b3b888cd59fa53ffb06f2e7f53"}, - {file = "scancode_toolkit_mini-32.0.8-cp311-none-any.whl", hash = "sha256:3430c0d46cbaa3cca0d2fc534031e3c44680b8a868cdc5ef03115b96dac26571"}, - {file = "scancode_toolkit_mini-32.0.8-cp37-none-any.whl", hash = "sha256:33706e6400c2746bf7cdfa8f1a180c5f4e0e32df99afada11bf4a559303d5b57"}, - {file = "scancode_toolkit_mini-32.0.8-cp38-none-any.whl", hash = "sha256:8b09e394224f1a7781ee6cacc72ffed711f83fe15ee2be40a2c0bbaa04cd5919"}, - {file = "scancode_toolkit_mini-32.0.8-cp39-none-any.whl", hash = "sha256:9c161056a8cc43707efc9fc0f7fe11071ea64bcb038f06264185c570d02314ac"}, +name = "scipy" +version = "1.11.3" +description = "Fundamental algorithms for scientific computing in Python" +category = "main" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "scipy-1.11.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:370f569c57e1d888304052c18e58f4a927338eafdaef78613c685ca2ea0d1fa0"}, + {file = "scipy-1.11.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:9885e3e4f13b2bd44aaf2a1a6390a11add9f48d5295f7a592393ceb8991577a3"}, + {file = "scipy-1.11.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e04aa19acc324a1a076abb4035dabe9b64badb19f76ad9c798bde39d41025cdc"}, + {file = "scipy-1.11.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e1a8a4657673bfae1e05e1e1d6e94b0cabe5ed0c7c144c8aa7b7dbb774ce5c1"}, + {file = "scipy-1.11.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7abda0e62ef00cde826d441485e2e32fe737bdddee3324e35c0e01dee65e2a88"}, + {file = "scipy-1.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:033c3fd95d55012dd1148b201b72ae854d5086d25e7c316ec9850de4fe776929"}, + {file = "scipy-1.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:925c6f09d0053b1c0f90b2d92d03b261e889b20d1c9b08a3a51f61afc5f58165"}, + {file = "scipy-1.11.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5664e364f90be8219283eeb844323ff8cd79d7acbd64e15eb9c46b9bc7f6a42a"}, + {file = "scipy-1.11.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00f325434b6424952fbb636506f0567898dca7b0f7654d48f1c382ea338ce9a3"}, + {file = "scipy-1.11.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f290cf561a4b4edfe8d1001ee4be6da60c1c4ea712985b58bf6bc62badee221"}, + {file = "scipy-1.11.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:91770cb3b1e81ae19463b3c235bf1e0e330767dca9eb4cd73ba3ded6c4151e4d"}, + {file = "scipy-1.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1f97cd89c0fe1a0685f8f89d85fa305deb3067d0668151571ba50913e445820"}, + {file = "scipy-1.11.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dfcc1552add7cb7c13fb70efcb2389d0624d571aaf2c80b04117e2755a0c5d15"}, + {file = "scipy-1.11.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:0d3a136ae1ff0883fffbb1b05b0b2fea251cb1046a5077d0b435a1839b3e52b7"}, + {file = "scipy-1.11.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bae66a2d7d5768eaa33008fa5a974389f167183c87bf39160d3fefe6664f8ddc"}, + {file = "scipy-1.11.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2f6dee6cbb0e263b8142ed587bc93e3ed5e777f1f75448d24fb923d9fd4dce6"}, + {file = "scipy-1.11.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:74e89dc5e00201e71dd94f5f382ab1c6a9f3ff806c7d24e4e90928bb1aafb280"}, + {file = "scipy-1.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:90271dbde4be191522b3903fc97334e3956d7cfb9cce3f0718d0ab4fd7d8bfd6"}, + {file = "scipy-1.11.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a63d1ec9cadecce838467ce0631c17c15c7197ae61e49429434ba01d618caa83"}, + {file = "scipy-1.11.3-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:5305792c7110e32ff155aed0df46aa60a60fc6e52cd4ee02cdeb67eaccd5356e"}, + {file = "scipy-1.11.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ea7f579182d83d00fed0e5c11a4aa5ffe01460444219dedc448a36adf0c3917"}, + {file = "scipy-1.11.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c77da50c9a91e23beb63c2a711ef9e9ca9a2060442757dffee34ea41847d8156"}, + {file = "scipy-1.11.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:15f237e890c24aef6891c7d008f9ff7e758c6ef39a2b5df264650eb7900403c0"}, + {file = "scipy-1.11.3-cp39-cp39-win_amd64.whl", hash = "sha256:4b4bb134c7aa457e26cc6ea482b016fef45db71417d55cc6d8f43d799cdf9ef2"}, + {file = "scipy-1.11.3.tar.gz", hash = "sha256:bba4d955f54edd61899776bad459bf7326e14b9fa1c552181f0479cc60a568cd"}, ] [package.dependencies] -attrs = [ - {version = ">=18.1,<20.1.0 || >20.1.0", markers = "python_version < \"3.11\""}, - {version = ">=22.1.0", markers = "python_version >= \"3.11\""}, -] -Beautifulsoup4 = ">=4.0.0" -"boolean.py" = ">=4.0" -chardet = ">=3.0.0" -click = ">=6.7,<7.0 || >7.0" -colorama = ">=0.3.9" -commoncode = ">=31.0.2" -container-inspector = ">=31.0.0" -debian-inspector = ">=31.0.0" -dparse2 = ">=0.7.0" -fasteners = "*" -fingerprints = ">=0.6.0" -ftfy = ">=6.0.0" -gemfileparser2 = ">=0.9.0" -html5lib = "*" -importlib-metadata = "*" -intbitset = ">=3.0.2" -"jaraco.functools" = "*" -javaproperties = ">=0.5" -jinja2 = ">=2.7.0" -jsonstreams = ">=0.5.0" -license-expression = ">=30.1.1" -lxml = ">=4.9.2" -MarkupSafe = ">=2.1.2" -packageurl-python = ">=0.9.0" -packvers = ">=21.0.0" -parameter-expansion-patched = ">=0.3.1" -"pdfminer.six" = ">=20200101" -pefile = ">=2020.1.1" -pip-requirements-parser = ">=32.0.1" -pkginfo2 = ">=30.0.0" -pluggy = ">=1.0.0" -plugincode = ">=32.0.0" -publicsuffix2 = "*" -pyahocorasick = ">=2.0.0" -pygmars = ">=0.7.0" -pygments = "*" -pymaven-patch = ">=0.2.8" -requests = ">=2.7.0" -saneyaml = ">=0.6.0" -spdx-tools = "0.7.0rc0" -text-unidecode = ">=1.0" -toml = ">=0.10.0" -typecode = ">=30.0.1" -urlpy = "*" -xmltodict = ">=0.11.0" -zipp = {version = ">=3.0.0", markers = "python_version < \"3.9\""} +numpy = ">=1.21.6,<1.28.0" [package.extras] -docs = ["Sphinx (==5.1.0)", "doc8 (>=0.8.1)", "sphinx-reredirects (>=0.1.2)", "sphinx-rtd-theme (>=0.5.1)"] -full = ["extractcode[full] (>=31.0.0)", "typecode[full] (>=30.0.0)"] -packages = ["packagedcode-msitools (>=0.101.210706)", "regipy (>=3.1.0)", "rpm-inspector-rpm (>=4.16.1.3)"] -testing = ["aboutcode-toolkit (>=7.0.2)", "black", "isort", "pycodestyle (>=2.8.0)", "pytest (>=6,!=7.0.0)", "pytest-rerunfailures", "pytest-xdist (>=2)", "twine", "vendorize (>=0.3.0)"] +dev = ["click", "cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"] +doc = ["jupytext", "matplotlib (>2)", "myst-nb", "numpydoc", "pooch", "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 = "setuptools" @@ -2255,7 +1667,7 @@ files = [ name = "soupsieve" version = "2.5" description = "A modern CSS selector implementation for Beautiful Soup." -category = "main" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2275,28 +1687,6 @@ files = [ {file = "spdx_license_list-3.22.tar.gz", hash = "sha256:09205e81a3add4ded20ec4ce91e7fd8fe46486904df3ee82a2848cd070c446c7"}, ] -[[package]] -name = "spdx-tools" -version = "0.7.0rc0" -description = "SPDX parser and tools." -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "spdx-tools-0.7.0rc0.tar.gz", hash = "sha256:650dd0786eb5012ec5fb267228f1cf226e9d53a31be59a4f2e2a840942c1402f"}, - {file = "spdx_tools-0.7.0rc0-py3-none-any.whl", hash = "sha256:cf950dc92e916a0097f3cef3aaa6632462d2e0312561244a3f7640d018749a01"}, -] - -[package.dependencies] -click = "*" -ply = "*" -pyyaml = "*" -rdflib = "*" -xmltodict = "*" - -[package.extras] -test = ["pytest"] - [[package]] name = "sphinx" version = "6.2.1" @@ -2412,48 +1802,57 @@ sphinx = ">4" [[package]] name = "sphinxcontrib-applehelp" -version = "1.0.4" +version = "1.0.7" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" category = "dev" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, - {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, + {file = "sphinxcontrib_applehelp-1.0.7-py3-none-any.whl", hash = "sha256:094c4d56209d1734e7d252f6e0b3ccc090bd52ee56807a5d9315b19c122ab15d"}, + {file = "sphinxcontrib_applehelp-1.0.7.tar.gz", hash = "sha256:39fdc8d762d33b01a7d8f026a3b7d71563ea3b72787d5f00ad8465bd9d6dfbfa"}, ] +[package.dependencies] +Sphinx = ">=5" + [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sphinxcontrib-devhelp" -version = "1.0.2" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +version = "1.0.5" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, + {file = "sphinxcontrib_devhelp-1.0.5-py3-none-any.whl", hash = "sha256:fe8009aed765188f08fcaadbb3ea0d90ce8ae2d76710b7e29ea7d047177dae2f"}, + {file = "sphinxcontrib_devhelp-1.0.5.tar.gz", hash = "sha256:63b41e0d38207ca40ebbeabcf4d8e51f76c03e78cd61abe118cf4435c73d4212"}, ] +[package.dependencies] +Sphinx = ">=5" + [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" -version = "2.0.1" +version = "2.0.4" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" category = "dev" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, - {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, + {file = "sphinxcontrib_htmlhelp-2.0.4-py3-none-any.whl", hash = "sha256:8001661c077a73c29beaf4a79968d0726103c5605e27db92b9ebed8bab1359e9"}, + {file = "sphinxcontrib_htmlhelp-2.0.4.tar.gz", hash = "sha256:6c26a118a05b76000738429b724a0568dbde5b72391a688577da08f11891092a"}, ] +[package.dependencies] +Sphinx = ">=5" + [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["html5lib", "pytest"] @@ -2475,60 +1874,42 @@ test = ["flake8", "mypy", "pytest"] [[package]] name = "sphinxcontrib-qthelp" -version = "1.0.3" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +version = "1.0.6" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, + {file = "sphinxcontrib_qthelp-1.0.6-py3-none-any.whl", hash = "sha256:bf76886ee7470b934e363da7a954ea2825650013d367728588732c7350f49ea4"}, + {file = "sphinxcontrib_qthelp-1.0.6.tar.gz", hash = "sha256:62b9d1a186ab7f5ee3356d906f648cacb7a6bdb94d201ee7adf26db55092982d"}, ] +[package.dependencies] +Sphinx = ">=5" + [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sphinxcontrib-serializinghtml" -version = "1.1.5" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +version = "1.1.9" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, + {file = "sphinxcontrib_serializinghtml-1.1.9-py3-none-any.whl", hash = "sha256:9b36e503703ff04f20e9675771df105e58aa029cfcbc23b8ed716019b7416ae1"}, + {file = "sphinxcontrib_serializinghtml-1.1.9.tar.gz", hash = "sha256:0c64ff898339e1fac29abd2bf5f11078f3ec413cfe9c046d3120d7ca65530b54"}, ] +[package.dependencies] +Sphinx = ">=5" + [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] -[[package]] -name = "text-unidecode" -version = "1.3" -description = "The most basic Text::Unidecode port" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, - {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, -] - -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "main" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] - [[package]] name = "tomli" version = "2.0.1" @@ -2541,30 +1922,6 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -[[package]] -name = "typecode" -version = "30.0.1" -description = "Comprehensive filetype and mimetype detection using libmagic and Pygments." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "typecode-30.0.1-py3-none-any.whl", hash = "sha256:05ccbacf89a51ec0451a5b04ad2835c94a9a82c24b5d0b16670208d4eaabe0c1"}, - {file = "typecode-30.0.1.tar.gz", hash = "sha256:1a57394224d5afffdebb299e353c4637e15568e11ae9c5311c6c86a3d6d0ac97"}, -] - -[package.dependencies] -attrs = ">=18.1,<20.1.0 || >20.1.0" -binaryornot = "*" -commoncode = ">=30.2.0" -"pdfminer.six" = ">=20200101" -plugincode = ">=30.0.0" - -[package.extras] -docs = ["Sphinx (==6.2.1)", "doc8 (>=0.8.1)", "sphinx-rtd-theme (>=0.5.0)"] -full = ["typecode-libmagic (>=5.39.210223)"] -testing = ["aboutcode-toolkit (>=8.0.0)", "black", "isort", "pycodestyle (>=2.8.0)", "pytest (>=6,!=7.0.0)", "pytest-xdist (>=2)", "saneyaml", "twine", "vendy"] - [[package]] name = "typer" version = "0.7.0" @@ -2628,21 +1985,6 @@ secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17. socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] -[[package]] -name = "urlpy" -version = "0.5" -description = "Simple URL parsing, canonicalization and equivalence." -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "urlpy-0.5-py2.py3-none-any.whl", hash = "sha256:841673d97e0dd7a4d7ba47abd49fa8e3a61709e189e40de1b04b150ce7c5ed9f"}, - {file = "urlpy-0.5.tar.gz", hash = "sha256:e98ead47f4e422ca35080fd60a039f4546b7788bbba1b0a542a34c193dfba4bc"}, -] - -[package.dependencies] -publicsuffix2 = ">=2.20191221" - [[package]] name = "virtualenv" version = "20.24.6" @@ -2688,23 +2030,11 @@ files = [ {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, ] -[[package]] -name = "xmltodict" -version = "0.13.0" -description = "Makes working with XML feel like you are working with JSON" -category = "main" -optional = false -python-versions = ">=3.4" -files = [ - {file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"}, - {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"}, -] - [[package]] name = "zipp" version = "3.17.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2718,5 +2048,5 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" -python-versions = "^3.8" -content-hash = "59b4c4f53dcce723506adf3c17a1af5863e0c3a4fd308f06b64e1430024eaf14" +python-versions = ">=3.9,<=3.11" +content-hash = "87fc6e4dd46c078989110387af2291258afecf7026238f0d159df222abc8c95a" diff --git a/pyproject.toml b/pyproject.toml index 3dfcfc38..7db82437 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,9 +11,10 @@ keywords = ["metadata", "git", "extraction", "linked-data"] readme = "README.md" classifiers = [ "Development Status :: 4 - Beta", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Intended Audience :: Science/Research", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", @@ -23,7 +24,7 @@ classifiers = [ # Dependency management [tool.poetry.dependencies] -python = "^3.8" +python = ">=3.9,<=3.11" gitpython = ">=3.1.35" PyDriller = "^2.5" pyshacl = "^0.20.0" @@ -33,8 +34,10 @@ calamus = "^0.4.2" requests = "^2.28.2" python-dotenv = "^0.21.1" python-dateutil = "^2.8.2" -scancode-toolkit-mini = "^32.0.8" spdx-license-list = "^3.22" +numpy = "^1.26.1" +pydantic = "^2.4.2" +scipy = "^1.11.3" [tool.poetry.group.dev.dependencies] black = "^22.10.0" diff --git a/scripts/generate_tfidf.py b/scripts/generate_tfidf.py new file mode 100644 index 00000000..23f72162 --- /dev/null +++ b/scripts/generate_tfidf.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""Download all SPDX licenses and fit a tf-idf vectorizer to them. +The tf-idf matrix, vectorizer and license list are then saved to disk.""" + +import json +from pathlib import Path +from typing import List, NamedTuple + +import numpy as np +import scipy.sparse as sp +import requests + +from gimie.utils.text import TfidfConfig, TfidfVectorizer + +OUT_DIR = Path("gimie") / "parsers" / "license" / "data" + +# Retrieve metadata for all OSI approved and valid licenses from SPDX +SPDX_LIST_URL = "https://raw.githubusercontent.com/spdx/license-list-data/main/json/licenses.json" +all_licenses = requests.get(SPDX_LIST_URL).json()["licenses"] +licenses = filter(lambda l: l["isOsiApproved"], all_licenses) +licenses = filter(lambda l: not l["isDeprecatedLicenseId"], licenses) +licenses = list(licenses) + +# Assemble corpus of license texts (this takes a while) +class License(NamedTuple): + license_id: str + text: str + + +corpus: List[License] = [] + +for idx, license in enumerate(licenses): + resp = requests.get(license["detailsUrl"]) + if not resp.ok: + continue + text = resp.json()["licenseText"] + corpus.append(License(license["licenseId"], text)) + +# Fit tfidf vectorizer to corpus +texts = [l.text for l in corpus] +vectorizer = TfidfVectorizer( + config=TfidfConfig( + max_features=700, ngram_range=(1, 2), sublinear_tf=True, norm="l2" + ) +) +tfidf = vectorizer.fit_transform(texts) + +# Save vectorizer and tfidf matrix +with open(OUT_DIR / "tfidf_vectorizer.json", "w") as fp: + fp.write(vectorizer.model_dump_json()) +# Prune precision to reduce size +tfidf.data = tfidf.data.astype(np.float16) +sp.save_npz(OUT_DIR / "tfidf_matrix.npz", tfidf) +with open(OUT_DIR / "spdx_licenses.csv", "w") as fp: + for l in corpus: + fp.write(f"{l.license_id},{len(l.text)}\n") diff --git a/tests/test_tfidf.py b/tests/test_tfidf.py new file mode 100644 index 00000000..ec00be33 --- /dev/null +++ b/tests/test_tfidf.py @@ -0,0 +1,69 @@ +import json +from typing import List + +import numpy as np +import pytest + +from gimie.utils.text_processing import TfidfConfig, TfidfVectorizer + +CORPUS = [ + "This is my test document.", + "This is another test document.", +] + + +@pytest.fixture +def tfidf_vectorizer() -> TfidfVectorizer: + """Fixture for a TfidfVectorizer instance.""" + config = TfidfConfig(norm="l2", sublinear_tf=True) + return TfidfVectorizer(config=config) + + +def test_tfidf_serde(tfidf_vectorizer: TfidfVectorizer): + """Test json serialization and deserialization of TfidfVectorizer.""" + json_str = tfidf_vectorizer.model_dump_json(indent=2) + json.loads(json_str) + print(TfidfVectorizer.model_validate_json(json_str)) + + +def test_tfidf_fit_transform(tfidf_vectorizer: TfidfVectorizer): + """Test correctness of tfidf fit.""" + _ = tfidf_vectorizer.fit_transform(CORPUS) + # targets computed using sklearn 1.2.2 + target_voc = { + "another": 0, + "document": 1, + "is": 2, + "my": 3, + "test": 4, + "this": 5, + } + target_idf = np.array( + [1.4054651081081644, 1.0, 1.0, 1.4054651081081644, 1.0, 1.0] + ) + assert all( + [v == target_voc[t] for t, v in tfidf_vectorizer.vocabulary.items()] + ) + pred_idf: List[float] = tfidf_vectorizer.idf_vector + assert all([pred == target for pred, target in zip(pred_idf, target_idf)]) + + +# Test fitting different configurations +@pytest.mark.parametrize( + "config", + [ + TfidfConfig(), + TfidfConfig(max_features=10), + TfidfConfig(ngram_range=(1, 2)), + TfidfConfig(ngram_range=(2, 2)), + TfidfConfig(smooth_idf=False), + TfidfConfig(norm="l1"), + TfidfConfig(norm="l2"), + TfidfConfig(sublinear_tf=True), + TfidfConfig(vocabulary={"this": 0, "is": 1, "test": 2}), + ], +) +def test_tfidf_configs(config): + """Test fitting different configurations.""" + vectorizer = TfidfVectorizer(config=config) + _ = vectorizer.fit_transform(CORPUS)