├── .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 |
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 |
--------------------------------------------------------------------------------