├── .dockerignore ├── .gitignore ├── .gitlab-ci.yml ├── .pre-commit-config.yaml ├── .releaserc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.txt ├── Makefile ├── README.md ├── certitude ├── __init__.py ├── __main__.py ├── app │ ├── __init__.py │ ├── cli.py │ └── main.py ├── core │ ├── __init__.py │ ├── classification.py │ ├── data_model.py │ ├── enrichment.py │ └── wrappers.py └── utils │ ├── __init__.py │ ├── cert.py │ ├── config.py │ ├── data │ ├── __init__.py │ ├── config_default.yml │ └── default_model.pkl │ └── miscellaneous.py ├── docs ├── CERTITUDE-logo.jpeg └── cli-flow.png ├── poetry.lock ├── pyproject.toml ├── tbump.toml └── tests ├── __init__.py ├── data ├── testset.csv └── testset_labeled.csv └── test_model.py /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !pyproject.toml 3 | !poetry.lock 4 | !certitude 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.ipynb 3 | .ipynb_checkpoints/ 4 | */.ipynb_checkpoints/* 5 | .venv 6 | dist/ 7 | .vscode 8 | .mypy_cache 9 | .pytest_cache 10 | config.yml 11 | /.coverage* 12 | *.joblib 13 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | variables: 3 | PYTHON_PACKAGE: "certitude" 4 | PYVERSION: "3.9-slim" 5 | KANIKO_EXTRA_ARGS: "--build-arg=PYVERSION" 6 | 7 | include: 8 | - project: just-ci/templates 9 | file: 10 | - templates/python-docker.yml 11 | - project-automation/badge.yml 12 | ref: v4.1.2 13 | 14 | python:pylint: 15 | variables: 16 | PYLINT_THRESHOLD: "7" 17 | 18 | python:mypy: 19 | allow_failure: true 20 | 21 | certitude:train: 22 | stage: test 23 | script: 24 | - python -m certitude --train /tmp/newmodel.joblib -d tests/data/testset_labeled.csv 25 | 26 | certitude:url: 27 | stage: test 28 | script: 29 | - python -m certitude --url https://www.tno.nl/en/about-tno/ --model certitude/utils/data/default_model.pkl 30 | - python -m certitude --url https://www.sjfkhadsfsdaklfdjsfhas.com/phi --model certitude/utils/data/default_model.pkl 31 | - python -m certitude --url https://www.sjfkhadsfsdaklfdjsfhas.com/phi -v --model certitude/utils/data/default_model.pkl 32 | 33 | badge:prepare: 34 | stage: .pre 35 | image: 36 | name: alpine/git 37 | entrypoint: [""] 38 | script: 39 | - echo "COMMIT_COUNT=$(git rev-list --all --count)" > badges.env 40 | - echo "LATEST_TAG=$(git tag | sort -V | tail -1)" >> badges.env 41 | artifacts: 42 | reports: 43 | dotenv: badges.env 44 | 45 | .badge: 46 | dependencies: 47 | - badge:prepare 48 | 49 | badge:commits: 50 | extends: .badge 51 | variables: 52 | LABEL: "commits" 53 | VALUE: "${COMMIT_COUNT}" 54 | COLOR: "green" 55 | URL: "${CI_PROJECT_URL}" 56 | 57 | badge:version: 58 | extends: .badge 59 | variables: 60 | LABEL: "version" 61 | VALUE: "${LATEST_TAG}" 62 | COLOR: "blue" 63 | URL: "${CI_PROJECT_URL}" 64 | 65 | badge:license: 66 | extends: .badge 67 | variables: 68 | LABEL: "license" 69 | VALUE: "MPL-2.0" 70 | COLOR: "orange" 71 | URL: "${CI_PROJECT_URL}" 72 | 73 | badge:code-style: 74 | extends: .badge 75 | variables: 76 | LABEL: "code-style" 77 | VALUE: "black" 78 | COLOR: "black" 79 | URL: "${CI_PROJECT_URL}" 80 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fail_fast: false 3 | default_stages: [commit] 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v4.0.1 7 | hooks: 8 | - id: check-yaml 9 | - id: check-added-large-files 10 | - id: detect-private-key 11 | - id: check-executables-have-shebangs 12 | - id: check-merge-conflict 13 | - id: end-of-file-fixer 14 | - id: trailing-whitespace 15 | - id: mixed-line-ending 16 | args: ['--fix=lf'] 17 | description: Forces to replace line ending by the UNIX 'lf' character. 18 | - repo: local 19 | hooks: 20 | - id: system 21 | name: black 22 | entry: poetry run black . 23 | pass_filenames: false 24 | always_run: true 25 | language: system 26 | - id: system 27 | name: isort 28 | entry: poetry run isort --profile=black . 29 | pass_filenames: false 30 | always_run: true 31 | language: system 32 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "@semantic-release/exec", 5 | { 6 | "verifyReleaseCmd": "echo ${nextRelease.version} > .VERSION" 7 | } 8 | ], 9 | "@semantic-release/commit-analyzer", 10 | "@semantic-release/release-notes-generator", 11 | "@semantic-release/changelog", 12 | ["@semantic-release/git", { 13 | "assets": ["CHANGELOG.md", "pyproject.toml", "tbump.toml", "certitude/__init__.py"], 14 | "message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}" 15 | }], 16 | "@semantic-release/gitlab" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.0 (2021-10-25) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * bug in prediction parsing ([3a854dc](https://gitlab.com/cossas/certitude/commit/3a854dc9ed25a17a8e550e4634c17e8de4cbbd46)) 7 | * releases ([f42f2af](https://gitlab.com/cossas/certitude/commit/f42f2affc89323ec22bcedbc75701780a561babc)) 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributor guidelines 2 | Thank you for reading this document and considering contributing to a COSSAS product. With every contribution, we improve the quality of these products. To help you with a potential contribution, we have crafted this document. 3 | 4 | ## Why should I contribute? 5 | COSSAS is a community for open-source security automation software that we are creating and improving together. Contributing to community will benefit us all, so just do it! 6 | 7 | ## What do I need to know to help? 8 | If you are looking to help to improve one of the COSSAS products with a code contribution, you might want to read more about the product first. First, have a look at our [website](https://cossas-project.org) to see where this product came from, what's it trying to achieve and what the status of this product is. Afterwards, have a look at the [issues on Gitlab](https://gitlab.com/cossas/certitude/-/issues) to find the problems you might want to solve. If you just want to help others with this COSSAS product, responding to issues is also much appreciated. 9 | 10 | In short if you want to know more about a COSSAS product: 11 | - Read the product entry on our [website](https://cossas-project.org/portfolio/certitude/) 12 | - Browse through the list of [issues on GitLab](https://gitlab.com/cossas/certitude/-/issues) 13 | 14 | ### GitLab vs GitHub 15 | Most of the COSSAS products are the results of TNO projects and TNO uses GitLab internally for software development. Therefor, continuous integration and continuous deployment (CI/CD) is done through GitLab pipelines. To easily open source TNO projects, the COSSAS initiative decided to host all software on [GitLab](https://gitlab.com/cossas) with a push mirroring to [GitHub](https://github.com/cossas). So, if you are reading this document on GitHub and want to make a contribution, please switch over the [GitLab](https://gitlab.com/cossas/certitude/) to continue! 16 | 17 | ## How do I contribute? 18 | Any contribution is appreciated, and many don’t imply coding. Contributions can range from a suggestion for improving documentation, requesting a new feature, reporting a bug, to developing features or fixing bugs yourself. 19 | 20 | If you're ready to contribute by coding and want to make your first contribution to a COSSAS product, but you're not that familiar with contributing to open-source software, we have included a useful step-by-step tutorial how to contribute: 21 | 22 | 1. Find an issue that you are interested in addressing or think of a feature that you would like to add. 23 | 1. Fork the repository to your GitLab account. This means that you will have a copy of the repository under **your-GitLab-username/repository-name**. 24 | 1. Clone the repository to your local machine using `git clone https://gitlab.com/your-GitLab-username/repository-name.git`. 25 | 1. Create a new branch for your fix using `git checkout -b [fix/feature]-branch-name-here`. Please use the `fix` prefix if you are fixing stuff, and `feature` if you are adding a new feature. 26 | 1. Make the necessary changes for the issue you are trying to fix or the feature that you want to add. 27 | 1. Use `git add the-files-you-want-to-include` to add the file contents of the changed files to the "snapshot" git uses to manage the state of the project, also known as the index. 28 | 1. Use `git commit -m "Insert a short message of the changes made here"` to store the contents of the index with a descriptive message. 29 | 1. Push the changes to the remote repository using `git push origin branch-name-here`. 30 | 1. Submit a pull request to the upstream repository. This means that you request your fork to be merged with the COSSAS product main branch. 31 | 1. Title the pull request with a short description of the changes made and the issue number associated with your change. For example, you can title an issue like so "Added more log outputting to resolve #435". 32 | 1. In the description of the pull request, explain the changes that you made, any issues you think exist with the pull request you made, and any questions you have for the maintainer. It's OK if your pull request is not perfect (no pull request is), the reviewer will be able to help you fix any problems and improve it! 33 | 1. Wait for the pull request to be reviewed by a maintainer or any other member in the community. We encourage every contributor to collaborate as much as possible! We really appreciate it when contributor review each other’s pull requests. 34 | 1. Make changes to the pull request if the reviewing maintainer recommends them. 35 | 1. Celebrate your success after your pull request is merged! 36 | 37 | ## Where can I go for help? 38 | If you need help, you can ask your questions in a separate [Gitlab issue](https://gitlab.com/cossas/certitude/-/issues) or email them. You can find our email address at our [contact page](https://cossas-project.org/contact/). 39 | 40 | ## What does the Code of Conduct mean for me? 41 | Our [Code of Conduct](https://gitlab.com/cossas/home/-/blob/main/CODE_OF_CONDUCT.md) means that you are responsible for treating everyone on the project with respect and courtesy regardless of their identity. If you are the victim of any inappropriate behavior or comments as described in our [Code of Conduct](https://gitlab.com/cossas/home/-/blob/main/CODE_OF_CONDUCT.md), we are here for you and will do the best to ensure that the abuser is reprimanded appropriately, per our code. 42 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PYVERSION=3.9-slim 2 | FROM python:$PYVERSION 3 | 4 | RUN apt-get update && apt-get install -y whois 5 | 6 | COPY . /app 7 | 8 | WORKDIR /app 9 | 10 | RUN pip install . 11 | 12 | ENTRYPOINT ["python", "-m", "certitude"] 13 | CMD [ "-h" ] 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | @echo 3 | @echo --- rebuilding image --- 4 | docker build -t certitude:dev . 5 | 6 | test-train: 7 | @echo 8 | @echo --- creating new model using labelled dataset --- 9 | docker run -it -v $$(pwd)/tests/data:/data certitude:dev --train /data/newmodel101.joblib -d /data/testset_labeled.csv 10 | 11 | test-use-existing-model-unlabelled-batch: 12 | @echo 13 | @echo --- classifying batch of url against existing model --- 14 | docker run -it -v $$(pwd)/tests/data:/data certitude:dev -m /data/newmodel101.joblib --batch /data/testset.csv 15 | 16 | test-single: 17 | @echo 18 | @echo --- classifying single url against existing model --- 19 | docker run -it -v $$(pwd)/tests/data:/data certitude:dev --url https://www.google.com -m /data/newmodel101.joblib 20 | 21 | delete: 22 | @echo 23 | @echo --- deleting existing model --- 24 | rm $$(pwd)/tests/data/newmodel101.joblib || true 25 | 26 | demo: delete build test-train test-use-existing-model-unlabelled-batch test-single delete 27 | @echo 28 | @echo --- Everything OK --- 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | ![Website](https://img.shields.io/badge/website-cossas--project.org-orange) 5 | ![Commits](https://gitlab.com/cossas/certitude/-/jobs/artifacts/master/raw/ci_badges/commits.svg?job=badge:commits) 6 | ![Pipeline status](https://gitlab.com/cossas/certitude/badges/master/pipeline.svg) 7 | ![Version](https://gitlab.com/cossas/certitude/-/jobs/artifacts/master/raw/ci_badges/version.svg?job=badge:version) 8 | ![License: MPL2.0](https://gitlab.com/cossas/certitude/-/jobs/artifacts/master/raw/ci_badges/license.svg?job=badge:license) 9 | ![Code-style](https://gitlab.com/cossas/certitude/-/jobs/artifacts/master/raw/ci_badges/code-style.svg?job=badge:code-style) 10 |
11 | 12 |
13 |
14 | Certitude is a Python package to perform supervised malicious URL classification using a joint set of lexicographic and certificate features. 15 |
16 |
17 | 18 | _All COSSAS projects are hosted on [GitLab](https://gitlab.com/cossas/dgad/) with a push mirror to GitHub. For issues/contributions check [CONTRIBUTING.md](CONTRIBUTING.md)_ 19 | 20 | ## Getting Started 21 | 22 | Certitude requires `whois`, which may not be available on some systems, and is thus distributed as a docker image. 23 | If `whois` is available it can also be installed as a python package, see the development section below. 24 | 25 | ```bash 26 | # pull image from registry 27 | docker pull registry.gitlab.com/cossas/certitude:latest 28 | 29 | # print help 30 | docker run -it registry.gitlab.com/cossas/certitude:latest 31 | 32 | # example perform training from data in local directory 33 | docker run -it -v $(pwd)/tests/data:/data registry.gitlab.com/cossas/certitude:latest --train /data/newmodel -d /data/testset_labeled.csv 34 | 35 | # example performing classification of a url with the trained model 36 | docker run -it -v $(pwd)/tests/data:/data registry.gitlab.com/cossas/certitude:latest --model /data/newmodel --url https://www.tno.nl 37 | 38 | ``` 39 | 40 | ## Development 41 | 42 | To start developing this package, follow these steps: 43 | 44 | - Start WSL 45 | - `git clone` this project, ensuring you do that in the WSL filesystem. Run `cd` to 46 | ensure you're in the WSL home directory 47 | - `cd` into the just cloned directory 48 | - Run `code .` to start VS Code 49 | - In a VS Code terminal, run `poetry install`, `poetry shell` and finally 50 | `poetry run pre-commit install` 51 | 52 | ### Code flow 53 | Checkout the code flow [here](docs/cli-flow.png) 54 | 55 | ### Demo & Test 56 | 57 | To see some useful commands and to test the code you can check the makefile: 58 | 59 | ```bash 60 | make demo 61 | ``` 62 | 63 | ## Contributing 64 | 65 | Contributions to CERTITUDE are highly appreciated and more than welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) for more information about our contributions process. 66 | 67 | ## Maintainance status 68 | 69 | This project has been developed until TRL4 and is currently not actively maintained. 70 | We envision the following steps to raise the TRL from 4 to 6: 71 | - Technical trials in security pipelines of small to midsized companies. 72 | - Retraining of the default model using company security data. 73 | - Validating the accuracy of the packages' classification method in relevant circumstances. 74 | - Improving the package on shortcomings for the needs of a small to midsized company. 75 | -------------------------------------------------------------------------------- /certitude/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.0.0" 2 | -------------------------------------------------------------------------------- /certitude/__main__.py: -------------------------------------------------------------------------------- 1 | from certitude.app.cli import parse_arguments 2 | from certitude.app.main import main 3 | 4 | if __name__ == "__main__": 5 | args = parse_arguments() 6 | main(args) 7 | -------------------------------------------------------------------------------- /certitude/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/COSSAS/Certitude/0d04a82d56c1ce8e1b997c0f15ca7031058767de/certitude/app/__init__.py -------------------------------------------------------------------------------- /certitude/app/cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | from importlib.metadata import metadata 4 | from pathlib import Path 5 | from typing import Dict 6 | 7 | import certitude 8 | 9 | 10 | def parse_arguments() -> Dict: 11 | package_description = metadata(certitude.__package__)["Summary"] 12 | package_version = metadata(certitude.__package__)["Version"] 13 | description = f"""{package_description} - v{package_version}""" 14 | 15 | parser = argparse.ArgumentParser( 16 | description=description, prog="python -m certitude" 17 | ) 18 | 19 | def existing_path(string) -> Path: 20 | path = Path(string) 21 | if path.is_file(): 22 | return path.absolute() 23 | else: 24 | parser.error("The provided path does not exist or is not a file.") 25 | 26 | def nonexisting_path(string) -> Path: 27 | path = Path(string) 28 | if not path.exists(): 29 | return path.absolute() 30 | else: 31 | parser.error("The provided path already exists.") 32 | 33 | group = parser.add_mutually_exclusive_group() 34 | group.add_argument("--url", help="A single URL to classify.", type=str) 35 | group.add_argument( 36 | "--batch", 37 | help="A CSV of URLs to classify.", 38 | type=existing_path, 39 | metavar=".CSV FILE", 40 | ) 41 | group.add_argument( 42 | "--train", 43 | help="Train a new model. Provide a non-existing path to create the new model file.", 44 | type=nonexisting_path, 45 | metavar=".JOBLIB FILE", 46 | ) 47 | 48 | parser.add_argument( 49 | "-m", 50 | "--model", 51 | help="Path to a trained model. Defaults to a built-in sample model.", 52 | type=existing_path, 53 | metavar=".JOBLIB FILE", 54 | ) 55 | parser.add_argument( 56 | "-d", 57 | "--dataset", 58 | help="Path to a CSV dataset you want to train on. Defaults to a built-in sample dataset.", 59 | type=existing_path, 60 | metavar=".CSV FILE", 61 | ) 62 | parser.add_argument( 63 | "-c", 64 | "--config", 65 | help="Path to a custom configuration file. If omitted, will use default settings.", 66 | type=existing_path, 67 | metavar=".YML FILE", 68 | ) 69 | parser.add_argument( 70 | "-v", "--verbose", help="Show debug logging.", action="store_true" 71 | ) 72 | args = parser.parse_args() 73 | if not args.url and not args.train and not args.batch: 74 | parser.print_help() 75 | sys.exit(2) 76 | 77 | return args 78 | -------------------------------------------------------------------------------- /certitude/app/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | from importlib import resources 4 | from pathlib import Path 5 | from typing import Dict 6 | 7 | import joblib 8 | import whois 9 | 10 | import certitude 11 | import certitude.utils.data 12 | from certitude.core.data_model import ( 13 | create_features_dataframe, 14 | process_url, 15 | train_model, 16 | ) 17 | from certitude.utils.config import config 18 | 19 | logging.basicConfig() 20 | config = config() 21 | 22 | 23 | def set_loglevel(level: int = logging.INFO): 24 | logging.getLogger(certitude.__package__).setLevel(level) 25 | 26 | 27 | set_loglevel() 28 | logger = logging.getLogger(__name__) 29 | 30 | 31 | def main(args: Dict) -> None: 32 | logger.debug(f"The arguments we received from you: {args}") 33 | 34 | try: 35 | whois.query("github.com") 36 | except FileNotFoundError: 37 | logger.critical("whois is not installed. Please install.") 38 | sys.exit(1) 39 | except whois.exceptions.WhoisCommandFailed: 40 | logger.critical("whois failed a basic test. The application may not work.") 41 | 42 | if args.verbose: 43 | set_loglevel(logging.DEBUG) 44 | 45 | logger.debug(f"These features are enabled in the config: {config['features']}") 46 | 47 | if args.train: 48 | if args.dataset: 49 | logger.info(f"Using user supplied dataset at {args.dataset}") 50 | dataset_path = args.dataset 51 | else: 52 | logger.warning(f"Using default dataset") 53 | with resources.path(certitude.utils.data, "default_dataset.csv") as path: 54 | dataset_path = Path(path) 55 | 56 | features_dataframe = create_features_dataframe(dataset_path, labeled=True) 57 | 58 | model_path = Path(args.train) 59 | train_model(features_dataframe, config["features"], model_path) 60 | 61 | if args.url: 62 | if "//" not in args.url: 63 | logger.critical( 64 | "url provided %s is not RFC 1808 compliant. You are probably missing http:// or https://", 65 | args.url, 66 | ) 67 | logger.critical( 68 | "see https://docs.python.org/3/library/urllib.parse.html#url-parsing" 69 | ) 70 | sys.exit(1) 71 | url_obj = process_url(args.url) 72 | 73 | model = load_model(args.model) 74 | 75 | if url_obj.features_dataframe is not None: 76 | if not url_obj.features_dataframe.empty: 77 | prediction = model.predict( 78 | url_obj.features_dataframe[config["features"]] 79 | ) 80 | 81 | if int(prediction[0]): 82 | result = "URL is Malicious" 83 | else: 84 | result = "URL is Safe" 85 | 86 | logger.critical(f"{args.url}: {result}") 87 | return None 88 | logger.critical(f"{args.url} not found.") 89 | 90 | if args.batch: 91 | model = load_model(model=args.model) 92 | features_dataframe = create_features_dataframe(args.batch, model=model) 93 | 94 | 95 | def load_model(model): 96 | if model: 97 | model = joblib.load(model) 98 | return model 99 | else: 100 | logger.critical( 101 | "url classification requested, but no model provided! You must pass a model with --model" 102 | ) 103 | sys.exit(1) 104 | # logger.warning("no model was provided, will use default model") 105 | # with resources.path( 106 | # certitude.utils.data, "default_model.pkl" 107 | # ) as model_path: 108 | # model = joblib.load(model_path) 109 | -------------------------------------------------------------------------------- /certitude/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/COSSAS/Certitude/0d04a82d56c1ce8e1b997c0f15ca7031058767de/certitude/core/__init__.py -------------------------------------------------------------------------------- /certitude/core/classification.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__name__) 4 | 5 | 6 | class RandomForest: 7 | def __init__(self, url): 8 | self.url = url 9 | logger.error(f"Nothing but trees here. But thanks for the {self.url}") 10 | logger.debug("I'm a lonely debug message") 11 | -------------------------------------------------------------------------------- /certitude/core/data_model.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import logging 3 | import math 4 | import multiprocessing as mp 5 | import re 6 | from datetime import datetime 7 | from pathlib import Path 8 | from typing import Dict, List 9 | from urllib.parse import urlparse 10 | 11 | import joblib 12 | import pandas as pd 13 | import whois 14 | from pandas.core.frame import DataFrame 15 | from sklearn.ensemble import RandomForestClassifier 16 | from whois.exceptions import FailedParsingWhoisOutput, UnknownTld 17 | 18 | from certitude.utils.cert import Certsearch 19 | from certitude.utils.config import config 20 | from certitude.utils.miscellaneous import ( 21 | count_delimiters, 22 | dataframe_postprocessing, 23 | entropy, 24 | ) 25 | 26 | logger = logging.getLogger(__name__) 27 | mgr = mp.Manager() 28 | ns = mgr.Namespace() 29 | config = config() 30 | 31 | 32 | class URL: 33 | def __init__(self, url: str) -> None: 34 | self.url = url 35 | self.valid_url = None 36 | try: 37 | logger.debug(self.url) 38 | self.parsed_url = urlparse(self.url) 39 | self.valid_url = True 40 | except Exception: 41 | logger.warning("[!] Unable to parse url. It will be ignored.") 42 | self.valid_url = False 43 | self.scheme = self.parsed_url.scheme 44 | self.netloc = self.parsed_url.netloc 45 | self.query = self.parsed_url.query 46 | self.path = self.parsed_url.path 47 | self.params = self.parsed_url.params 48 | self.fragment = self.parsed_url.fragment 49 | self.port = self.parsed_url.port 50 | self.certificate_collection = None 51 | self.features_dataframe = None 52 | self.valid = None 53 | 54 | def domains_separator(self) -> None: 55 | """separates domains within netloc""" 56 | self.domains_splitted = self.netloc.split(".") 57 | if self.domains_splitted[0] == "www": 58 | self.domains_splitted = self.domains_splitted[1:] 59 | # This is in case we do not get an error with domains_splitted[0] 60 | if len(self.domains_splitted) == 0: 61 | self.domains_splitted.append("") 62 | self.ft_dummy_domains = self.domains_splitted[0] 63 | 64 | def domains_features(self) -> None: 65 | """features based on subdomains of url""" 66 | self.ft_nr_of_domains = len(self.domains_splitted) 67 | self.ft_length_first_subdomain = len(self.domains_splitted[0]) 68 | self.ft_length_tld = len(self.domains_splitted[-1]) 69 | 70 | def netloc_features(self) -> None: 71 | """features based on main domain/hostname (netloc)""" 72 | self.ft_netloc_length = len(self.netloc) 73 | 74 | def query_features(self) -> None: 75 | """features based on url query section""" 76 | self.ft_query_length = len(self.query) 77 | 78 | def path_features(self) -> None: 79 | """features based on url path section""" 80 | self.ft_path_length = len(self.path) 81 | 82 | def combined_features(self) -> None: 83 | """features based on combined parts of url""" 84 | if self.ft_path_length > 0: 85 | self.ft_ratio_netloc_path = self.ft_netloc_length / self.ft_path_length 86 | else: 87 | self.ft_ratio_netloc_path = 0 88 | 89 | def count_signs(self) -> None: 90 | """feature count number of signs in url sections""" 91 | # count number of dots 92 | self.ft_nr_dots_query = self.query.count(".") 93 | self.ft_nr_dots_path = self.path.count(".") 94 | 95 | # count @ signs 96 | self.ft_nr_at_query = self.query.count("@") 97 | self.ft_nr_at_path = self.path.count("@") 98 | 99 | # count % signs 100 | self.ft_nr_percent_query = self.query.count("%") 101 | self.ft_nr_percent_path = self.path.count("%") 102 | 103 | # number of delimiters (e.g. '_', '&', etc.) 104 | self.ft_nr_del_query = count_delimiters(self.query) 105 | self.ft_nr_del_path = count_delimiters(self.path) 106 | 107 | def has_port(self) -> None: 108 | "Does the url contain a port?" 109 | self.ft_has_port = True 110 | if self.port is not None: 111 | if math.isnan(self.port): 112 | self.ft_has_port = False 113 | else: 114 | self.ft_has_port = False 115 | 116 | def contains_puny(self) -> None: 117 | "Does the url contain puny code?" 118 | self.ft_contains_puny = False 119 | puny = re.findall("--xn", self.netloc, re.IGNORECASE) 120 | if len(puny) > 0: 121 | self.ft_contains_puny = True 122 | 123 | def is_http(self) -> None: 124 | """feature is it http""" 125 | self.ft_is_http = False 126 | if self.scheme == "http": 127 | self.ft_is_http = True 128 | 129 | def entropy_features(self) -> None: 130 | """check entropy of different url sections""" 131 | # The domain right after tld. THIS DOES NOT WORK FOR 132 | # E.G. .co.uk tld. 133 | self.ft_domain_entropy = entropy(self.domains_splitted[0]) 134 | self.ft_query_entropy = entropy(self.query) 135 | self.ft_path_entropy = entropy(self.path) 136 | 137 | def feature_extraction(self) -> None: 138 | """method containing all features""" 139 | self.domains_separator() 140 | self.domains_features() 141 | self.netloc_features() 142 | self.query_features() 143 | self.path_features() 144 | self.combined_features() 145 | self.count_signs() 146 | self.has_port() 147 | self.contains_puny() 148 | self.is_http() 149 | self.entropy_features() 150 | 151 | 152 | class CertificateCollection: 153 | """class for the certificate collection class""" 154 | 155 | # setup initial state for some code we don't want to constantly rerun 156 | sql_tables_cert = None 157 | sql_tables_dom = None 158 | sql_tablenames_cert = None 159 | sql_tablenames_dom = None 160 | sql_db_cursor = None 161 | 162 | def __init__(self, url_object: URL) -> None: 163 | logger.debug("url_object netloc %s", url_object.netloc) 164 | self.c_name = url_object.netloc 165 | logger.debug("c_name %s", self.c_name) 166 | self.cert_df = Certsearch().find(self.c_name) 167 | self.ft_wildcard_cert = False 168 | 169 | if self.cert_df.empty: 170 | self.ft_no_certs_found = False 171 | logger.error( 172 | "No Cert Found for %s - skipping feature collection", self.c_name 173 | ) 174 | raise LookupError 175 | else: 176 | self.ft_no_certs_found = True 177 | 178 | def certificate_features(self) -> None: 179 | """certificate features are computed in this method""" 180 | 181 | # FEATURE: Number of certificates found. 182 | # NOTE: to save bandwidth no searching beyond two pages with 10 per page. 183 | # adjust in tools.cert_grabber 184 | self.ft_nr_of_certs = self.cert_df.shape[0] 185 | 186 | # FEATURE: Age in days of most recent certificate since validity / expiry 187 | # certificates are ordered by when they were created 188 | # there is no created at date in the response only valid from. 189 | # We will just use the most recent valid from to determine age (it 190 | # could be minus in some cases) 191 | now = datetime.now() 192 | self.cert_df["Days since valid"] = ( 193 | now 194 | - pd.to_datetime(self.cert_df["Valid from"], unit="ms").dt.to_pydatetime() 195 | ) 196 | self.cert_df["Days since expiry"] = ( 197 | now - pd.to_datetime(self.cert_df["Valid to"], unit="ms").dt.to_pydatetime() 198 | ) 199 | 200 | self.ft_days_since_valid = self.cert_df["Days since valid"].iloc[0].days 201 | self.ft_days_since_expiry = self.cert_df["Days since expiry"].iloc[0].days 202 | 203 | # FEATURE: get mean age between start of cert validitys 204 | # Get mean age between certs (don't count duplicate validaty start 205 | # dates) 206 | day_range = [] 207 | for timed_row in self.cert_df["Days since valid"]: 208 | if timed_row.days not in day_range: 209 | day_range.append(int(timed_row.days)) 210 | 211 | self.ft_renewal_days_mean = sum(day_range) / len(day_range) 212 | 213 | # FEATURE: whois creation age, whether dnssec is used and also expiry 214 | # Whois lookup, technically not a cert feature but here anyway 215 | 216 | try: 217 | if not (domain := whois.query(self.c_name)): 218 | logger.error(f"No whois match for {self.c_name}") 219 | raise LookupError 220 | except FailedParsingWhoisOutput as e: 221 | logger.error(f"whois query failed for {self.c_name}: {e}") 222 | raise LookupError 223 | except UnknownTld as e: 224 | logger.error(f"whois query failed for {self.c_name}: {e}") 225 | raise LookupError 226 | 227 | if domain.creation_date: 228 | whois_creation_age_timedelta = datetime.now() - domain.creation_date 229 | self.ft_whois_creation_age_days = whois_creation_age_timedelta.days 230 | else: 231 | logger.debug("domain creation_date not found in whois for %s", self.c_name) 232 | if config["set_missing_whois_to_zero"]: 233 | self.ft_whois_creation_age_days = 0 234 | else: 235 | self.ft_whois_creation_age_days = None 236 | 237 | if domain.dnssec: 238 | self.ft_whois_dnssec = domain.dnssec 239 | else: 240 | logger.debug("domain dnssec not found in whois for %s", self.c_name) 241 | self.ft_whois_dnssec = False 242 | 243 | if domain.expiration_date: 244 | whois_expiry_timedelta = datetime.now() - domain.expiration_date 245 | self.ft_whois_expiry_days = whois_expiry_timedelta.days 246 | else: 247 | logger.debug( 248 | "domain expiration date not found in whois for %s", self.c_name 249 | ) 250 | if config["set_missing_whois_to_zero"]: 251 | self.ft_whois_expiry_days = 0 252 | else: 253 | self.ft_whois_expiry_days = None 254 | 255 | # FEATURE: free certificate authority is used 256 | free_ca = [ 257 | "Let's Encrypt Authority X3", 258 | "COMODO ECC Domain Validation Secure Server CA 2", 259 | "COMODO ECC Domain Validation Secure Server CA", 260 | "CloudFlare Inc ECC CA-2", 261 | "CloudFlare Inc ECC CA-3", 262 | "R3", 263 | ] 264 | if self.cert_df["CA"][0] in free_ca: 265 | self.ft_free_ca_issuer = True 266 | else: 267 | self.ft_free_ca_issuer = False 268 | 269 | # validity period feature 270 | # assign validity period but only perform on newest rows 271 | self.cert_df["Validity period"] = ( 272 | pd.to_datetime(self.cert_df["Valid to"], unit="ms").dt.to_pydatetime() 273 | - pd.to_datetime(self.cert_df["Valid from"], unit="ms").dt.to_pydatetime() 274 | ) 275 | # get valid period of first row as all rows should be the same 276 | self.ft_valid_period = self.cert_df["Validity period"].iloc[0].days 277 | 278 | def print_certificate_features(self): 279 | logger.debug("--------------------------") 280 | logger.debug("-----Certificate features:") 281 | logger.debug("--------------------------") 282 | logger.debug( 283 | "Number of certificates found for this URL : %s", str(self.ft_nr_of_certs) 284 | ) 285 | 286 | 287 | def process_url(url: str) -> URL: 288 | logger.debug(f"URL: {url}") 289 | try: 290 | url_obj = URL(url) 291 | logger.debug("url netloc %s", url_obj.netloc) 292 | url_obj.feature_extraction() 293 | cert_collection_obj = CertificateCollection(url_obj) 294 | cert_collection_obj.certificate_features() 295 | url_obj.certificate_collection = cert_collection_obj 296 | except LookupError: 297 | url_obj.valid = False 298 | return url_obj 299 | 300 | features_dataframe = dataframe_postprocessing( 301 | url_obj, url_obj.certificate_collection 302 | ) 303 | url_obj.features_dataframe = features_dataframe 304 | url_obj.valid = True 305 | return url_obj 306 | 307 | 308 | def process_url_dict(url_dict: Dict, training: bool = False) -> URL: 309 | url = url_dict[1] 310 | url_obj = process_url(url) 311 | if training: 312 | return url_obj 313 | if url_obj.features_dataframe is not None: 314 | if not url_obj.features_dataframe.empty: 315 | prediction = ns.model.predict( 316 | url_obj.features_dataframe[config["features"]] 317 | ) 318 | if prediction[0]: 319 | result = "URL is Malicious" 320 | else: 321 | result = "URL is Safe" 322 | print(f"{url}: {result}") 323 | 324 | 325 | def process_labeled_url_dict(url_dict: Dict) -> None: 326 | url_obj = process_url_dict(url_dict, True) 327 | 328 | if url_obj.valid: 329 | try: 330 | url_obj.features_dataframe["label"] = url_dict[2] 331 | ns.feature_df = ns.feature_df.append(url_obj.features_dataframe) 332 | except IndexError: 333 | logger.warning( 334 | f"No label was found for this URL. {url_obj.netloc} not added to the training dataframe." 335 | ) 336 | logger.debug(f"Dataframe shape for this URL: {ns.feature_df.shape}") 337 | 338 | 339 | def create_features_dataframe( 340 | urls_csv_path: Path, model: RandomForestClassifier = None, labeled: bool = False 341 | ) -> DataFrame: 342 | workers = max(mp.cpu_count() - 1, 1) 343 | logger.info(f"[+] Using {workers} workers") 344 | 345 | pool = mp.Pool(workers) 346 | 347 | ns.feature_df = pd.DataFrame() 348 | 349 | with open(urls_csv_path) as csvfile: 350 | reader = csv.reader(csvfile, delimiter=",") 351 | next(reader) # Skip headers 352 | if labeled: 353 | pool.map(process_labeled_url_dict, reader) 354 | else: 355 | ns.model = model 356 | pool.map(process_url_dict, reader) 357 | pool.close() 358 | pool.join() 359 | return ns.feature_df 360 | 361 | 362 | def train_model(labeled_dataset: DataFrame, features: List, model_path: Path) -> None: 363 | x_train = labeled_dataset[features] 364 | y_train = labeled_dataset["label"] 365 | 366 | n_estimators = 250 367 | min_samples_split = 10 368 | logger.debug( 369 | f"[*] Classifying dataframe using Random Forest ({n_estimators},{min_samples_split})..." 370 | ) 371 | model = RandomForestClassifier( 372 | n_estimators=n_estimators, min_samples_split=min_samples_split 373 | ) 374 | model.fit(x_train, y_train) 375 | 376 | logger.info(f"[*] Writing new model to {model_path}...") 377 | joblib.dump(model, model_path) 378 | -------------------------------------------------------------------------------- /certitude/core/enrichment.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__name__) 4 | -------------------------------------------------------------------------------- /certitude/core/wrappers.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/COSSAS/Certitude/0d04a82d56c1ce8e1b997c0f15ca7031058767de/certitude/core/wrappers.py -------------------------------------------------------------------------------- /certitude/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/COSSAS/Certitude/0d04a82d56c1ce8e1b997c0f15ca7031058767de/certitude/utils/__init__.py -------------------------------------------------------------------------------- /certitude/utils/cert.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | 4 | import pandas as pd 5 | import requests 6 | from pandas.core.frame import DataFrame 7 | 8 | logger = logging.getLogger(__name__) 9 | # Usage: 10 | # df = Certsearch().find("tno.nl") 11 | 12 | 13 | class Certsearch: 14 | """class for certsearch called using Certsearch.find()""" 15 | 16 | def __init__(self) -> None: 17 | # include expired true, include subdomains false 18 | self.api = "https://transparencyreport.google.com/transparencyreport/api/v3/httpsreport/ct/certsearch?include_expired=true&include_subdomains=false&domain=" 19 | self.api_key = "https://transparencyreport.google.com/transparencyreport/api/v3/httpsreport/ct/certsearch/page?p={key}" 20 | # only get 2 pages, max seems to be 50 21 | self.limit = 2 22 | 23 | self.parsed = "" 24 | self.result = "" 25 | self.next_key = "" 26 | self.df = pd.DataFrame() 27 | self.paginate_complete = False 28 | 29 | def pagination(self) -> None: 30 | """handles multiple pages of cert results""" 31 | counter = 1 32 | while self.next_key and counter < self.limit: 33 | counter += 1 34 | if self.next_key != "": 35 | key_url = self.api_key.format(key=self.next_key) 36 | elif self.next_key == "": 37 | pass 38 | try: 39 | r = requests.get(key_url) 40 | self.result = r.text 41 | except Exception as e: 42 | print(e) 43 | 44 | self.parse_result() 45 | else: 46 | self.paginate_complete = True 47 | 48 | def parse_result(self) -> None: 49 | """parse the results gotten from json to df""" 50 | # parse and skip first line 51 | self.parsed = json.loads(self.result.split("\n", 2)[2]) 52 | # need this key in case of multiple pages, who knows maybe there is 53 | # only one page so try 54 | try: 55 | self.next_key = self.parsed[0][3][1] 56 | except BaseException: 57 | logger.warning("No results found for %s using Google cert search", self.dom) 58 | raise LookupError 59 | 60 | if self.parsed[0][1] is not None: 61 | for array in self.parsed[0][1]: 62 | a_series = pd.Series(array) 63 | self.df = self.df.append(a_series, ignore_index=True) 64 | else: 65 | logger.error( 66 | "Could not parse cert result for %s, certsearch API may be unavailable", 67 | self.dom, 68 | ) 69 | 70 | def find(self, domain: str) -> DataFrame: 71 | """main method that makes our custom request to find certs""" 72 | self.dom = domain 73 | url = self.api + self.dom 74 | 75 | try: 76 | response = requests.get(url) 77 | except Exception as e: 78 | print(e) 79 | 80 | self.result = response.text 81 | self.parse_result() 82 | self.pagination() 83 | 84 | if self.df.empty is False: 85 | if len(self.df.columns) < 9: 86 | logger.critical("Less than 9 columns detected in: %s", self.dom) 87 | logger.critical("Dataframe for %s: %s", self.dom, self.df) 88 | 89 | # we don't want this confusing dataframe but also don't want to 90 | # crash again 91 | self.df = pd.DataFrame() 92 | return self.df 93 | 94 | self.df.columns = [ 95 | "Unknown", 96 | "CN", 97 | "CA", 98 | "Valid from", 99 | "Valid to", 100 | "Details hash", 101 | "CT logs", 102 | "Unknown", 103 | "DNS names", 104 | ] 105 | 106 | return self.df 107 | -------------------------------------------------------------------------------- /certitude/utils/config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | from importlib import resources 4 | from pathlib import Path 5 | from typing import Dict, Union 6 | 7 | import yaml 8 | 9 | import certitude.utils.data 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | def config(user_config: Union[Path, None] = None) -> Dict: 15 | if user_config: 16 | with open(user_config, "r") as config_stream: 17 | try: 18 | config = yaml.safe_load(config_stream) 19 | return config 20 | except yaml.YAMLError as e: 21 | logger.error( 22 | "[!] Unable to load configuration file.", 23 | exc_info=e, 24 | ) 25 | sys.exit(1) 26 | else: 27 | with resources.open_text( 28 | certitude.utils.data, "config_default.yml" 29 | ) as config_stream: 30 | config = yaml.safe_load(config_stream) 31 | return config 32 | -------------------------------------------------------------------------------- /certitude/utils/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/COSSAS/Certitude/0d04a82d56c1ce8e1b997c0f15ca7031058767de/certitude/utils/data/__init__.py -------------------------------------------------------------------------------- /certitude/utils/data/config_default.yml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - lex_ft_nr_of_domains 4 | - lex_ft_length_first_subdomain 5 | - lex_ft_length_tld 6 | - lex_ft_netloc_length 7 | - lex_ft_query_length 8 | - lex_ft_path_length 9 | - lex_ft_ratio_netloc_path 10 | - lex_ft_nr_dots_query 11 | - lex_ft_nr_dots_path 12 | - lex_ft_nr_at_query 13 | - lex_ft_nr_at_path 14 | - lex_ft_nr_percent_query 15 | - lex_ft_nr_percent_path 16 | - lex_ft_nr_del_query 17 | - lex_ft_nr_del_path 18 | - lex_ft_has_port 19 | - lex_ft_contains_puny 20 | - lex_ft_is_http 21 | - lex_ft_domain_entropy 22 | - lex_ft_query_entropy 23 | - lex_ft_path_entropy 24 | - cert_ft_wildcard_cert 25 | - cert_ft_no_certs_found 26 | - cert_ft_nr_of_certs 27 | - cert_ft_days_since_valid 28 | - cert_ft_days_since_expiry 29 | - cert_ft_renewal_days_mean 30 | - cert_ft_whois_creation_age_days 31 | - cert_ft_whois_dnssec 32 | - cert_ft_whois_expiry_days 33 | - cert_ft_free_ca_issuer 34 | - cert_ft_valid_period 35 | set_missing_whois_to_zero: true 36 | -------------------------------------------------------------------------------- /certitude/utils/data/default_model.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/COSSAS/Certitude/0d04a82d56c1ce8e1b997c0f15ca7031058767de/certitude/utils/data/default_model.pkl -------------------------------------------------------------------------------- /certitude/utils/miscellaneous.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import math 3 | 4 | import pandas as pd 5 | from pandas import DataFrame 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | def count_delimiters(my_string: str) -> int: 11 | """script used for counting delimiters in different parts of URL""" 12 | count = 0 13 | delimiters = [ 14 | ";", 15 | "_", 16 | "?", 17 | "=", 18 | "&", 19 | "|", 20 | "$", 21 | "-", 22 | "_", 23 | ".", 24 | "+", 25 | "!", 26 | "*", 27 | "'", 28 | "(", 29 | ")", 30 | ] 31 | for letter in my_string: 32 | if letter in delimiters: 33 | count = count + 1 34 | return count 35 | 36 | 37 | def entropy(string: str) -> float: 38 | "Calculates the Shannon entropy of a string" 39 | 40 | # get probability of chars in string 41 | prob = [float(string.count(c)) / len(string) for c in dict.fromkeys(list(string))] 42 | 43 | # calculate the entropy 44 | entropy_result = -sum([p * math.log(p) / math.log(2.0) for p in prob]) 45 | 46 | return entropy_result 47 | 48 | 49 | def entropy_ideal(length): 50 | "Calculates the ideal Shannon entropy of a string with given length" 51 | prob = 1.0 / length 52 | return -1.0 * length * prob * math.log(prob) / math.log(2.0) 53 | 54 | 55 | def dataframe_postprocessing(url_object, cert_collection_object) -> DataFrame: 56 | """ 57 | [TRAINING_MODUS ONLY] 58 | This function will be used in 'training_modus' to create a dataframe. 59 | The dataframe is created by adding a new row to it, 60 | everytime a URL is digested in the MagicURLBox stream. 61 | The dataframe will contain the a the following columns: 62 | - 'url_as_string' df_output = "./results/computed_feature_dataframes/fdf_{}_{}.pkl".format( 63 | fname, time_str 64 | )ow, i.e., url, in the dataframe. 65 | The idx will be generated in the main for every URL that is passed in the stream. 66 | """ 67 | 68 | idx = [0] 69 | 70 | my_lex_dict = dict( 71 | filter(lambda item: "ft_" in item[0], url_object.__dict__.items()) 72 | ) 73 | my_lex_dict = {"lex_" + k: v for k, v in my_lex_dict.items()} # add lex_ to keys 74 | df_lex = pd.DataFrame(my_lex_dict, index=idx) 75 | 76 | my_cert_dict = dict( 77 | filter(lambda item: "ft_" in item[0], cert_collection_object.__dict__.items()) 78 | ) 79 | my_cert_dict = {"cert_" + k: v for k, v in my_cert_dict.items()} 80 | df_cert = pd.DataFrame(my_cert_dict, index=idx) 81 | df_res = pd.concat([df_lex, df_cert], axis=1) 82 | 83 | df_res.insert(loc=0, column="url_as_string", value=url_object.url) 84 | 85 | return df_res 86 | -------------------------------------------------------------------------------- /docs/CERTITUDE-logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/COSSAS/Certitude/0d04a82d56c1ce8e1b997c0f15ca7031058767de/docs/CERTITUDE-logo.jpeg -------------------------------------------------------------------------------- /docs/cli-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/COSSAS/Certitude/0d04a82d56c1ce8e1b997c0f15ca7031058767de/docs/cli-flow.png -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "appdirs" 3 | version = "1.4.4" 4 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 5 | category = "dev" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "atomicwrites" 11 | version = "1.4.0" 12 | description = "Atomic file writes." 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 16 | 17 | [[package]] 18 | name = "attrs" 19 | version = "21.2.0" 20 | description = "Classes Without Boilerplate" 21 | category = "dev" 22 | optional = false 23 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 24 | 25 | [package.extras] 26 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] 27 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 28 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] 29 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] 30 | 31 | [[package]] 32 | name = "backports.entry-points-selectable" 33 | version = "1.1.0" 34 | description = "Compatibility shim providing selectable entry points for older implementations" 35 | category = "dev" 36 | optional = false 37 | python-versions = ">=2.7" 38 | 39 | [package.extras] 40 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 41 | testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] 42 | 43 | [[package]] 44 | name = "bandit" 45 | version = "1.7.0" 46 | description = "Security oriented static analyser for python code." 47 | category = "dev" 48 | optional = false 49 | python-versions = ">=3.5" 50 | 51 | [package.dependencies] 52 | colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} 53 | GitPython = ">=1.0.1" 54 | PyYAML = ">=5.3.1" 55 | six = ">=1.10.0" 56 | stevedore = ">=1.20.0" 57 | 58 | [[package]] 59 | name = "black" 60 | version = "20.8b1" 61 | description = "The uncompromising code formatter." 62 | category = "dev" 63 | optional = false 64 | python-versions = ">=3.6" 65 | 66 | [package.dependencies] 67 | appdirs = "*" 68 | click = ">=7.1.2" 69 | mypy-extensions = ">=0.4.3" 70 | pathspec = ">=0.6,<1" 71 | regex = ">=2020.1.8" 72 | toml = ">=0.10.1" 73 | typed-ast = ">=1.4.0" 74 | typing-extensions = ">=3.7.4" 75 | 76 | [package.extras] 77 | colorama = ["colorama (>=0.4.3)"] 78 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 79 | 80 | [[package]] 81 | name = "build" 82 | version = "0.4.0" 83 | description = "A simple, correct PEP517 package builder" 84 | category = "dev" 85 | optional = false 86 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 87 | 88 | [package.dependencies] 89 | colorama = {version = "*", markers = "os_name == \"nt\""} 90 | packaging = ">=19.0" 91 | pep517 = ">=0.9.1" 92 | toml = ">=0.10.0" 93 | 94 | [package.extras] 95 | docs = ["furo (>=2020.11.19b18)", "sphinx (>=3.0,<4.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)"] 96 | test = ["filelock (>=3)", "pytest (>=4)", "pytest-cov (>=2)", "pytest-mock (>=2)", "pytest-xdist (>=1.34)"] 97 | typing = ["mypy (==0.800)", "typing-extensions (>=3.7.4.3)"] 98 | virtualenv = ["virtualenv (>=20.0.35)"] 99 | 100 | [[package]] 101 | name = "certifi" 102 | version = "2021.5.30" 103 | description = "Python package for providing Mozilla's CA Bundle." 104 | category = "main" 105 | optional = false 106 | python-versions = "*" 107 | 108 | [[package]] 109 | name = "cfgv" 110 | version = "3.3.0" 111 | description = "Validate configuration and produce human readable error messages." 112 | category = "dev" 113 | optional = false 114 | python-versions = ">=3.6.1" 115 | 116 | [[package]] 117 | name = "charset-normalizer" 118 | version = "2.0.4" 119 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 120 | category = "main" 121 | optional = false 122 | python-versions = ">=3.5.0" 123 | 124 | [package.extras] 125 | unicode_backport = ["unicodedata2"] 126 | 127 | [[package]] 128 | name = "click" 129 | version = "8.0.1" 130 | description = "Composable command line interface toolkit" 131 | category = "dev" 132 | optional = false 133 | python-versions = ">=3.6" 134 | 135 | [package.dependencies] 136 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 137 | 138 | [[package]] 139 | name = "colorama" 140 | version = "0.4.4" 141 | description = "Cross-platform colored terminal text." 142 | category = "dev" 143 | optional = false 144 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 145 | 146 | [[package]] 147 | name = "coverage" 148 | version = "5.5" 149 | description = "Code coverage measurement for Python" 150 | category = "dev" 151 | optional = false 152 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 153 | 154 | [package.extras] 155 | toml = ["toml"] 156 | 157 | [[package]] 158 | name = "distlib" 159 | version = "0.3.2" 160 | description = "Distribution utilities" 161 | category = "dev" 162 | optional = false 163 | python-versions = "*" 164 | 165 | [[package]] 166 | name = "filelock" 167 | version = "3.0.12" 168 | description = "A platform independent file lock." 169 | category = "dev" 170 | optional = false 171 | python-versions = "*" 172 | 173 | [[package]] 174 | name = "flake8" 175 | version = "3.9.2" 176 | description = "the modular source code checker: pep8 pyflakes and co" 177 | category = "dev" 178 | optional = false 179 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 180 | 181 | [package.dependencies] 182 | mccabe = ">=0.6.0,<0.7.0" 183 | pycodestyle = ">=2.7.0,<2.8.0" 184 | pyflakes = ">=2.3.0,<2.4.0" 185 | 186 | [[package]] 187 | name = "gitdb" 188 | version = "4.0.7" 189 | description = "Git Object Database" 190 | category = "dev" 191 | optional = false 192 | python-versions = ">=3.4" 193 | 194 | [package.dependencies] 195 | smmap = ">=3.0.1,<5" 196 | 197 | [[package]] 198 | name = "gitpython" 199 | version = "3.1.20" 200 | description = "Python Git Library" 201 | category = "dev" 202 | optional = false 203 | python-versions = ">=3.6" 204 | 205 | [package.dependencies] 206 | gitdb = ">=4.0.1,<5" 207 | typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\""} 208 | 209 | [[package]] 210 | name = "identify" 211 | version = "2.2.13" 212 | description = "File identification library for Python" 213 | category = "dev" 214 | optional = false 215 | python-versions = ">=3.6.1" 216 | 217 | [package.extras] 218 | license = ["editdistance-s"] 219 | 220 | [[package]] 221 | name = "idna" 222 | version = "3.2" 223 | description = "Internationalized Domain Names in Applications (IDNA)" 224 | category = "main" 225 | optional = false 226 | python-versions = ">=3.5" 227 | 228 | [[package]] 229 | name = "importlib-metadata" 230 | version = "4.6.4" 231 | description = "Read metadata from Python packages" 232 | category = "dev" 233 | optional = false 234 | python-versions = ">=3.6" 235 | 236 | [package.dependencies] 237 | zipp = ">=0.5" 238 | 239 | [package.extras] 240 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 241 | perf = ["ipython"] 242 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] 243 | 244 | [[package]] 245 | name = "iniconfig" 246 | version = "1.1.1" 247 | description = "iniconfig: brain-dead simple config-ini parsing" 248 | category = "dev" 249 | optional = false 250 | python-versions = "*" 251 | 252 | [[package]] 253 | name = "isort" 254 | version = "5.9.3" 255 | description = "A Python utility / library to sort Python imports." 256 | category = "dev" 257 | optional = false 258 | python-versions = ">=3.6.1,<4.0" 259 | 260 | [package.extras] 261 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 262 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 263 | colors = ["colorama (>=0.4.3,<0.5.0)"] 264 | plugins = ["setuptools"] 265 | 266 | [[package]] 267 | name = "joblib" 268 | version = "1.0.1" 269 | description = "Lightweight pipelining with Python functions" 270 | category = "main" 271 | optional = false 272 | python-versions = ">=3.6" 273 | 274 | [[package]] 275 | name = "mccabe" 276 | version = "0.6.1" 277 | description = "McCabe checker, plugin for flake8" 278 | category = "dev" 279 | optional = false 280 | python-versions = "*" 281 | 282 | [[package]] 283 | name = "mypy" 284 | version = "0.812" 285 | description = "Optional static typing for Python" 286 | category = "dev" 287 | optional = false 288 | python-versions = ">=3.5" 289 | 290 | [package.dependencies] 291 | mypy-extensions = ">=0.4.3,<0.5.0" 292 | typed-ast = ">=1.4.0,<1.5.0" 293 | typing-extensions = ">=3.7.4" 294 | 295 | [package.extras] 296 | dmypy = ["psutil (>=4.0)"] 297 | 298 | [[package]] 299 | name = "mypy-extensions" 300 | version = "0.4.3" 301 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 302 | category = "dev" 303 | optional = false 304 | python-versions = "*" 305 | 306 | [[package]] 307 | name = "nodeenv" 308 | version = "1.6.0" 309 | description = "Node.js virtual environment builder" 310 | category = "dev" 311 | optional = false 312 | python-versions = "*" 313 | 314 | [[package]] 315 | name = "numpy" 316 | version = "1.21.1" 317 | description = "NumPy is the fundamental package for array computing with Python." 318 | category = "main" 319 | optional = false 320 | python-versions = ">=3.7" 321 | 322 | [[package]] 323 | name = "packaging" 324 | version = "21.0" 325 | description = "Core utilities for Python packages" 326 | category = "dev" 327 | optional = false 328 | python-versions = ">=3.6" 329 | 330 | [package.dependencies] 331 | pyparsing = ">=2.0.2" 332 | 333 | [[package]] 334 | name = "pandas" 335 | version = "1.3.2" 336 | description = "Powerful data structures for data analysis, time series, and statistics" 337 | category = "main" 338 | optional = false 339 | python-versions = ">=3.7.1" 340 | 341 | [package.dependencies] 342 | numpy = ">=1.17.3" 343 | python-dateutil = ">=2.7.3" 344 | pytz = ">=2017.3" 345 | 346 | [package.extras] 347 | test = ["hypothesis (>=3.58)", "pytest (>=6.0)", "pytest-xdist"] 348 | 349 | [[package]] 350 | name = "pathspec" 351 | version = "0.9.0" 352 | description = "Utility library for gitignore style pattern matching of file paths." 353 | category = "dev" 354 | optional = false 355 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 356 | 357 | [[package]] 358 | name = "pbr" 359 | version = "5.6.0" 360 | description = "Python Build Reasonableness" 361 | category = "dev" 362 | optional = false 363 | python-versions = ">=2.6" 364 | 365 | [[package]] 366 | name = "pep517" 367 | version = "0.11.0" 368 | description = "Wrappers to build Python packages using PEP 517 hooks" 369 | category = "dev" 370 | optional = false 371 | python-versions = "*" 372 | 373 | [package.dependencies] 374 | tomli = {version = "*", markers = "python_version >= \"3.6\""} 375 | 376 | [[package]] 377 | name = "platformdirs" 378 | version = "2.2.0" 379 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 380 | category = "dev" 381 | optional = false 382 | python-versions = ">=3.6" 383 | 384 | [package.extras] 385 | docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] 386 | test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] 387 | 388 | [[package]] 389 | name = "pluggy" 390 | version = "0.13.1" 391 | description = "plugin and hook calling mechanisms for python" 392 | category = "dev" 393 | optional = false 394 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 395 | 396 | [package.extras] 397 | dev = ["pre-commit", "tox"] 398 | 399 | [[package]] 400 | name = "pre-commit" 401 | version = "2.14.0" 402 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 403 | category = "dev" 404 | optional = false 405 | python-versions = ">=3.6.1" 406 | 407 | [package.dependencies] 408 | cfgv = ">=2.0.0" 409 | identify = ">=1.0.0" 410 | nodeenv = ">=0.11.1" 411 | pyyaml = ">=5.1" 412 | toml = "*" 413 | virtualenv = ">=20.0.8" 414 | 415 | [[package]] 416 | name = "py" 417 | version = "1.10.0" 418 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 419 | category = "dev" 420 | optional = false 421 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 422 | 423 | [[package]] 424 | name = "pycodestyle" 425 | version = "2.7.0" 426 | description = "Python style guide checker" 427 | category = "dev" 428 | optional = false 429 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 430 | 431 | [[package]] 432 | name = "pyflakes" 433 | version = "2.3.1" 434 | description = "passive checker of Python programs" 435 | category = "dev" 436 | optional = false 437 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 438 | 439 | [[package]] 440 | name = "pylic" 441 | version = "1.2.5" 442 | description = "A Python license checker" 443 | category = "dev" 444 | optional = false 445 | python-versions = ">=3.6" 446 | 447 | [package.dependencies] 448 | importlib-metadata = "*" 449 | toml = "*" 450 | 451 | [[package]] 452 | name = "pyparsing" 453 | version = "2.4.7" 454 | description = "Python parsing module" 455 | category = "dev" 456 | optional = false 457 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 458 | 459 | [[package]] 460 | name = "pytest" 461 | version = "6.2.4" 462 | description = "pytest: simple powerful testing with Python" 463 | category = "dev" 464 | optional = false 465 | python-versions = ">=3.6" 466 | 467 | [package.dependencies] 468 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 469 | attrs = ">=19.2.0" 470 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 471 | iniconfig = "*" 472 | packaging = "*" 473 | pluggy = ">=0.12,<1.0.0a1" 474 | py = ">=1.8.2" 475 | toml = "*" 476 | 477 | [package.extras] 478 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 479 | 480 | [[package]] 481 | name = "pytest-cov" 482 | version = "2.12.1" 483 | description = "Pytest plugin for measuring coverage." 484 | category = "dev" 485 | optional = false 486 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 487 | 488 | [package.dependencies] 489 | coverage = ">=5.2.1" 490 | pytest = ">=4.6" 491 | toml = "*" 492 | 493 | [package.extras] 494 | testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] 495 | 496 | [[package]] 497 | name = "python-dateutil" 498 | version = "2.8.2" 499 | description = "Extensions to the standard Python datetime module" 500 | category = "main" 501 | optional = false 502 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 503 | 504 | [package.dependencies] 505 | six = ">=1.5" 506 | 507 | [[package]] 508 | name = "pytz" 509 | version = "2021.1" 510 | description = "World timezone definitions, modern and historical" 511 | category = "main" 512 | optional = false 513 | python-versions = "*" 514 | 515 | [[package]] 516 | name = "pyyaml" 517 | version = "5.4.1" 518 | description = "YAML parser and emitter for Python" 519 | category = "main" 520 | optional = false 521 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 522 | 523 | [[package]] 524 | name = "regex" 525 | version = "2021.8.21" 526 | description = "Alternative regular expression module, to replace re." 527 | category = "dev" 528 | optional = false 529 | python-versions = "*" 530 | 531 | [[package]] 532 | name = "requests" 533 | version = "2.26.0" 534 | description = "Python HTTP for Humans." 535 | category = "main" 536 | optional = false 537 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 538 | 539 | [package.dependencies] 540 | certifi = ">=2017.4.17" 541 | charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} 542 | idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} 543 | urllib3 = ">=1.21.1,<1.27" 544 | 545 | [package.extras] 546 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 547 | use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] 548 | 549 | [[package]] 550 | name = "scikit-learn" 551 | version = "0.24.2" 552 | description = "A set of python modules for machine learning and data mining" 553 | category = "main" 554 | optional = false 555 | python-versions = ">=3.6" 556 | 557 | [package.dependencies] 558 | joblib = ">=0.11" 559 | numpy = ">=1.13.3" 560 | scipy = ">=0.19.1" 561 | threadpoolctl = ">=2.0.0" 562 | 563 | [package.extras] 564 | benchmark = ["matplotlib (>=2.1.1)", "pandas (>=0.25.0)", "memory-profiler (>=0.57.0)"] 565 | docs = ["matplotlib (>=2.1.1)", "scikit-image (>=0.13)", "pandas (>=0.25.0)", "seaborn (>=0.9.0)", "memory-profiler (>=0.57.0)", "sphinx (>=3.2.0)", "sphinx-gallery (>=0.7.0)", "numpydoc (>=1.0.0)", "Pillow (>=7.1.2)", "sphinx-prompt (>=1.3.0)"] 566 | examples = ["matplotlib (>=2.1.1)", "scikit-image (>=0.13)", "pandas (>=0.25.0)", "seaborn (>=0.9.0)"] 567 | tests = ["matplotlib (>=2.1.1)", "scikit-image (>=0.13)", "pandas (>=0.25.0)", "pytest (>=5.0.1)", "pytest-cov (>=2.9.0)", "flake8 (>=3.8.2)", "mypy (>=0.770)", "pyamg (>=4.0.0)"] 568 | 569 | [[package]] 570 | name = "scipy" 571 | version = "1.6.1" 572 | description = "SciPy: Scientific Library for Python" 573 | category = "main" 574 | optional = false 575 | python-versions = ">=3.7" 576 | 577 | [package.dependencies] 578 | numpy = ">=1.16.5" 579 | 580 | [[package]] 581 | name = "six" 582 | version = "1.16.0" 583 | description = "Python 2 and 3 compatibility utilities" 584 | category = "main" 585 | optional = false 586 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 587 | 588 | [[package]] 589 | name = "smmap" 590 | version = "4.0.0" 591 | description = "A pure Python implementation of a sliding window memory map manager" 592 | category = "dev" 593 | optional = false 594 | python-versions = ">=3.5" 595 | 596 | [[package]] 597 | name = "stevedore" 598 | version = "3.4.0" 599 | description = "Manage dynamic plugins for Python applications" 600 | category = "dev" 601 | optional = false 602 | python-versions = ">=3.6" 603 | 604 | [package.dependencies] 605 | pbr = ">=2.0.0,<2.1.0 || >2.1.0" 606 | 607 | [[package]] 608 | name = "threadpoolctl" 609 | version = "2.2.0" 610 | description = "threadpoolctl" 611 | category = "main" 612 | optional = false 613 | python-versions = ">=3.6" 614 | 615 | [[package]] 616 | name = "toml" 617 | version = "0.10.2" 618 | description = "Python Library for Tom's Obvious, Minimal Language" 619 | category = "dev" 620 | optional = false 621 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 622 | 623 | [[package]] 624 | name = "tomli" 625 | version = "1.2.1" 626 | description = "A lil' TOML parser" 627 | category = "dev" 628 | optional = false 629 | python-versions = ">=3.6" 630 | 631 | [[package]] 632 | name = "typed-ast" 633 | version = "1.4.3" 634 | description = "a fork of Python 2 and 3 ast modules with type comment support" 635 | category = "dev" 636 | optional = false 637 | python-versions = "*" 638 | 639 | [[package]] 640 | name = "typing-extensions" 641 | version = "3.10.0.0" 642 | description = "Backported and Experimental Type Hints for Python 3.5+" 643 | category = "dev" 644 | optional = false 645 | python-versions = "*" 646 | 647 | [[package]] 648 | name = "urllib3" 649 | version = "1.26.6" 650 | description = "HTTP library with thread-safe connection pooling, file post, and more." 651 | category = "main" 652 | optional = false 653 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 654 | 655 | [package.extras] 656 | brotli = ["brotlipy (>=0.6.0)"] 657 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 658 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 659 | 660 | [[package]] 661 | name = "virtualenv" 662 | version = "20.7.2" 663 | description = "Virtual Python Environment builder" 664 | category = "dev" 665 | optional = false 666 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 667 | 668 | [package.dependencies] 669 | "backports.entry-points-selectable" = ">=1.0.4" 670 | distlib = ">=0.3.1,<1" 671 | filelock = ">=3.0.0,<4" 672 | platformdirs = ">=2,<3" 673 | six = ">=1.9.0,<2" 674 | 675 | [package.extras] 676 | docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] 677 | testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] 678 | 679 | [[package]] 680 | name = "vulture" 681 | version = "2.3" 682 | description = "Find dead code" 683 | category = "dev" 684 | optional = false 685 | python-versions = ">=3.6" 686 | 687 | [package.dependencies] 688 | toml = "*" 689 | 690 | [[package]] 691 | name = "whois" 692 | version = "0.9.13" 693 | description = "Python package for retrieving WHOIS information of domains." 694 | category = "main" 695 | optional = false 696 | python-versions = "*" 697 | 698 | [[package]] 699 | name = "zipp" 700 | version = "3.5.0" 701 | description = "Backport of pathlib-compatible object wrapper for zip files" 702 | category = "dev" 703 | optional = false 704 | python-versions = ">=3.6" 705 | 706 | [package.extras] 707 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 708 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 709 | 710 | [metadata] 711 | lock-version = "1.1" 712 | python-versions = "^3.9" 713 | content-hash = "df2e1d27480ba5e2b7fac6c5ed66954a80fdd4d00a8654cd8d70f582b25075ab" 714 | 715 | [metadata.files] 716 | appdirs = [ 717 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 718 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 719 | ] 720 | atomicwrites = [ 721 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 722 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 723 | ] 724 | attrs = [ 725 | {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, 726 | {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, 727 | ] 728 | "backports.entry-points-selectable" = [ 729 | {file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"}, 730 | {file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"}, 731 | ] 732 | bandit = [ 733 | {file = "bandit-1.7.0-py3-none-any.whl", hash = "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07"}, 734 | {file = "bandit-1.7.0.tar.gz", hash = "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"}, 735 | ] 736 | black = [ 737 | {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, 738 | ] 739 | build = [ 740 | {file = "build-0.4.0-py2.py3-none-any.whl", hash = "sha256:5950f98775a59f0c5ac68586691003d2db58a809fbea2ade3fe32109dfd12790"}, 741 | {file = "build-0.4.0.tar.gz", hash = "sha256:b798f3f490c779fa88c99816ebee97ab636acd6630b1d91c8cf8eb8a4d922a19"}, 742 | ] 743 | certifi = [ 744 | {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, 745 | {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, 746 | ] 747 | cfgv = [ 748 | {file = "cfgv-3.3.0-py2.py3-none-any.whl", hash = "sha256:b449c9c6118fe8cca7fa5e00b9ec60ba08145d281d52164230a69211c5d597a1"}, 749 | {file = "cfgv-3.3.0.tar.gz", hash = "sha256:9e600479b3b99e8af981ecdfc80a0296104ee610cab48a5ae4ffd0b668650eb1"}, 750 | ] 751 | charset-normalizer = [ 752 | {file = "charset-normalizer-2.0.4.tar.gz", hash = "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"}, 753 | {file = "charset_normalizer-2.0.4-py3-none-any.whl", hash = "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b"}, 754 | ] 755 | click = [ 756 | {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, 757 | {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, 758 | ] 759 | colorama = [ 760 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 761 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 762 | ] 763 | coverage = [ 764 | {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, 765 | {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, 766 | {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, 767 | {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, 768 | {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, 769 | {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, 770 | {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, 771 | {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, 772 | {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, 773 | {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, 774 | {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, 775 | {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, 776 | {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, 777 | {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, 778 | {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, 779 | {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, 780 | {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, 781 | {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, 782 | {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, 783 | {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, 784 | {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, 785 | {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, 786 | {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, 787 | {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, 788 | {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, 789 | {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, 790 | {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, 791 | {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, 792 | {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, 793 | {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, 794 | {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, 795 | {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, 796 | {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, 797 | {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, 798 | {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, 799 | {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, 800 | {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, 801 | {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, 802 | {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, 803 | {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, 804 | {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, 805 | {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, 806 | {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, 807 | {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, 808 | {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, 809 | {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, 810 | {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, 811 | {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, 812 | {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, 813 | {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, 814 | {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, 815 | {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, 816 | ] 817 | distlib = [ 818 | {file = "distlib-0.3.2-py2.py3-none-any.whl", hash = "sha256:23e223426b28491b1ced97dc3bbe183027419dfc7982b4fa2f05d5f3ff10711c"}, 819 | {file = "distlib-0.3.2.zip", hash = "sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736"}, 820 | ] 821 | filelock = [ 822 | {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, 823 | {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, 824 | ] 825 | flake8 = [ 826 | {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, 827 | {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, 828 | ] 829 | gitdb = [ 830 | {file = "gitdb-4.0.7-py3-none-any.whl", hash = "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0"}, 831 | {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"}, 832 | ] 833 | gitpython = [ 834 | {file = "GitPython-3.1.20-py3-none-any.whl", hash = "sha256:b1e1c269deab1b08ce65403cf14e10d2ef1f6c89e33ea7c5e5bb0222ea593b8a"}, 835 | {file = "GitPython-3.1.20.tar.gz", hash = "sha256:df0e072a200703a65387b0cfdf0466e3bab729c0458cf6b7349d0e9877636519"}, 836 | ] 837 | identify = [ 838 | {file = "identify-2.2.13-py2.py3-none-any.whl", hash = "sha256:7199679b5be13a6b40e6e19ea473e789b11b4e3b60986499b1f589ffb03c217c"}, 839 | {file = "identify-2.2.13.tar.gz", hash = "sha256:7bc6e829392bd017236531963d2d937d66fc27cadc643ac0aba2ce9f26157c79"}, 840 | ] 841 | idna = [ 842 | {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, 843 | {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, 844 | ] 845 | importlib-metadata = [ 846 | {file = "importlib_metadata-4.6.4-py3-none-any.whl", hash = "sha256:ed5157fef23a4bc4594615a0dd8eba94b2bb36bf2a343fa3d8bb2fa0a62a99d5"}, 847 | {file = "importlib_metadata-4.6.4.tar.gz", hash = "sha256:7b30a78db2922d78a6f47fb30683156a14f3c6aa5cc23f77cc8967e9ab2d002f"}, 848 | ] 849 | iniconfig = [ 850 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 851 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 852 | ] 853 | isort = [ 854 | {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, 855 | {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"}, 856 | ] 857 | joblib = [ 858 | {file = "joblib-1.0.1-py3-none-any.whl", hash = "sha256:feeb1ec69c4d45129954f1b7034954241eedfd6ba39b5e9e4b6883be3332d5e5"}, 859 | {file = "joblib-1.0.1.tar.gz", hash = "sha256:9c17567692206d2f3fb9ecf5e991084254fe631665c450b443761c4186a613f7"}, 860 | ] 861 | mccabe = [ 862 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 863 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 864 | ] 865 | mypy = [ 866 | {file = "mypy-0.812-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a26f8ec704e5a7423c8824d425086705e381b4f1dfdef6e3a1edab7ba174ec49"}, 867 | {file = "mypy-0.812-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:28fb5479c494b1bab244620685e2eb3c3f988d71fd5d64cc753195e8ed53df7c"}, 868 | {file = "mypy-0.812-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:9743c91088d396c1a5a3c9978354b61b0382b4e3c440ce83cf77994a43e8c521"}, 869 | {file = "mypy-0.812-cp35-cp35m-win_amd64.whl", hash = "sha256:d7da2e1d5f558c37d6e8c1246f1aec1e7349e4913d8fb3cb289a35de573fe2eb"}, 870 | {file = "mypy-0.812-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4eec37370483331d13514c3f55f446fc5248d6373e7029a29ecb7b7494851e7a"}, 871 | {file = "mypy-0.812-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d65cc1df038ef55a99e617431f0553cd77763869eebdf9042403e16089fe746c"}, 872 | {file = "mypy-0.812-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:61a3d5b97955422964be6b3baf05ff2ce7f26f52c85dd88db11d5e03e146a3a6"}, 873 | {file = "mypy-0.812-cp36-cp36m-win_amd64.whl", hash = "sha256:25adde9b862f8f9aac9d2d11971f226bd4c8fbaa89fb76bdadb267ef22d10064"}, 874 | {file = "mypy-0.812-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:552a815579aa1e995f39fd05dde6cd378e191b063f031f2acfe73ce9fb7f9e56"}, 875 | {file = "mypy-0.812-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:499c798053cdebcaa916eef8cd733e5584b5909f789de856b482cd7d069bdad8"}, 876 | {file = "mypy-0.812-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:5873888fff1c7cf5b71efbe80e0e73153fe9212fafdf8e44adfe4c20ec9f82d7"}, 877 | {file = "mypy-0.812-cp37-cp37m-win_amd64.whl", hash = "sha256:9f94aac67a2045ec719ffe6111df543bac7874cee01f41928f6969756e030564"}, 878 | {file = "mypy-0.812-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d23e0ea196702d918b60c8288561e722bf437d82cb7ef2edcd98cfa38905d506"}, 879 | {file = "mypy-0.812-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:674e822aa665b9fd75130c6c5f5ed9564a38c6cea6a6432ce47eafb68ee578c5"}, 880 | {file = "mypy-0.812-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:abf7e0c3cf117c44d9285cc6128856106183938c68fd4944763003decdcfeb66"}, 881 | {file = "mypy-0.812-cp38-cp38-win_amd64.whl", hash = "sha256:0d0a87c0e7e3a9becdfbe936c981d32e5ee0ccda3e0f07e1ef2c3d1a817cf73e"}, 882 | {file = "mypy-0.812-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7ce3175801d0ae5fdfa79b4f0cfed08807af4d075b402b7e294e6aa72af9aa2a"}, 883 | {file = "mypy-0.812-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:b09669bcda124e83708f34a94606e01b614fa71931d356c1f1a5297ba11f110a"}, 884 | {file = "mypy-0.812-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:33f159443db0829d16f0a8d83d94df3109bb6dd801975fe86bacb9bf71628e97"}, 885 | {file = "mypy-0.812-cp39-cp39-win_amd64.whl", hash = "sha256:3f2aca7f68580dc2508289c729bd49ee929a436208d2b2b6aab15745a70a57df"}, 886 | {file = "mypy-0.812-py3-none-any.whl", hash = "sha256:2f9b3407c58347a452fc0736861593e105139b905cca7d097e413453a1d650b4"}, 887 | {file = "mypy-0.812.tar.gz", hash = "sha256:cd07039aa5df222037005b08fbbfd69b3ab0b0bd7a07d7906de75ae52c4e3119"}, 888 | ] 889 | mypy-extensions = [ 890 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 891 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 892 | ] 893 | nodeenv = [ 894 | {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, 895 | {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, 896 | ] 897 | numpy = [ 898 | {file = "numpy-1.21.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38e8648f9449a549a7dfe8d8755a5979b45b3538520d1e735637ef28e8c2dc50"}, 899 | {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd7d7409fa643a91d0a05c7554dd68aa9c9bb16e186f6ccfe40d6e003156e33a"}, 900 | {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a75b4498b1e93d8b700282dc8e655b8bd559c0904b3910b144646dbbbc03e062"}, 901 | {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1412aa0aec3e00bc23fbb8664d76552b4efde98fb71f60737c83efbac24112f1"}, 902 | {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e46ceaff65609b5399163de5893d8f2a82d3c77d5e56d976c8b5fb01faa6b671"}, 903 | {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c6a2324085dd52f96498419ba95b5777e40b6bcbc20088fddb9e8cbb58885e8e"}, 904 | {file = "numpy-1.21.1-cp37-cp37m-win32.whl", hash = "sha256:73101b2a1fef16602696d133db402a7e7586654682244344b8329cdcbbb82172"}, 905 | {file = "numpy-1.21.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7a708a79c9a9d26904d1cca8d383bf869edf6f8e7650d85dbc77b041e8c5a0f8"}, 906 | {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95b995d0c413f5d0428b3f880e8fe1660ff9396dcd1f9eedbc311f37b5652e16"}, 907 | {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:635e6bd31c9fb3d475c8f44a089569070d10a9ef18ed13738b03049280281267"}, 908 | {file = "numpy-1.21.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a3d5fb89bfe21be2ef47c0614b9c9c707b7362386c9a3ff1feae63e0267ccb6"}, 909 | {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a326af80e86d0e9ce92bcc1e65c8ff88297de4fa14ee936cb2293d414c9ec63"}, 910 | {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:791492091744b0fe390a6ce85cc1bf5149968ac7d5f0477288f78c89b385d9af"}, 911 | {file = "numpy-1.21.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0318c465786c1f63ac05d7c4dbcecd4d2d7e13f0959b01b534ea1e92202235c5"}, 912 | {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a513bd9c1551894ee3d31369f9b07460ef223694098cf27d399513415855b68"}, 913 | {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:91c6f5fc58df1e0a3cc0c3a717bb3308ff850abdaa6d2d802573ee2b11f674a8"}, 914 | {file = "numpy-1.21.1-cp38-cp38-win32.whl", hash = "sha256:978010b68e17150db8765355d1ccdd450f9fc916824e8c4e35ee620590e234cd"}, 915 | {file = "numpy-1.21.1-cp38-cp38-win_amd64.whl", hash = "sha256:9749a40a5b22333467f02fe11edc98f022133ee1bfa8ab99bda5e5437b831214"}, 916 | {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d7a4aeac3b94af92a9373d6e77b37691b86411f9745190d2c351f410ab3a791f"}, 917 | {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9e7912a56108aba9b31df688a4c4f5cb0d9d3787386b87d504762b6754fbb1b"}, 918 | {file = "numpy-1.21.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:25b40b98ebdd272bc3020935427a4530b7d60dfbe1ab9381a39147834e985eac"}, 919 | {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a92c5aea763d14ba9d6475803fc7904bda7decc2a0a68153f587ad82941fec1"}, 920 | {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05a0f648eb28bae4bcb204e6fd14603de2908de982e761a2fc78efe0f19e96e1"}, 921 | {file = "numpy-1.21.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f01f28075a92eede918b965e86e8f0ba7b7797a95aa8d35e1cc8821f5fc3ad6a"}, 922 | {file = "numpy-1.21.1-cp39-cp39-win32.whl", hash = "sha256:88c0b89ad1cc24a5efbb99ff9ab5db0f9a86e9cc50240177a571fbe9c2860ac2"}, 923 | {file = "numpy-1.21.1-cp39-cp39-win_amd64.whl", hash = "sha256:01721eefe70544d548425a07c80be8377096a54118070b8a62476866d5208e33"}, 924 | {file = "numpy-1.21.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2d4d1de6e6fb3d28781c73fbde702ac97f03d79e4ffd6598b880b2d95d62ead4"}, 925 | {file = "numpy-1.21.1.zip", hash = "sha256:dff4af63638afcc57a3dfb9e4b26d434a7a602d225b42d746ea7fe2edf1342fd"}, 926 | ] 927 | packaging = [ 928 | {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, 929 | {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, 930 | ] 931 | pandas = [ 932 | {file = "pandas-1.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ba7ceb8abc6dbdb1e34612d1173d61e4941f1a1eb7e6f703b2633134ae6a6c89"}, 933 | {file = "pandas-1.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcb71b1935249de80e3a808227189eee381d4d74a31760ced2df21eedc92a8e3"}, 934 | {file = "pandas-1.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa54dc1d3e5d004a09ab0b1751473698011ddf03e14f1f59b84ad9a6ac630975"}, 935 | {file = "pandas-1.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34ced9ce5d5b17b556486da7256961b55b471d64a8990b56e67a84ebeb259416"}, 936 | {file = "pandas-1.3.2-cp37-cp37m-win32.whl", hash = "sha256:a56246de744baf646d1f3e050c4653d632bc9cd2e0605f41051fea59980e880a"}, 937 | {file = "pandas-1.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:53b17e4debba26b7446b1e4795c19f94f0c715e288e08145e44bdd2865e819b3"}, 938 | {file = "pandas-1.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f07a9745ca075ae73a5ce116f5e58f691c0dc9de0bff163527858459df5c176f"}, 939 | {file = "pandas-1.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9e8e0ce5284ebebe110efd652c164ed6eab77f5de4c3533abc756302ee77765"}, 940 | {file = "pandas-1.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59a78d7066d1c921a77e3306aa0ebf6e55396c097d5dfcc4df8defe3dcecb735"}, 941 | {file = "pandas-1.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:132def05e73d292c949b02e7ef873debb77acc44a8b119d215921046f0c3a91d"}, 942 | {file = "pandas-1.3.2-cp38-cp38-win32.whl", hash = "sha256:69e1b2f5811f46827722fd641fdaeedb26002bd1e504eacc7a8ec36bdc25393e"}, 943 | {file = "pandas-1.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:7996d311413379136baf0f3cf2a10e331697657c87ced3f17ac7c77f77fe34a3"}, 944 | {file = "pandas-1.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1738154049062156429a5cf2fd79a69c9f3fa4f231346a7ec6fd156cd1a9a621"}, 945 | {file = "pandas-1.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cce01f6d655b4add966fcd36c32c5d1fe84628e200626b3f5e2f40db2d16a0f"}, 946 | {file = "pandas-1.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1099e2a0cd3a01ec62cca183fc1555833a2d43764950ef8cb5948c8abfc51014"}, 947 | {file = "pandas-1.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cd5776be891331a3e6b425b5abeab9596abea18435c5982191356f9b24ae731"}, 948 | {file = "pandas-1.3.2-cp39-cp39-win32.whl", hash = "sha256:66a95361b81b4ba04b699ecd2416b0591f40cd1e24c60a8bfe0d19009cfa575a"}, 949 | {file = "pandas-1.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:89f40e5d21814192802421df809f948247d39ffe171e45fe2ab4abf7bd4279d8"}, 950 | {file = "pandas-1.3.2.tar.gz", hash = "sha256:cbcb84d63867af3411fa063af3de64902665bb5b3d40b25b2059e40603594e87"}, 951 | ] 952 | pathspec = [ 953 | {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, 954 | {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, 955 | ] 956 | pbr = [ 957 | {file = "pbr-5.6.0-py2.py3-none-any.whl", hash = "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"}, 958 | {file = "pbr-5.6.0.tar.gz", hash = "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd"}, 959 | ] 960 | pep517 = [ 961 | {file = "pep517-0.11.0-py2.py3-none-any.whl", hash = "sha256:3fa6b85b9def7ba4de99fb7f96fe3f02e2d630df8aa2720a5cf3b183f087a738"}, 962 | {file = "pep517-0.11.0.tar.gz", hash = "sha256:e1ba5dffa3a131387979a68ff3e391ac7d645be409216b961bc2efe6468ab0b2"}, 963 | ] 964 | platformdirs = [ 965 | {file = "platformdirs-2.2.0-py3-none-any.whl", hash = "sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c"}, 966 | {file = "platformdirs-2.2.0.tar.gz", hash = "sha256:632daad3ab546bd8e6af0537d09805cec458dce201bccfe23012df73332e181e"}, 967 | ] 968 | pluggy = [ 969 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 970 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 971 | ] 972 | pre-commit = [ 973 | {file = "pre_commit-2.14.0-py2.py3-none-any.whl", hash = "sha256:ec3045ae62e1aa2eecfb8e86fa3025c2e3698f77394ef8d2011ce0aedd85b2d4"}, 974 | {file = "pre_commit-2.14.0.tar.gz", hash = "sha256:2386eeb4cf6633712c7cc9ede83684d53c8cafca6b59f79c738098b51c6d206c"}, 975 | ] 976 | py = [ 977 | {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, 978 | {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, 979 | ] 980 | pycodestyle = [ 981 | {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, 982 | {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, 983 | ] 984 | pyflakes = [ 985 | {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, 986 | {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, 987 | ] 988 | pylic = [ 989 | {file = "pylic-1.2.5-py3-none-any.whl", hash = "sha256:da31b60df9c8de582daad2d6e395d2d2080aead755f009d819b11d8295e94dd3"}, 990 | {file = "pylic-1.2.5.tar.gz", hash = "sha256:2facb4694e52c7a1286829d21f20d83c3329f6fde1836a029b44de3962c73399"}, 991 | ] 992 | pyparsing = [ 993 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 994 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 995 | ] 996 | pytest = [ 997 | {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, 998 | {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, 999 | ] 1000 | pytest-cov = [ 1001 | {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, 1002 | {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, 1003 | ] 1004 | python-dateutil = [ 1005 | {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, 1006 | {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, 1007 | ] 1008 | pytz = [ 1009 | {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, 1010 | {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, 1011 | ] 1012 | pyyaml = [ 1013 | {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, 1014 | {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, 1015 | {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, 1016 | {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, 1017 | {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, 1018 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, 1019 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, 1020 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, 1021 | {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, 1022 | {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, 1023 | {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, 1024 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, 1025 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, 1026 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, 1027 | {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, 1028 | {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, 1029 | {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, 1030 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, 1031 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, 1032 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, 1033 | {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, 1034 | {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, 1035 | {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, 1036 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, 1037 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, 1038 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, 1039 | {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, 1040 | {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, 1041 | {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, 1042 | ] 1043 | regex = [ 1044 | {file = "regex-2021.8.21-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b0c211c55d4aac4309c3209833c803fada3fc21cdf7b74abedda42a0c9dc3ce"}, 1045 | {file = "regex-2021.8.21-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d5209c3ba25864b1a57461526ebde31483db295fc6195fdfc4f8355e10f7376"}, 1046 | {file = "regex-2021.8.21-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c835c30f3af5c63a80917b72115e1defb83de99c73bc727bddd979a3b449e183"}, 1047 | {file = "regex-2021.8.21-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:615fb5a524cffc91ab4490b69e10ae76c1ccbfa3383ea2fad72e54a85c7d47dd"}, 1048 | {file = "regex-2021.8.21-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9966337353e436e6ba652814b0a957a517feb492a98b8f9d3b6ba76d22301dcc"}, 1049 | {file = "regex-2021.8.21-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a49f85f0a099a5755d0a2cc6fc337e3cb945ad6390ec892332c691ab0a045882"}, 1050 | {file = "regex-2021.8.21-cp310-cp310-win32.whl", hash = "sha256:f93a9d8804f4cec9da6c26c8cfae2c777028b4fdd9f49de0302e26e00bb86504"}, 1051 | {file = "regex-2021.8.21-cp310-cp310-win_amd64.whl", hash = "sha256:a795829dc522227265d72b25d6ee6f6d41eb2105c15912c230097c8f5bfdbcdc"}, 1052 | {file = "regex-2021.8.21-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:bca14dfcfd9aae06d7d8d7e105539bd77d39d06caaae57a1ce945670bae744e0"}, 1053 | {file = "regex-2021.8.21-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41acdd6d64cd56f857e271009966c2ffcbd07ec9149ca91f71088574eaa4278a"}, 1054 | {file = "regex-2021.8.21-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96f0c79a70642dfdf7e6a018ebcbea7ea5205e27d8e019cad442d2acfc9af267"}, 1055 | {file = "regex-2021.8.21-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:45f97ade892ace20252e5ccecdd7515c7df5feeb42c3d2a8b8c55920c3551c30"}, 1056 | {file = "regex-2021.8.21-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f9974826aeeda32a76648fc677e3125ade379869a84aa964b683984a2dea9f1"}, 1057 | {file = "regex-2021.8.21-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea9753d64cba6f226947c318a923dadaf1e21cd8db02f71652405263daa1f033"}, 1058 | {file = "regex-2021.8.21-cp36-cp36m-win32.whl", hash = "sha256:ef9326c64349e2d718373415814e754183057ebc092261387a2c2f732d9172b2"}, 1059 | {file = "regex-2021.8.21-cp36-cp36m-win_amd64.whl", hash = "sha256:6dbd51c3db300ce9d3171f4106da18fe49e7045232630fe3d4c6e37cb2b39ab9"}, 1060 | {file = "regex-2021.8.21-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a89ca4105f8099de349d139d1090bad387fe2b208b717b288699ca26f179acbe"}, 1061 | {file = "regex-2021.8.21-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6c2b1d78ceceb6741d703508cd0e9197b34f6bf6864dab30f940f8886e04ade"}, 1062 | {file = "regex-2021.8.21-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a34ba9e39f8269fd66ab4f7a802794ffea6d6ac500568ec05b327a862c21ce23"}, 1063 | {file = "regex-2021.8.21-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ecb6e7c45f9cd199c10ec35262b53b2247fb9a408803ed00ee5bb2b54aa626f5"}, 1064 | {file = "regex-2021.8.21-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:330836ad89ff0be756b58758878409f591d4737b6a8cef26a162e2a4961c3321"}, 1065 | {file = "regex-2021.8.21-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:71a904da8c9c02aee581f4452a5a988c3003207cb8033db426f29e5b2c0b7aea"}, 1066 | {file = "regex-2021.8.21-cp37-cp37m-win32.whl", hash = "sha256:b511c6009d50d5c0dd0bab85ed25bc8ad6b6f5611de3a63a59786207e82824bb"}, 1067 | {file = "regex-2021.8.21-cp37-cp37m-win_amd64.whl", hash = "sha256:93f9f720081d97acee38a411e861d4ce84cbc8ea5319bc1f8e38c972c47af49f"}, 1068 | {file = "regex-2021.8.21-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a195e26df1fbb40ebee75865f9b64ba692a5824ecb91c078cc665b01f7a9a36"}, 1069 | {file = "regex-2021.8.21-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06ba444bbf7ede3890a912bd4904bb65bf0da8f0d8808b90545481362c978642"}, 1070 | {file = "regex-2021.8.21-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b8d551f1bd60b3e1c59ff55b9e8d74607a5308f66e2916948cafd13480b44a3"}, 1071 | {file = "regex-2021.8.21-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ebbceefbffae118ab954d3cd6bf718f5790db66152f95202ebc231d58ad4e2c2"}, 1072 | {file = "regex-2021.8.21-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccd721f1d4fc42b541b633d6e339018a08dd0290dc67269df79552843a06ca92"}, 1073 | {file = "regex-2021.8.21-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ae87ab669431f611c56e581679db33b9a467f87d7bf197ac384e71e4956b4456"}, 1074 | {file = "regex-2021.8.21-cp38-cp38-win32.whl", hash = "sha256:38600fd58c2996829480de7d034fb2d3a0307110e44dae80b6b4f9b3d2eea529"}, 1075 | {file = "regex-2021.8.21-cp38-cp38-win_amd64.whl", hash = "sha256:61e734c2bcb3742c3f454dfa930ea60ea08f56fd1a0eb52d8cb189a2f6be9586"}, 1076 | {file = "regex-2021.8.21-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b091dcfee169ad8de21b61eb2c3a75f9f0f859f851f64fdaf9320759a3244239"}, 1077 | {file = "regex-2021.8.21-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:640ccca4d0a6fcc6590f005ecd7b16c3d8f5d52174e4854f96b16f34c39d6cb7"}, 1078 | {file = "regex-2021.8.21-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac95101736239260189f426b1e361dc1b704513963357dc474beb0f39f5b7759"}, 1079 | {file = "regex-2021.8.21-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b79dc2b2e313565416c1e62807c7c25c67a6ff0a0f8d83a318df464555b65948"}, 1080 | {file = "regex-2021.8.21-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b623fc429a38a881ab2d9a56ef30e8ea20c72a891c193f5ebbddc016e083ee"}, 1081 | {file = "regex-2021.8.21-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8021dee64899f993f4b5cca323aae65aabc01a546ed44356a0965e29d7893c94"}, 1082 | {file = "regex-2021.8.21-cp39-cp39-win32.whl", hash = "sha256:d6ec4ae13760ceda023b2e5ef1f9bc0b21e4b0830458db143794a117fdbdc044"}, 1083 | {file = "regex-2021.8.21-cp39-cp39-win_amd64.whl", hash = "sha256:03840a07a402576b8e3a6261f17eb88abd653ad4e18ec46ef10c9a63f8c99ebd"}, 1084 | {file = "regex-2021.8.21.tar.gz", hash = "sha256:faf08b0341828f6a29b8f7dd94d5cf8cc7c39bfc3e67b78514c54b494b66915a"}, 1085 | ] 1086 | requests = [ 1087 | {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, 1088 | {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, 1089 | ] 1090 | scikit-learn = [ 1091 | {file = "scikit-learn-0.24.2.tar.gz", hash = "sha256:d14701a12417930392cd3898e9646cf5670c190b933625ebe7511b1f7d7b8736"}, 1092 | {file = "scikit_learn-0.24.2-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:d5bf9c863ba4717b3917b5227463ee06860fc43931dc9026747de416c0a10fee"}, 1093 | {file = "scikit_learn-0.24.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5beaeb091071625e83f5905192d8aecde65ba2f26f8b6719845bbf586f7a04a1"}, 1094 | {file = "scikit_learn-0.24.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:06ffdcaaf81e2a3b1b50c3ac6842cfb13df2d8b737d61f64643ed61da7389cde"}, 1095 | {file = "scikit_learn-0.24.2-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:fec42690a2eb646b384eafb021c425fab48991587edb412d4db77acc358b27ce"}, 1096 | {file = "scikit_learn-0.24.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:5ff3e4e4cf7592d36541edec434e09fb8ab9ba6b47608c4ffe30c9038d301897"}, 1097 | {file = "scikit_learn-0.24.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:3cbd734e1aefc7c5080e6b6973fe062f97c26a1cdf1a991037ca196ce1c8f427"}, 1098 | {file = "scikit_learn-0.24.2-cp36-cp36m-win32.whl", hash = "sha256:f74429a07fedb36a03c159332b914e6de757176064f9fed94b5f79ebac07d913"}, 1099 | {file = "scikit_learn-0.24.2-cp36-cp36m-win_amd64.whl", hash = "sha256:dd968a174aa82f3341a615a033fa6a8169e9320cbb46130686562db132d7f1f0"}, 1100 | {file = "scikit_learn-0.24.2-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:49ec0b1361da328da9bb7f1a162836028e72556356adeb53342f8fae6b450d47"}, 1101 | {file = "scikit_learn-0.24.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f18c3ed484eeeaa43a0d45dc2efb4d00fc6542ccdcfa2c45d7b635096a2ae534"}, 1102 | {file = "scikit_learn-0.24.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:cdf24c1b9bbeb4936456b42ac5bd32c60bb194a344951acb6bfb0cddee5439a4"}, 1103 | {file = "scikit_learn-0.24.2-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d177fe1ff47cc235942d628d41ee5b1c6930d8f009f1a451c39b5411e8d0d4cf"}, 1104 | {file = "scikit_learn-0.24.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f3ec00f023d84526381ad0c0f2cff982852d035c921bbf8ceb994f4886c00c64"}, 1105 | {file = "scikit_learn-0.24.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:ae19ac105cf7ce8c205a46166992fdec88081d6e783ab6e38ecfbe45729f3c39"}, 1106 | {file = "scikit_learn-0.24.2-cp37-cp37m-win32.whl", hash = "sha256:f0ed4483c258fb23150e31b91ea7d25ff8495dba108aea0b0d4206a777705350"}, 1107 | {file = "scikit_learn-0.24.2-cp37-cp37m-win_amd64.whl", hash = "sha256:39b7e3b71bcb1fe46397185d6c1a5db1c441e71c23c91a31e7ad8cc3f7305f9a"}, 1108 | {file = "scikit_learn-0.24.2-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:90a297330f608adeb4d2e9786c6fda395d3150739deb3d42a86d9a4c2d15bc1d"}, 1109 | {file = "scikit_learn-0.24.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:f1d2108e770907540b5248977e4cff9ffaf0f73d0d13445ee938df06ca7579c6"}, 1110 | {file = "scikit_learn-0.24.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:1eec963fe9ffc827442c2e9333227c4d49749a44e592f305398c1db5c1563393"}, 1111 | {file = "scikit_learn-0.24.2-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:2db429090b98045d71218a9ba913cc9b3fe78e0ba0b6b647d8748bc6d5a44080"}, 1112 | {file = "scikit_learn-0.24.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:62214d2954377fcf3f31ec867dd4e436df80121e7a32947a0b3244f58f45e455"}, 1113 | {file = "scikit_learn-0.24.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8fac72b9688176922f9f54fda1ba5f7ffd28cbeb9aad282760186e8ceba9139a"}, 1114 | {file = "scikit_learn-0.24.2-cp38-cp38-win32.whl", hash = "sha256:ae426e3a52842c6b6d77d00f906b6031c8c2cfdfabd6af7511bb4bc9a68d720e"}, 1115 | {file = "scikit_learn-0.24.2-cp38-cp38-win_amd64.whl", hash = "sha256:038f4e9d6ef10e1f3fe82addc3a14735c299866eb10f2c77c090410904828312"}, 1116 | {file = "scikit_learn-0.24.2-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:48f273836e19901ba2beecd919f7b352f09310ce67c762f6e53bc6b81cacf1f0"}, 1117 | {file = "scikit_learn-0.24.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:a2a47449093dcf70babc930beba2ca0423cb7df2fa5fd76be5260703d67fa574"}, 1118 | {file = "scikit_learn-0.24.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:0e71ce9c7cbc20f6f8b860107ce15114da26e8675238b4b82b7e7cd37ca0c087"}, 1119 | {file = "scikit_learn-0.24.2-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2754c85b2287333f9719db7f23fb7e357f436deed512db3417a02bf6f2830aa5"}, 1120 | {file = "scikit_learn-0.24.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:7be1b88c23cfac46e06404582215a917017cd2edaa2e4d40abe6aaff5458f24b"}, 1121 | {file = "scikit_learn-0.24.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:4e6198675a6f9d333774671bd536668680eea78e2e81c0b19e57224f58d17f37"}, 1122 | {file = "scikit_learn-0.24.2-cp39-cp39-win32.whl", hash = "sha256:cbdb0b3db99dd1d5f69d31b4234367d55475add31df4d84a3bd690ef017b55e2"}, 1123 | {file = "scikit_learn-0.24.2-cp39-cp39-win_amd64.whl", hash = "sha256:40556bea1ef26ef54bc678d00cf138a63069144a0b5f3a436eecd8f3468b903e"}, 1124 | ] 1125 | scipy = [ 1126 | {file = "scipy-1.6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a15a1f3fc0abff33e792d6049161b7795909b40b97c6cc2934ed54384017ab76"}, 1127 | {file = "scipy-1.6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e79570979ccdc3d165456dd62041d9556fb9733b86b4b6d818af7a0afc15f092"}, 1128 | {file = "scipy-1.6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a423533c55fec61456dedee7b6ee7dce0bb6bfa395424ea374d25afa262be261"}, 1129 | {file = "scipy-1.6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:33d6b7df40d197bdd3049d64e8e680227151673465e5d85723b3b8f6b15a6ced"}, 1130 | {file = "scipy-1.6.1-cp37-cp37m-win32.whl", hash = "sha256:6725e3fbb47da428794f243864f2297462e9ee448297c93ed1dcbc44335feb78"}, 1131 | {file = "scipy-1.6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:5fa9c6530b1661f1370bcd332a1e62ca7881785cc0f80c0d559b636567fab63c"}, 1132 | {file = "scipy-1.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bd50daf727f7c195e26f27467c85ce653d41df4358a25b32434a50d8870fc519"}, 1133 | {file = "scipy-1.6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:f46dd15335e8a320b0fb4685f58b7471702234cba8bb3442b69a3e1dc329c345"}, 1134 | {file = "scipy-1.6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0e5b0ccf63155d90da576edd2768b66fb276446c371b73841e3503be1d63fb5d"}, 1135 | {file = "scipy-1.6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2481efbb3740977e3c831edfd0bd9867be26387cacf24eb5e366a6a374d3d00d"}, 1136 | {file = "scipy-1.6.1-cp38-cp38-win32.whl", hash = "sha256:68cb4c424112cd4be886b4d979c5497fba190714085f46b8ae67a5e4416c32b4"}, 1137 | {file = "scipy-1.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:5f331eeed0297232d2e6eea51b54e8278ed8bb10b099f69c44e2558c090d06bf"}, 1138 | {file = "scipy-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0c8a51d33556bf70367452d4d601d1742c0e806cd0194785914daf19775f0e67"}, 1139 | {file = "scipy-1.6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:83bf7c16245c15bc58ee76c5418e46ea1811edcc2e2b03041b804e46084ab627"}, 1140 | {file = "scipy-1.6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:794e768cc5f779736593046c9714e0f3a5940bc6dcc1dba885ad64cbfb28e9f0"}, 1141 | {file = "scipy-1.6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5da5471aed911fe7e52b86bf9ea32fb55ae93e2f0fac66c32e58897cfb02fa07"}, 1142 | {file = "scipy-1.6.1-cp39-cp39-win32.whl", hash = "sha256:8e403a337749ed40af60e537cc4d4c03febddcc56cd26e774c9b1b600a70d3e4"}, 1143 | {file = "scipy-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a5193a098ae9f29af283dcf0041f762601faf2e595c0db1da929875b7570353f"}, 1144 | {file = "scipy-1.6.1.tar.gz", hash = "sha256:c4fceb864890b6168e79b0e714c585dbe2fd4222768ee90bc1aa0f8218691b11"}, 1145 | ] 1146 | six = [ 1147 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 1148 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 1149 | ] 1150 | smmap = [ 1151 | {file = "smmap-4.0.0-py2.py3-none-any.whl", hash = "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"}, 1152 | {file = "smmap-4.0.0.tar.gz", hash = "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182"}, 1153 | ] 1154 | stevedore = [ 1155 | {file = "stevedore-3.4.0-py3-none-any.whl", hash = "sha256:920ce6259f0b2498aaa4545989536a27e4e4607b8318802d7ddc3a533d3d069e"}, 1156 | {file = "stevedore-3.4.0.tar.gz", hash = "sha256:59b58edb7f57b11897f150475e7bc0c39c5381f0b8e3fa9f5c20ce6c89ec4aa1"}, 1157 | ] 1158 | threadpoolctl = [ 1159 | {file = "threadpoolctl-2.2.0-py3-none-any.whl", hash = "sha256:e5a995e3ffae202758fa8a90082e35783b9370699627ae2733cd1c3a73553616"}, 1160 | {file = "threadpoolctl-2.2.0.tar.gz", hash = "sha256:86d4b6801456d780e94681d155779058759eaef3c3564758b17b6c99db5f81cb"}, 1161 | ] 1162 | toml = [ 1163 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 1164 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 1165 | ] 1166 | tomli = [ 1167 | {file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"}, 1168 | {file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"}, 1169 | ] 1170 | typed-ast = [ 1171 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, 1172 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, 1173 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, 1174 | {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, 1175 | {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, 1176 | {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, 1177 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, 1178 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, 1179 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, 1180 | {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, 1181 | {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, 1182 | {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, 1183 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, 1184 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, 1185 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, 1186 | {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, 1187 | {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, 1188 | {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, 1189 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, 1190 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, 1191 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, 1192 | {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, 1193 | {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, 1194 | {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, 1195 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, 1196 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, 1197 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, 1198 | {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, 1199 | {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, 1200 | {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, 1201 | ] 1202 | typing-extensions = [ 1203 | {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, 1204 | {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, 1205 | {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, 1206 | ] 1207 | urllib3 = [ 1208 | {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, 1209 | {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, 1210 | ] 1211 | virtualenv = [ 1212 | {file = "virtualenv-20.7.2-py2.py3-none-any.whl", hash = "sha256:e4670891b3a03eb071748c569a87cceaefbf643c5bac46d996c5a45c34aa0f06"}, 1213 | {file = "virtualenv-20.7.2.tar.gz", hash = "sha256:9ef4e8ee4710826e98ff3075c9a4739e2cb1040de6a2a8d35db0055840dc96a0"}, 1214 | ] 1215 | vulture = [ 1216 | {file = "vulture-2.3-py2.py3-none-any.whl", hash = "sha256:f39de5e6f1df1f70c3b50da54f1c8d494159e9ca3d01a9b89eac929600591703"}, 1217 | {file = "vulture-2.3.tar.gz", hash = "sha256:03d5a62bcbe9ceb9a9b0575f42d71a2d414070229f2e6f95fa6e7c71aaaed967"}, 1218 | ] 1219 | whois = [ 1220 | {file = "whois-0.9.13.tar.gz", hash = "sha256:478a4f10673412d774078f74302b2b62cbab20fbda9216918815687582a0c68d"}, 1221 | ] 1222 | zipp = [ 1223 | {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, 1224 | {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, 1225 | ] 1226 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "certitude" 3 | version = "1.0.0" 4 | description = "Determine if a URL may be malicious" 5 | authors = ["Paul Wiper ", "Marie Beth van Egmond =1.0.0"] 33 | build-backend = "poetry.core.masonry.api" 34 | 35 | [tool.pylic] 36 | safe_licenses = [ 37 | "Apache Software License", 38 | "MIT License", 39 | "BSD License", 40 | "new BSD", 41 | "MPL-2.0", 42 | "Mozilla Public License 2.0 (MPL 2.0)", 43 | ] 44 | -------------------------------------------------------------------------------- /tbump.toml: -------------------------------------------------------------------------------- 1 | [version] 2 | current = "1.0.0" 3 | 4 | regex = ''' 5 | (?P\d+) 6 | \. 7 | (?P\d+) 8 | \. 9 | (?P\d+) 10 | ''' 11 | 12 | [git] 13 | message_template = "Bump to {new_version}" 14 | tag_template = "v{new_version}" 15 | 16 | [[file]] 17 | src = "pyproject.toml" 18 | 19 | [[file]] 20 | src = "certitude/__init__.py" 21 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/COSSAS/Certitude/0d04a82d56c1ce8e1b997c0f15ca7031058767de/tests/__init__.py -------------------------------------------------------------------------------- /tests/data/testset.csv: -------------------------------------------------------------------------------- 1 | ,url 2 | 1,https://www.tno.nl/ 3 | 2,https://www.tno.nl/automated-security 4 | 3,https://www.tno.nl/nl/aandachtsgebieden/defensie-veiligheid/roadmaps/operations-human-factors/operationele-analyse-in-het-veld-bij-militaire-operaties/ 5 | 4,http://www.cardispeed.com/motors-eby/ 6 | 5,https://letsencrypt.org/ 7 | 6,https://stackoverflow.com/ 8 | 7,https://linkedin.com 9 | 8,https://www.ncl.ac.uk/computing/ 10 | 9,https://example.com/foo?bar 11 | 10,https://example.com/foo/foo/foo?bar/bar/bar 12 | 11,https://example.com/?bar 13 | 12,https://example.com/?@bar._=???/1: 14 | 13,https://example.com/?bar1=a&bar2=b 15 | 14,https://wipersnonexistingsite.com 16 | -------------------------------------------------------------------------------- /tests/data/testset_labeled.csv: -------------------------------------------------------------------------------- 1 | ,url,label 2 | 1,https://www.tno.nl/,0 3 | 2,https://www.tno.nl/automated-security,1 4 | 3,https://www.tno.nl/nl/aandachtsgebieden/defensie-veiligheid/roadmaps/operations-human-factors/operationele-analyse-in-het-veld-bij-militaire-operaties/,1 5 | 4,http://www.cardispeed.com/motors-eby/,0 6 | 5,https://letsencrypt.org/,0 7 | 6,https://stackoverflow.com/,0 8 | 7,https://linkedin.com,0 9 | 8,https://www.ncl.ac.uk/computing/,0 10 | 9,https://example.com/foo?bar,0 11 | 10,https://example.com/foo/foo/foo?bar/bar/bar,0 12 | 11,https://example.com/?bar,0 13 | 12,https://example.com/?@bar._=???/1:,0 14 | 13,https://example.com/?bar1=a&bar2=b,0 15 | 14,https://wipersnonexistingsite.com,1 16 | -------------------------------------------------------------------------------- /tests/test_model.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | from pathlib import Path 4 | 5 | from certitude.core.data_model import create_features_dataframe, train_model 6 | from certitude.utils.config import config 7 | 8 | config = config() 9 | 10 | 11 | def test_train() -> None: 12 | dirname = os.path.dirname(__file__) 13 | dataset = create_features_dataframe( 14 | Path(os.path.join(dirname, "data/testset_labeled.csv")), labeled=True 15 | ) 16 | 17 | temp_model_file = tempfile.NamedTemporaryFile() 18 | 19 | train_model(dataset, config["features"], Path(temp_model_file.name)) 20 | --------------------------------------------------------------------------------