diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 02c5f5d..f4050da 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,11 +38,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -56,7 +56,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -69,6 +69,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 2466e15..8e155b4 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -11,17 +11,18 @@ jobs: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - python-version: [3.7, 3.8, 3.9, '3.10'] + python-version: [3.8, 3.9, '3.10', '3.11', '3.12'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - name: Set up Python ${{matrix.python-version}} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{matrix.python-version}} @@ -33,7 +34,12 @@ jobs: - name: Test with nosetests run: | poetry run pytest --cov=pymisp tests/test_*.py - poetry run mypy tests/testlive_comprehensive.py tests/test_mispevent.py tests/testlive_sync.py pymisp + poetry run mypy . + + - name: Test with nosetests with orjson + run: | + pip3 install orjson + poetry run pytest --cov=pymisp tests/test_*.py - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 diff --git a/.readthedocs.yml b/.readthedocs.yml index eeb1d06..6df72a3 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,14 +1,14 @@ version: 2 +build: + os: "ubuntu-22.04" + tools: + python: "3" python: - version: 3.7 install: - method: pip path: . extra_requirements: - docs -build: - image: latest - formats: all diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 235a1b4..acf26d2 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -2,6 +2,866 @@ Changelog ========= +v2.4.194 (2024-06-21) +--------------------- + +Changes +~~~~~~~ +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Make a response in the tests a MISPUser obj. [Raphaël Vinot] +- Tests failing du to missing error. [Raphaël Vinot] + + +v2.4.193 (2024-06-06) +--------------------- + +New +~~~ +- [analyst-data] Added initial support of analyst data concept and + functions - WiP. [Sami Mokaddem] + +Changes +~~~~~~~ +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- A bit more refactoring. [Raphaël Vinot] +- Use from_dict in the mixin to initialize the objects. [Raphaël Vinot] +- [analyst-data] Added improvements, API endpoints and tests. [Sami + Mokaddem] +- [analyst-data] Make sure to include note_type_name. [Sami Mokaddem] +- Make mypy happy, change inheritance. [Raphaël Vinot] +- Allow orgc context for search_galaxy_clusters. [Jeroen Pinoy] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- [analyst-data] Continued implementation of analyst-data support. [Sami + Mokaddem] +- Allow orgc context for search_galaxy_clusters. [Jeroen Pinoy] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] + +Fix +~~~ +- Get the tests to pass. [Raphaël Vinot] +- Properly load AnalystData from dict. [Raphaël Vinot] +- More changes to get the tests to pass. [Raphaël Vinot] +- [event-report] Make sure to generate an UUID. [Sami Mokaddem] +- Pass kwargs to abstract. [Raphaël Vinot] + +Other +~~~~~ +- Chg; Bump changelog. [Raphaël Vinot] +- Chg; Bump version. [Raphaël Vinot] +- Add test case. [Vincenzo] +- Add attach galaxy cluster method. [Vincenzo] + + +v2.4.190 (2024-04-18) +--------------------- + +Changes +~~~~~~~ +- Bump object templates. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump version, deps. [Raphaël Vinot] +- Bump deps, require python 3.9+ for doc. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- [data] describeTypes file updated. [Alexandre Dulaunoy] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- [internal] Correct way to convert bytes to string if orjson exists. + [Jakub Onderka] + + +v2.4.188 (2024-03-22) +--------------------- + +New +~~~ +- Support X-MISP-AUTH Header. [Raphaël Vinot] + + Also, improve HTTP headers init + + Fix #1179 + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump version, templates. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Strip API key before setting it. [Raphaël Vinot] +- Python 3.8 support & typing. [Raphaël Vinot] +- Typing for Python < 3.10. [Raphaël Vinot] +- Avoid issue when payload ist a list. [Raphaël Vinot] + + +v2.4.187 (2024-03-07) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump templates, version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump extract-msg. [Raphaël Vinot] + + +v2.4.186 (2024-02-27) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Correct FileObject import. [Johannes Bader] + + The FileObject import has been moved outside the try-except-block + related to lief, as the import is needed regardless whether lief + is available or not. +- Disable WL when calling the disable method, not toggle. [Raphaël + Vinot] + + Fix #1159 + +Other +~~~~~ +- Build(deps): bump urllib3 from 2.2.0 to 2.2.1. [dependabot[bot]] + + Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.2.0 to 2.2.1. + - [Release notes](https://github.com/urllib3/urllib3/releases) + - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) + - [Commits](https://github.com/urllib3/urllib3/compare/2.2.0...2.2.1) + + --- + updated-dependencies: + - dependency-name: urllib3 + dependency-type: direct:production + update-type: version-update:semver-patch + ... + + +v2.4.185 (2024-02-16) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump deps, version. [Raphaël Vinot] + + +v2.4.184.3 (2024-02-12) +----------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Properly get body from message, without headers. [Raphaël Vinot] +- Remove from __all__ entries that shouldn't be there. [Raphaël Vinot] + +Other +~~~~~ +- Build(deps-dev): bump jupyter-lsp from 2.2.1 to 2.2.2. + [dependabot[bot]] + + Bumps [jupyter-lsp](https://github.com/jupyter-lsp/jupyterlab-lsp) from 2.2.1 to 2.2.2. + - [Release notes](https://github.com/jupyter-lsp/jupyterlab-lsp/releases) + - [Changelog](https://github.com/jupyter-lsp/jupyterlab-lsp/blob/main/CHANGELOG.md) + - [Commits](https://github.com/jupyter-lsp/jupyterlab-lsp/commits) + + --- + updated-dependencies: + - dependency-name: jupyter-lsp + dependency-type: indirect + ... + + +v2.4.184.2 (2024-02-06) +----------------------- + +Changes +~~~~~~~ +- Add changelog. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Re-add ExpandedPyMISP, with a warning. [Raphaël Vinot] + +Fix +~~~ +- Do not throw a warning every time one import pymisp... [Raphaël Vinot] + +Other +~~~~~ +- Build(deps): bump codecov/codecov-action from 3 to 4. + [dependabot[bot]] + + Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4. + - [Release notes](https://github.com/codecov/codecov-action/releases) + - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) + - [Commits](https://github.com/codecov/codecov-action/compare/v3...v4) + + --- + updated-dependencies: + - dependency-name: codecov/codecov-action + dependency-type: direct:production + update-type: version-update:semver-major + ... + + +v2.4.184.1 (2024-02-06) +----------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Re-add ExpandedPyMISP, with a warning. [Raphaël Vinot] + + +v2.4.184 (2024-02-02) +--------------------- + +New +~~~ +- Enable support for python 3.12. [Raphaël Vinot] +- Relationship_type in tag. [Raphaël Vinot] + + Fix https://github.com/MISP/MISP/issues/9483 +- [internal] Add support for orjson. [Jakub Onderka] + + orjson is much faster library for decoding and encoding JSON formats + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump deps, version, objects. [Raphaël Vinot] +- Remove IntEnum. [Raphaël Vinot] +- Add even more debug for gha. [Raphaël Vinot] +- Add some debug for gha. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Add more strict typing, not done yet. [Raphaël Vinot] +- Add a bunch more typing. [Raphaël Vinot] +- Use typing info of lief. [Raphaël Vinot] +- First batch of changes for strict typing. [Raphaël Vinot] +- Update typing to please lief. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- [internal] Simplify code. [Jakub Onderka] +- [internal] User faster method to convert bytes to str. [Jakub Onderka] +- New annotations in tests. [Raphaël Vinot] +- Initial changes to use new annotations. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps, try to install with python 3.12. [Raphaël Vinot] +- Make the publish_timestamp a string, as per specs. [Raphaël Vinot] +- [internal] Update poetry.lock. [Jakub Onderka] + +Fix +~~~ +- Revert typing changes. [Raphaël Vinot] +- More responses athat are lists. [Raphaël Vinot] +- Another call that cn be a list or a dict. [Raphaël Vinot] +- Do not cast enum. [Raphaël Vinot] +- More fixes to support responses from MISP. [Raphaël Vinot] +- Handle list responses properly. [Raphaël Vinot] +- Import FileObject as needed. [Raphaël Vinot] +- Also skip docs from mypy. [Raphaël Vinot] +- Run mypy on what I want. [Raphaël Vinot] +- Compatibility with python 3.8. [Raphaël Vinot] +- Python < 3.10 support on typing, for good. [Raphaël Vinot] +- Python < 3.10 support on typing. [Raphaël Vinot] +- Rollback tests on python 3.12 as lief is not supported yet. [Raphaël + Vinot] +- Add missing wheel. [Raphaël Vinot] +- Make publish_timestamp a string in tests. [Raphaël Vinot] +- [internal] README typos. [Jakub Onderka] + +Other +~~~~~ +- Revert "fix: More responses athat are lists" [Raphaël Vinot] + + This reverts commit 709a10c64c0513b515f25c3ecfb9eb577b55084b. +- Build(deps): bump jinja2 from 3.1.2 to 3.1.3. [dependabot[bot]] + + Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.2 to 3.1.3. + - [Release notes](https://github.com/pallets/jinja/releases) + - [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst) + - [Commits](https://github.com/pallets/jinja/compare/3.1.2...3.1.3) + + --- + updated-dependencies: + - dependency-name: jinja2 + dependency-type: indirect + ... + + +v2.4.183 (2024-01-04) +--------------------- + +New +~~~ +- Documentation to install PyMISP on offline machine. [Raphaël Vinot] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Remove jsonschema from dependencies. [Raphaël Vinot] +- Encrypt malicious js. [Raphaël Vinot] + +Other +~~~~~ +- Fix api ssl verify typing. [Steven] +- Add HTTPS Adapter. [Steven] + + Add the ability to provide a custom HTTPS adapter to the PyMISP class. With M2Crypto and m2requests, this can enable mutual TLS with hardware tokens. + + +v2.4.182 (2023-12-14) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Avoid exception when the malware file name contains a "|" [Raphaël + Vinot] + +Other +~~~~~ +- Build(deps): bump github/codeql-action from 2 to 3. [dependabot[bot]] + + Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3. + - [Release notes](https://github.com/github/codeql-action/releases) + - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) + - [Commits](https://github.com/github/codeql-action/compare/v2...v3) + + --- + updated-dependencies: + - dependency-name: github/codeql-action + dependency-type: direct:production + update-type: version-update:semver-major + ... +- Build(deps): bump actions/setup-python from 4 to 5. [dependabot[bot]] + + Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. + - [Release notes](https://github.com/actions/setup-python/releases) + - [Commits](https://github.com/actions/setup-python/compare/v4...v5) + + --- + updated-dependencies: + - dependency-name: actions/setup-python + dependency-type: direct:production + update-type: version-update:semver-major + ... + + +v2.4.179 (2023-11-23) +--------------------- + +Changes +~~~~~~~ +- Bump version, changelog. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- [misp-objects] Bumped latest version. [Christian Studer] + +Fix +~~~ +- Eml and msg are in sync again. [Raphaël Vinot] +- Update calls to getStringStream to the public method. [Raphaël Vinot] +- Avoid confusing error when an auth key is limited to an IP. [Raphaël + Vinot] + + Fix #1099 + + +v2.4.178 (2023-10-24) +--------------------- + +New +~~~ +- Run tests on python 3.12 too. [Raphaël Vinot] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version, make __version__ dynamic. [Raphaël Vinot] +- Bump deps, allow older jsonschema for compatibility. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Make mypy happy. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Disable search logs tests for now. [Raphaël Vinot] +- Disable fastmode, reenable fetching files. [Raphaël Vinot] +- Try to speedup tests by not importing galaxies, taxos, ... [Raphaël + Vinot] +- Do not clone repo from test. [Raphaël Vinot] + +Fix +~~~ +- Make other fieldnames in CSV also valid... [Raphaël Vinot] +- Make fieldnames actually valid. [Raphaël Vinot] +- Remove CI for python 3.12, waiting for pydeep wheels. [Raphaël Vinot] +- Allow object-relation names with uppercase characters defined in the + templates. [Raphaël Vinot] +- Check if path exists in tests. [Raphaël Vinot] + +Other +~~~~~ +- Ch: Bump deps. [Raphaël Vinot] + + +v2.4.176 (2023-09-15) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version, deps. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Bump deps, objects. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Avoid exception when data is an empty iterator. [Raphaël Vinot] + + Fix #1053 + +Other +~~~~~ +- Revert "build(deps): bump codecov/codecov-action from 3 to 4" [Raphaël + Vinot] + + This reverts commit b7bb6b74317b70613ed42ea234eaafb00da6e5c6. +- Build(deps): bump codecov/codecov-action from 3 to 4. + [dependabot[bot]] + + Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4. + - [Release notes](https://github.com/codecov/codecov-action/releases) + - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) + - [Commits](https://github.com/codecov/codecov-action/compare/v3...v4) + + --- + updated-dependencies: + - dependency-name: codecov/codecov-action + dependency-type: direct:production + update-type: version-update:semver-major + ... +- Build(deps): bump actions/checkout from 3 to 4. [dependabot[bot]] + + Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. + - [Release notes](https://github.com/actions/checkout/releases) + - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) + - [Commits](https://github.com/actions/checkout/compare/v3...v4) + + --- + updated-dependencies: + - dependency-name: actions/checkout + dependency-type: direct:production + update-type: version-update:semver-major + ... + + +v2.4.175 (2023-08-23) +--------------------- + +Changes +~~~~~~~ +- Bump objects, missed that. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps, readthedocs config. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Update Sharing group info from full object. [Raphaël Vinot] + + Fix #1049 +- Changes in msg-extract strip a character. [Raphaël Vinot] + + +v2.4.174 (2023-07-31) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version, templates. [Raphaël Vinot] +- Bump deps, fix code accordingly. [Raphaël Vinot] + +Fix +~~~ +- Push code changes related to deps upgrade... [Raphaël Vinot] + +Other +~~~~~ +- Git: Bump deps. [Raphaël Vinot] + + +v2.4.173 (2023-07-10) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Maybe fixing a CakePHP issue. [Raphaël Vinot] + + Maybe fixing #1014 +- Use proper endpoint to unpublish event. [Raphaël Vinot] + + Fix #1012 + +Other +~~~~~ +- Feat: introduce setter for galaxies. [Sura De Silva] + + +v2.4.172 (2023-06-08) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- [misp-objects] Bumped latest version with updated templates. + [Christian Studer] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Proper changelog bump. [Raphaël Vinot] +- Properly bump version. [Raphaël Vinot] + +Other +~~~~~ +- Build(deps-dev): bump jupyterlab from 3.6.3 to 4.0.0. + [dependabot[bot]] + + Bumps [jupyterlab](https://github.com/jupyterlab/jupyterlab) from 3.6.3 to 4.0.0. + - [Release notes](https://github.com/jupyterlab/jupyterlab/releases) + - [Changelog](https://github.com/jupyterlab/jupyterlab/blob/master/CHANGELOG.md) + - [Commits](https://github.com/jupyterlab/jupyterlab/compare/@jupyterlab/vdom@3.6.3...@jupyterlab/lsp@4.0.0) + + --- + updated-dependencies: + - dependency-name: jupyterlab + dependency-type: direct:development + update-type: version-update:semver-major + ... +- Update settings.default.py - tags not tag. [Alexandre Dulaunoy] + + tags is now an array + + +v2.4.171 (2023-05-16) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump deps, object templates. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Remove old setup files, bump deps. [Raphaël Vinot] + +Fix +~~~ +- Extra print breaking the CI on MISP side. [Raphaël Vinot] +- Properly use lief on a file. [Raphaël Vinot] + +Other +~~~~~ +- Allow search by 'event_tags' and improve the handling of galaxy + clusters. [Stefano Ortolani] + + Changes: + - Add 'event_tags' parameter when searching for events + - Add new method to search for galaxies by value + - Add new parameter to fetch cluster information when retrieving clusters + - Add new parameter to hard-delete object references +- Using underscore name 'description_file' in setup.cfg. [Erhan] + + Usage of dash-separated 'description-file' will not be supported in future versions. Please use the underscore name 'description_file' instead. By 2023-Sep-26, you need to update your project and remove deprecated calls or your builds will no longer be supported. + + See https://setuptools.pypa.io/en/latest/userguide/declarative_config.html for details. + + +v2.4.170.2 (2023-05-04) +----------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + + +v2.4.170.1 (2023-04-19) +----------------------- + +Changes +~~~~~~~ +- Disable fail fast in GHA. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Update lief code to v0.13. [Raphaël Vinot] + + +v2.4.170 (2023-04-12) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Other +~~~~~ +- Add: support breakOnDuplicate option for attributes:add() [Luciano + Righetti] +- Update reportlab_generator.py. [CarlosLoureiro] + + +v2.4.169.3 (2023-03-27) +----------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump deps, version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Invalid check if taxo is enabled. [Raphaël Vinot] + + +v2.4.169.2 (2023-03-17) +----------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Include event reports by default in feed. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Use proper parameter to trigger the request in search_galaxy_clusters. + [Raphaël Vinot] +- Use POST in search galaxy cluster. [Raphaël Vinot] + +Other +~~~~~ +- Rename include_event_reports kwarg to with_event_reports, in-line with + other kwarg naming. [UFOSmuggler] +- Add kwarg to allow the inclusion of event reports into to_feed(), + honour with_distribution and valid_distributions kwargs. [UFOSmuggler] + + +v2.4.169.1 (2023-03-14) +----------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Add greynoise-ip object. [Raphaël Vinot] + + Fix #951 + + +v2.4.169 (2023-03-10) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump templates. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Add local key in MISPTag. [Raphaël Vinot] + + Related #947 +- Use pytest for the tests. [Raphaël Vinot] + + +v2.4.168.1 (2023-02-28) +----------------------- + +New +~~~ +- [doc] added the Jupyter notebook used in a.7-rest-api-extensive- + restsearch. [Alexandre Dulaunoy] + +Changes +~~~~~~~ +- Bump changelog, version. [Raphaël Vinot] +- Bump templates, again. [Raphaël Vinot] +- Bump templates. [Raphaël Vinot] +- Bump deps, templates. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Properly handle missing parameter in CSV importer. [Raphaël Vinot] + + Fix #931 +- Undefined variable in event delegation. [Raphaël Vinot] +- Remove reference to old pydeep. [Raphaël Vinot] + + Fix #914 + + +v2.4.168 (2023-01-23) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + + +v2.4.167.2 (2023-01-17) +----------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump deps, version. [Raphaël Vinot] + +Fix +~~~ +- Set relationship_type default in MISPTag to empty string. [Raphaël + Vinot] +- Another typo in readme. [Raphaël Vinot] +- Typo in readme. [Raphaël Vinot] + + +v2.4.167.1 (2023-01-16) +----------------------- + +New +~~~ +- Add relationship_type in Tag entries for feeds. [Raphaël Vinot] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump requests. [Raphaël Vinot] +- Bump pyzmq. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump python version used by read the docs. [Raphaël Vinot] +- Bump warning to inform user that python 3.10 wil be required in 12 + months. [Raphaël Vinot] +- Bump minimal PyMISP version to 3.8. [Raphaël Vinot] +- Re-bump changelog. [Raphaël Vinot] + +Fix +~~~ +- Update whl files. [Raphaël Vinot] +- Nvm, readthedocs requires python 3.8 at most. [Raphaël Vinot] + + +v2.4.167 (2022-12-22) +--------------------- + +Changes +~~~~~~~ +- Bump objects. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Bump dependencies, move to poetry 1.3. [Raphaël Vinot] +- Bump certifi. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Re-order classes. [Raphaël Vinot] + +Other +~~~~~ +- Creation fo "add_attributes_from_csv.py" [Julien Mongenet] + + The file aims to ingest a formated CSV file containing attributes for MISP ingestion. +- Graceful handling of tagging when name attribute is missing. [Sura De + Silva] +- Add: Galaxy test sample. [Christian Studer] +- Add: Added very straight forward tests to make sure the galaxy + clusters are properly defined. [Christian Studer] +- Add: Added the `Galaxy` field to MISPAttribute using the MISPGalaxy + class. [Christian Studer] + + - Including an `add_galaxy` method similar to the + one used for events + - `attribute.galaxies` gives the list of attached + galaxy clusters + + v2.4.166 (2022-11-28) --------------------- @@ -13,6 +873,7 @@ New Changes ~~~~~~~ +- Re-bump changelog. [Raphaël Vinot] - Bump changelog. [Raphaël Vinot] - Bump deps, version. [Raphaël Vinot] - [types] added azure-application-id. [iglocska] @@ -4622,5 +5483,3 @@ v1.1.2 (2015-08-05) - Json export is not supported everywhere. [Raphaël Vinot] - Some testing. [Raphaël Vinot] - Initial commit. [Raphaël Vinot] - - diff --git a/README.md b/README.md index 970ef6e..89da00f 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -**IMPORTANT NOTE**: This library will require **at least** python 3.8 starting the 1st of January 2022. If you have legacy versions of python, please use the latest PyMISP version that will be released in December 2021, and consider updating your system(s). Anything released within the last 2 years will do, starting with Ubuntu 20.04. +**IMPORTANT NOTE**: This library will require **at least** Python 3.10 starting the 1st of January 2024. If you have legacy versions of python, please use the latest PyMISP version that will be released in December 2023, and consider updating your system(s). Anything released within the last 2 years will do, starting with Ubuntu 22.04. # PyMISP - Python Library to access MISP [![Documentation Status](https://readthedocs.org/projects/pymisp/badge/?version=latest)](http://pymisp.readthedocs.io/?badge=latest) [![Coverage Status](https://coveralls.io/repos/github/MISP/PyMISP/badge.svg?branch=main)](https://coveralls.io/github/MISP/PyMISP?branch=main) -[![Python 3.6](https://img.shields.io/badge/python-3.6+-blue.svg)](https://www.python.org/downloads/release/python-360/) +[![Python 3.8](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/release/python-380/) [![PyPi version](https://img.shields.io/pypi/v/pymisp.svg)](https://pypi.python.org/pypi/pymisp/) [![Number of PyPI downloads](https://img.shields.io/pypi/dm/pymisp.svg)](https://pypi.python.org/pypi/pymisp/) @@ -33,7 +33,7 @@ And there are a few optional dependencies: * email: to generate MISP Email objects * brotli: to use the brotli compression when interacting with a MISP instance -Example: +Example: ``` pip3 install pymisp[virustotal,email] @@ -46,13 +46,13 @@ pip3 install pymisp[virustotal,email] ``` git clone https://github.com/MISP/PyMISP.git && cd PyMISP git submodule update --init -poetry install -E fileobjects -E openioc -E virustotal -E docs -E pdfexport +poetry install -E fileobjects -E openioc -E virustotal -E docs -E pdfexport -E email ``` ### Running the tests ```bash -poetry run nosetests-3.4 --with-coverage --cover-package=pymisp,tests --cover-tests tests/test_*.py +poetry run pytest --cov=pymisp tests/test_*.py ``` If you have a MISP instance to test against, you can also run the live ones: @@ -60,7 +60,7 @@ If you have a MISP instance to test against, you can also run the live ones: **Note**: You need to update the key in `tests/testlive_comprehensive.py` to the automation key of your admin account. ```bash -poetry run nosetests-3.4 --with-coverage --cover-package=pymisp,tests --cover-tests tests/testlive_comprehensive.py +poetry run pytest --cov=pymisp tests/testlive_comprehensive.py ``` ## Samples and how to use PyMISP @@ -124,8 +124,7 @@ logging.basicConfig(level=logging.DEBUG, filename="debug.log", filemode='w', for ```bash # From poetry -nosetests-3.4 -s --with-coverage --cover-package=pymisp,tests --cover-tests tests/testlive_comprehensive.py:TestComprehensive.[test_name] - +pytest --cov=pymisp tests/test_*.py tests/testlive_comprehensive.py:TestComprehensive.[test_name] ``` ## Documentation @@ -159,6 +158,35 @@ Your new MISPObject generator must generate attributes and add them as class pro When the object is sent to MISP, all the class properties will be exported to the JSON export. +## Installing PyMISP on a machine with no internet access + +This is done using poetry and you need to have this repository cloned on your machine. +The commands below have to be run from inside the cloned directory. + + +1. From a machine with access to the internet, get the dependencies: + +```bash +mkdir offline +poetry export --all-extras > offline/requirements.txt +poetry run pip download -r offline/requirements.txt -d offline/packages/ +``` + +2. Prepare the PyMISP Package + +```bash +poetry build +mv dist/*.whl offline/packages/ +``` + +3. Copy the content of `offline/packages/` to the machine with no internet access. + +4. Install the packages: + +```bash +python -m pip install --no-index --no-deps packages/*.whl +``` + # License PyMISP is distributed under an [open source license](./LICENSE). A simplified 2-BSD license. diff --git a/docs/tutorial/a.7-rest-api-extensive-restsearch.ipynb b/docs/tutorial/a.7-rest-api-extensive-restsearch.ipynb new file mode 100644 index 0000000..7e0fbc4 --- /dev/null +++ b/docs/tutorial/a.7-rest-api-extensive-restsearch.ipynb @@ -0,0 +1,1614 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Extracting data from MISP using PyMISP" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Recovering the API KEY" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- Go to `Global Actions` then `My Profile`\n", + "- Access the `/users/view/me` URL" + ] + }, + { + "cell_type": "code", + "execution_count": 491, + "metadata": {}, + "outputs": [], + "source": [ + "from pymisp import PyMISP\n", + "import urllib3\n", + "urllib3.disable_warnings()\n", + "\n", + "misp_url = 'https://localhost:8443/'\n", + "misp_key = 'GqfuZo444EFlylND0XaKZsEXgWgkPgguUZ6KVRuq'\n", + "# Should PyMISP verify the MISP certificate\n", + "misp_verifycert = False\n", + "\n", + "misp = PyMISP(misp_url, misp_key, misp_verifycert)" + ] + }, + { + "cell_type": "code", + "execution_count": 492, + "metadata": {}, + "outputs": [], + "source": [ + "import datetime\n", + "from pprint import pprint\n", + "import base64\n", + "import subprocess" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Retreiving an Event" + ] + }, + { + "cell_type": "code", + "execution_count": 493, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "r1 = misp.get_event('7907c4a9-a15c-4c60-a1b4-1d214cf8cf41', pythonify=True)\n", + "print(r1)\n", + "r2 = misp.get_event(2, pythonify=False)\n", + "print(type(r2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Searching the Event index" + ] + }, + { + "cell_type": "code", + "execution_count": 494, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "7907c4a9-a15c-4c60-a1b4-1d214cf8cf41\n" + ] + } + ], + "source": [ + "r = misp.search_index(pythonify=True)\n", + "print(r[1].uuid)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Only published Events" + ] + }, + { + "cell_type": "code", + "execution_count": 495, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[ ip-port), ]\n", + "List of tags: 18\n", + "\tThird Attribute [, ]\n" + ] + } + ], + "source": [ + "r1 = misp.search(controller='attributes', tags='tlp:red', pythonify=True)\n", + "print('Simple tag:', len(r1))\n", + "print('\\tFirst Attribute', r1[0].Tag)\n", + "\n", + "r2 = misp.search(controller='attributes', tags=['PAP:RED', 'tlp:red'], pythonify=True)\n", + "print('List of tags:', len(r2))\n", + "print('\\tThird Attribute', r2[2].Tag)" + ] + }, + { + "cell_type": "code", + "execution_count": 502, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Wildcard: 22\n", + "\tTags of all Attributes: [[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []]\n", + "\n", + "Open question: Why do we have Attributes despite them not having the correct tag attached?\n", + "\n" + ] + } + ], + "source": [ + "r3 = misp.search(controller='attributes', tags=['misp-galaxy:target-information=%'], pythonify=True)\n", + "print('Wildcard:', len(r3))\n", + "print('\\tTags of all Attributes:', [attr.Tag for attr in r3])\n", + "print()\n", + "print(base64.b64decode('T3BlbiBxdWVzdGlvbjogV2h5IGRvIHdlIGhhdmUgQXR0cmlidXRlcyBkZXNwaXRlIHRoZW0gbm90IGhhdmluZyB0aGUgY29ycmVjdCB0YWcgYXR0YWNoZWQ/Cg==').decode())" + ] + }, + { + "cell_type": "code", + "execution_count": 503, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "All unique Event tags: {'misp-galaxy:target-information=\"Canada\"', 'misp-galaxy:target-information=\"China\"', 'misp-galaxy:target-information=\"Germany\"', 'misp-galaxy:target-information=\"Luxembourg\"'}\n" + ] + } + ], + "source": [ + "allEventTags = [\n", + " [tag.name for tag in misp.get_event(attr.event_id, pythonify=True).Tag if tag.name.startswith('misp-galaxy:target-information=')]\n", + " for attr in r3\n", + "]\n", + "allUniqueEventTag = set()\n", + "for tags in allEventTags:\n", + " for tag in tags:\n", + " allUniqueEventTag.add(tag)\n", + "print('All unique Event tags:', allUniqueEventTag)" + ] + }, + { + "cell_type": "code", + "execution_count": 504, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Negation: 17\n", + "All unique Event tags: {'misp-galaxy:target-information=\"Canada\"', 'misp-galaxy:target-information=\"China\"', 'misp-galaxy:target-information=\"Germany\"'}\n" + ] + } + ], + "source": [ + "r4 = misp.search(\n", + " controller='attributes',\n", + " tags=['misp-galaxy:target-information=%', '!misp-galaxy:target-information=\"Luxembourg\"'],\n", + " pythonify=True)\n", + "print('Negation:', len(r4))\n", + "\n", + "\n", + "# Showing unique Event tags\n", + "allEventTags = [\n", + " [tag.name for tag in misp.get_event(attr.event_id, pythonify=True).Tag if tag.name.startswith('misp-galaxy:target-information=')]\n", + " for attr in r4\n", + "]\n", + "allUniqueEventTag = set()\n", + "for tags in allEventTags:\n", + " for tag in tags:\n", + " allUniqueEventTag.add(tag)\n", + "print('All unique Event tags:', allUniqueEventTag)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Want to also have the Event tags included**?" + ] + }, + { + "cell_type": "code", + "execution_count": 505, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tags of first attibute: []\n", + "Tags of first attibute: ['tlp:white', 'osint:lifetime=\"perpetual\"', 'osint:certainty=\"50\"', 'workflow:state=\"draft\"', 'misp-galaxy:threat-actor=\"APT 29\"', 'smo:sync', 'misp-galaxy:target-information=\"Canada\"', 'misp-galaxy:target-information=\"China\"', 'misp-galaxy:sector=\"Defense\"', 'misp-galaxy:sector=\"Infrastructure\"', 'misp-galaxy:malpedia=\"Kobalos\"', 'misp-galaxy:mitre-attack-pattern=\"SSH - T1021.004\"', 'misp-galaxy:mitre-attack-pattern=\"Software - T1592.002\"']\n" + ] + } + ], + "source": [ + "r5 = misp.search(\n", + " controller='attributes',\n", + " tags='misp-galaxy:target-information=%',\n", + " pythonify=True)\n", + "print('Tags of first attibute:', [tag.name for tag in r5[0].Tag])\n", + "\n", + "r6 = misp.search(\n", + " controller='attributes',\n", + " tags='misp-galaxy:target-information=%',\n", + " includeEventTags=True,\n", + " pythonify=True)\n", + "print('Tags of first attibute:', [tag.name for tag in r6[0].Tag])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Complex query**" + ] + }, + { + "cell_type": "code", + "execution_count": 506, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Or: 1056\n", + "[['tlp:amber'], ['tlp:amber'], ['tlp:amber'], ['tlp:amber'], ['tlp:amber']]\n", + "\n", + "And: 5\n", + "[['adversary:infrastructure-type=\"c2\"', 'tlp:amber'],\n", + " ['adversary:infrastructure-type=\"c2\"', 'tlp:amber'],\n", + " ['adversary:infrastructure-type=\"c2\"', 'tlp:amber'],\n", + " ['adversary:infrastructure-type=\"c2\"', 'tlp:amber'],\n", + " ['adversary:infrastructure-type=\"c2\"', 'tlp:amber']]\n" + ] + } + ], + "source": [ + "complex_query = misp.build_complex_query(or_parameters=['tlp:amber', 'adversary:infrastructure-type=\"c2\"'])\n", + "r7 = misp.search(\n", + " controller='attributes',\n", + " tags=complex_query,\n", + " includeEventTags=True,\n", + " pythonify=True)\n", + "print('Or:', len(r7))\n", + "pprint([\n", + " [tag.name for tag in attr.Tag if (tag.name == 'tlp:amber' or tag.name == 'adversary:infrastructure-type=\"c2\"')] for attr in r7[:5]\n", + "])\n", + "print()\n", + "\n", + "complex_query = misp.build_complex_query(and_parameters=['tlp:amber', 'adversary:infrastructure-type=\"c2\"'])\n", + "r8 = misp.search(\n", + " controller='attributes',\n", + " tags=complex_query,\n", + " includeEventTags=True,\n", + " pythonify=True)\n", + "print('And:', len(r8))\n", + "pprint([\n", + " [tag.name for tag in attr.Tag if (tag.name == 'tlp:amber' or tag.name == 'adversary:infrastructure-type=\"c2\"')] for attr in r8\n", + "])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Searching on GalaxyCluster metadata" + ] + }, + { + "cell_type": "code", + "execution_count": 507, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Events: 2\n", + "[['misp-galaxy:target-information=\"Canada\"',\n", + " 'misp-galaxy:target-information=\"China\"'],\n", + " ['misp-galaxy:target-information=\"Luxembourg\"']]\n" + ] + } + ], + "source": [ + "body = {\n", + " 'galaxy.member-of': 'NATO',\n", + " 'galaxy.official-languages': 'French',\n", + "}\n", + "\n", + "events = misp.direct_call('/events/restSearch', body)\n", + "print('Events: ', len(events))\n", + "pprint([\n", + " [tag['name'] for tag in event['Event']['Tag'] if tag['name'].startswith('misp-galaxy:target-information')] for event in events\n", + "])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- **Note 1**: The `galaxy.*` instructions are not supported by PyMISP\n", + "- **Note 2**: Each `galaxy.*` instructions are **AND**ed and are applied for the same cluster\n", + " - Cannot combine from different clusters\n", + " - Combining `Galaxy.official-languages` and `Galaxy.synonyms` would likely gives no result" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Searching on creator Organisation metadata" + ] + }, + { + "cell_type": "code", + "execution_count": 508, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Organisation nationality: {'admin_org': '', 'CIRCL': '', 'ORGNAME': '', 'Training': 'Luxembourg'}\n", + "Events: 4\n", + "Org for each Event: ['Training', 'Training', 'Training', 'Training']\n" + ] + } + ], + "source": [ + "all_orgs = misp.organisations()\n", + "print('Organisation nationality:', {org['Organisation']['name']: org['Organisation']['nationality'] for org in all_orgs})\n", + "\n", + "body = {\n", + " 'org.nationality': ['Luxembourg'],\n", + " 'org.sector': ['financial'],\n", + "}\n", + "\n", + "events = misp.direct_call('/events/restSearch', body)\n", + "print('Events: ', len(events))\n", + "print('Org for each Event:', [event['Event']['Orgc']['name'] for event in events])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- **Note 1**: The `org.*` instructions are not supported by PyMISP" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### ReturnFormat" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**CSV**" + ] + }, + { + "cell_type": "code", + "execution_count": 509, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "uuid,event_id,category,type,value,comment,to_ids,date,object_relation,attribute_tag,object_uuid,object_name,object_meta_category\n", + "\"724d5417-41e6-40a5-b368-bdfbe652302a\",2,\"Network activity\",\"ip-dst\",\"4.3.2.1\",\"Hello all!\",0,1639127173,\"\",\"\",\"\",\"\",\"\"\n", + "\"ba8e1a5a-6bb6-4ae5-9872-0a01b6b05cad\",2,\"Network activity\",\"ip-dst\",\"5.3.1.2\",\"\",1,1639060465,\"ip\",\"\",\"\",\"\",\"\"\n", + "\"8c16cf20-d5bd-4ed3-b243-98c00c16e591\",2,\"Network activity\",\"ip-dst\",\"23.1.4.2\",\"\",1,1639126626,\"ip\",\"\",\"\",\"\",\"\"\n", + "\"25a7bbb0-31f6-4525-94c0-89af86030201\",16,\"Network activity\",\"ip-dst\",\"127.0.0.1\",\"\",1,1645191487,\"ip-dst\",\"\",\"\",\"\",\"\"\n", + "\"f3eb2f37-d08d-4dbb-be0c-346ac508693f\",16,\"Network activity\",\"ip-dst\",\"127.0.0.1\",\"\",1,1645191487,\"ip-dst\",\"\",\"\",\"\",\"\"\n", + "\"f0a002d8-38a5-40f9-9a62-7e975cc8f987\",16,\"Network activity\",\"ip-dst\",\"127.0.0.1\",\"\",1,1645191487,\"ip-dst\",\"\",\"\",\"\",\"\"\n", + "\"61bfb8e3-20e3-4f37-905d-9d4e14f2564a\",20,\"Network activity\",\"ip-dst\",\"8.231.77.176\",\"\",1,1665471239,\"ip\",\"PAP:RED,adversary:infrastructure-type=\"\"exploit-distribution-point\"\"\",\"\",\"\",\"\"\n", + "\"1ac08260-a5d6-4bee-bdcd-1525685ea07d\",20,\"Network activity\",\"ip-dst\",\"226.140.183.77\",\"\",1,1665471204,\"ip\",\"PAP:RED,adversary:infrastructure-type=\"\"c2\"\"\",\"\",\"\",\"\"\n", + "\"78ce291d-241b-4162-8d6b-6a85964a31b8\",20,\"Network activity\",\"ip-dst\",\"2efe:65b4:7533:4f5f:1081:995:ff87:348f\",\"\",1,1665471204,\"ip\",\"PAP:RED,adversary:infrastructure-type=\"\"c2\"\"\",\"\",\"\",\"\"\n", + "\"b760f7a7-0d96-4b47-86b2-d5524cd2eff0\",26,\"Network activity\",\"ip-dst\",\"8.8.8.8\",\"\",1,1663321650,\"ip\",\"\",\"\",\"\",\"\"\n", + "\"9023deba-1ba0-4ab3-a0bf-64a2d5c90520\",29,\"Network activity\",\"ip-dst\",\"81.177.170.166\",\"\",1,1665472920,\"ip\",\"adversary:infrastructure-type=\"\"c2\"\",misp-galaxy:mitre-attack-pattern=\"\"Botnet - T1583.005\"\"\",\"\",\"\",\"\"\n", + "\"c9d681ad-4087-4847-8f93-aef2e54452f2\",42,\"Network activity\",\"ip-dst\",\"2.2.2.2\",\"\",0,1671095982,\"ip\",\"\",\"\",\"\",\"\"\n", + "\"60950f6a-b3bf-4a0a-b901-43308e2f761a\",2,\"Network activity\",\"ip-src\",\"1.2.3.4\",\"\",0,1639060409,\"\",\"\",\"\",\"\",\"\"\n", + "\"f2a6eb8c-7a3e-4524-8036-1b90cb18fe75\",7,\"Payload delivery\",\"ip-src\",\"149.23.54.0\",\"today\",1,1622184577,\"\",\"\",\"\",\"\",\"\"\n", + "\"93bc9e55-20e9-4be1-b3e5-057e56a3b82e\",7,\"Payload delivery\",\"ip-src\",\"149.23.54.1\",\"today - 1 days\",1,1622184577,\"\",\"\",\"\",\"\",\"\"\n", + "\"f7771a53-fbdf-4980-822d-9a2339ce9076\",7,\"Payload delivery\",\"ip-src\",\"149.23.54.2\",\"today - 2 days\",1,1622184577,\"\",\"\",\"\",\"\",\"\"\n", + "\"4972022a-26fd-4270-b614-506a9c951be6\",7,\"Payload delivery\",\"ip-src\",\"149.23.54.3\",\"today - 3 days\",1,1622184578,\"\",\"admiralty-scale:information-credibility=\"\"1\"\",admiralty-scale:source-reliability=\"\"a\"\"\",\"\",\"\",\"\"\n", + "\"c661cd4b-0474-48eb-b4ed-eb02f6b569ea\",7,\"Payload delivery\",\"ip-src\",\"149.23.54.4\",\"today - 4 days\",1,1622184578,\"\",\"\",\"\",\"\",\"\"\n", + "\"42f68239-a794-492c-8fed-7520677824b0\",7,\"Payload delivery\",\"ip-src\",\"149.23.54.5\",\"today - 5 days\",1,1622184578,\"\",\"\",\"\",\"\",\"\"\n", + "\"d6404ba7-c847-49b8-8748-3029ce62e2b0\",7,\"Payload delivery\",\"ip-src\",\"149.23.54.6\",\"today - 6 days\",1,1622184578,\"\",\"\",\"\",\"\",\"\"\n", + "\"f04de340-ec63-471e-b5a2-66c3fe0676b6\",9,\"Network activity\",\"ip-src\",\"5.4.2.1\",\"\",0,1650956697,\"\",\"misp-galaxy:mitre-course-of-action=\"\"Access Token Manipulation Mitigation - T1134\"\"\",\"\",\"\",\"\"\n", + "\"7bb5432f-3d67-4d59-8a43-04e57e0dcc3f\",16,\"Network activity\",\"ip-src\",\"127.0.0.1\",\"\",1,1645191487,\"ip-src\",\"\",\"\",\"\",\"\"\n", + "\"b663b3b3-92af-41bf-a18f-8582bd0983b1\",16,\"Network activity\",\"ip-src\",\"127.0.0.1\",\"\",1,1645191487,\"ip-src\",\"\",\"\",\"\",\"\"\n", + "\"0ee4a946-d826-4884-aa28-e1b9da8cbbcb\",16,\"Network activity\",\"ip-src\",\"127.0.0.1\",\"\",1,1645191487,\"ip-src\",\"\",\"\",\"\",\"\"\n", + "\"1f4b0f6b-6cf9-47bf-acd4-f15b33e7d588\",21,\"Network activity\",\"ip-src\",\"185.194.93.14\",\"Attribute #281 enriched by dns.\",0,1668077578,\"\",\"\",\"\",\"\",\"\"\n", + "\"9f7f2d28-bcc8-466e-847f-3cf2a1ec4070\",21,\"Network activity\",\"ip-src\",\"31.22.121.122\",\"Attribute #291 enriched by dns.\",0,1663922175,\"\",\"\",\"\",\"\",\"\"\n", + "\"8153e053-c7c3-4a34-ae1c-b5cd3c80ba06\",22,\"Network activity\",\"ip-src\",\"8.231.77.176\",\"\",0,1659602097,\"\",\"\",\"\",\"\",\"\"\n", + "\"a57f70a2-70dd-4ea4-b879-fbcd03d465df\",24,\"Network activity\",\"ip-src\",\"8.231.77.176\",\"\",0,1662025545,\"\",\"another:tag\",\"\",\"\",\"\"\n", + "\"af044e10-5549-4018-bc6b-162cde1a1016\",21,\"Network activity\",\"ip-src\",\"8.231.77.176\",\"\",0,1661517935,\"\",\"\",\"\",\"\",\"\"\n", + "\"fbb12142-0f82-4430-b0bc-2b1f9e26af67\",23,\"Network activity\",\"ip-src\",\"8.231.77.176\",\"\",0,1661518277,\"\",\"\",\"\",\"\",\"\"\n", + "\"a783c55f-ac52-44b4-8be1-74d52bc2c4c3\",17,\"Network activity\",\"ip-src\",\"8.231.77.176\",\"\",0,1661517997,\"\",\"\",\"\",\"\",\"\"\n", + "\"90f6fd39-a426-43b3-9157-0c48bf0710fb\",22,\"Network activity\",\"ip-src\",\"31.22.121.122\",\"\",0,1661762437,\"\",\"\",\"\",\"\",\"\"\n", + "\"bc0a1ba5-d337-42b3-81fe-9d4b75a17bec\",26,\"Network activity\",\"ip-src\",\"185.194.93.14\",\"\",0,1663137408,\"\",\"\",\"\",\"\",\"\"\n", + "\"93931645-c86c-4dcf-aa4e-591edab44c4e\",26,\"Network activity\",\"ip-src\",\"8.8.8.8\",\"\",1,1663320641,\"\",\"\",\"\",\"\",\"\"\n", + "\n", + "\n" + ] + } + ], + "source": [ + "r1 = misp.search(\n", + " controller='attributes',\n", + " type_attribute=['ip-src', 'ip-dst'],\n", + " return_format='csv')\n", + "print(r1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Aggregated context** with `context-markdown`, `context` and `attack`" + ] + }, + { + "cell_type": "code", + "execution_count": 510, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "# Aggregated context data\n", + "## Tags and Taxonomies\n", + "#### admiralty-scale\n", + "*The Admiralty Scale or Ranking (also called the NATO System) is used to rank the reliability of a source and the credibility of an information. Reference based on FM 2-22.3 (FM 34-52) HUMAN INTELLIGENCE COLLECTOR OPERATIONS and NATO documents.*\n", + "- admiralty-scale:information-credibility="1"\n", + "\n", + " - **information-credibility**: Information Credibility\n", + " - **1**: Confirmed by other sources\n", + "- admiralty-scale:information-credibility="2"\n", + "\n", + " - **information-credibility**: Information Credibility\n", + " - **2**: Probably true\n", + "- admiralty-scale:source-reliability="a"\n", + "\n", + " - **source-reliability**: Source Reliability\n", + " - **a**: Completely reliable\n", + "#### economical-impact\n", + "*Economical impact is a taxonomy to describe the financial impact as positive or negative gain to the tagged information (e.g. data exfiltration loss, a positive gain for an adversary).*\n", + "- economical-impact:loss="less-than-1B-euro"\n", + "\n", + " - **loss**: Loss\n", + " - **less-than-1B-euro**: Less than 1 billion EUR\n", + "#### osint\n", + "*Open Source Intelligence - Classification (MISP taxonomies)*\n", + "- osint:certainty="50"\n", + "\n", + " - **certainty**: Certainty of the elements mentioned in this Open Source Intelligence\n", + " - **50**: Chances about even (probability equals 0.50 - 50%)\n", + "- osint:lifetime="perpetual"\n", + "\n", + " - **lifetime**: Lifetime of the information as Open Source Intelligence\n", + " - **perpetual**: Perpetual\n", + "#### tlp\n", + "*The Traffic Light Protocol - or short: TLP - was designed with the objective to create a favorable classification scheme for sharing sensitive information while keeping the control over its distribution at the same time.*\n", + "- tlp:red\n", + "\n", + " - **red**: (TLP:RED) Information exclusively and directly given to (a group of) individual recipients. Sharing outside is not legitimate.\n", + "- tlp:white\n", + "\n", + " - **white**: (TLP:WHITE) Information can be shared publicly in accordance with the law.\n", + "#### workflow\n", + "*Workflow support language is a common language to support intelligence analysts to perform their analysis on data and information.*\n", + "- workflow:state="draft"\n", + "\n", + " - **state**: State\n", + " - **draft**: Draft means the information tagged can be released as a preliminary version or outline\n", + "## Galaxy Clusters\n", + "#### Misinformation Pattern\n", + "*AM!TT Tactic*\n", + "- *[Adapt existing narratives](https://localhost:8443/galaxy_clusters/view/2712)*\n", + "Adapting existing narratives to current operational goals is the tactical sweet-spot for an effective misinformation campaign. Leveraging existing narratives is not only more effective, it requires substantially less resourcing, as the promotion of new master narratives operates on a much larger sca...\n", + "#### Malpedia\n", + "*Malware galaxy based on Malpedia archive.*\n", + "- *[Kobalos](https://localhost:8443/galaxy_clusters/view/4530)*\n", + "\n", + "#### Attack Pattern\n", + "*ATT&CK Tactic*\n", + "- *[SSH - T1021.004](https://localhost:8443/galaxy_clusters/view/9691)*\n", + "Adversaries may use [Valid Accounts](https://attack.mitre.org/techniques/T1078) to log into remote machines using Secure Shell (SSH). The adversary may then perform actions as the logged-on user.\n", + "\n", + "SSH is a protocol that allows authorized users to open remote shells on other computers. Many Linux and...\n", + "- *[Software - T1592.002](https://localhost:8443/galaxy_clusters/view/9721)*\n", + "Adversaries may gather information about the victim's host software that can be used during targeting. Information about installed software may include a variety of details such as types and versions on specific hosts, as well as the presence of additional components that might be indicative of...\n", + "#### Course of Action\n", + "*ATT&CK Mitigation*\n", + "- *[Access Token Manipulation Mitigation - T1134](https://localhost:8443/galaxy_clusters/view/8213)*\n", + "Access tokens are an integral part of the security system within Windows and cannot be turned off. However, an attacker must already have administrator level access on the local system to make full use of this technique; be sure to restrict users and accounts to the least privileges they require to ...\n", + "#### Sector\n", + "*Activity sectors*\n", + "- *[Defense](https://localhost:8443/galaxy_clusters/view/2762)*\n", + "\n", + "- *[Infrastructure](https://localhost:8443/galaxy_clusters/view/2780)*\n", + "\n", + "#### Target Information\n", + "*Description of targets of threat actors.*\n", + "- *[Canada](https://localhost:8443/galaxy_clusters/view/1994)*\n", + "\n", + "- *[China](https://localhost:8443/galaxy_clusters/view/2000)*\n", + "\n", + "#### Threat Actor\n", + "*Threat actors are characteristics of malicious actors (or adversaries) representing a cyber attack threat including presumed intent and historically observed behaviour.*\n", + "- *[APT 29](https://localhost:8443/galaxy_clusters/view/7251)*\n", + "A 2015 report by F-Secure describe APT29 as: 'The Dukes are a well-resourced, highly dedicated and organized cyberespionage group that we believe has been working for the Russian Federation since at least 2008 to collect intelligence in support of foreign and security policy decision-making. Th...\n" + ] + } + ], + "source": [ + "# Get the context of Events that were created by organisations from the financial sector\n", + "\n", + "body = {\n", + " 'returnFormat': 'context-markdown',\n", + " 'org.sector': ['financial'],\n", + "}\n", + "\n", + "r2 = misp.direct_call('/events/restSearch', body)\n", + "print(r2)" + ] + }, + { + "cell_type": "code", + "execution_count": 511, + "metadata": {}, + "outputs": [], + "source": [ + "# Get the context of Events that had the threat actor APT-29 attached\n", + "\n", + "body = {\n", + " 'returnFormat': 'context',\n", + " 'tags': ['misp-galaxy:threat-actor=\\\"APT 29\\\"'],\n", + " 'staticHtml': 1, # If you want a JS-free HTML\n", + "}\n", + "\n", + "r2 = misp.direct_call('/events/restSearch', body)\n", + "with open('/tmp/attackOutput.html', 'w') as f:\n", + " f.write(r2)\n", + " # subprocess.run(['google-chrome', '--incognito', '/tmp/attackOutput.html'])\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Be carefull with the amount of data you ask, use `pagination` if needed\n", + "\n", + "- `limit`: Specify the amount of data to be returned\n", + "- `page`: Specify the start of the rolling window. Is **not** zero-indexed\n", + "\n", + "If the size of the returned data is larger than the memory enveloppe you might get a different behavior based on your MISP setting:\n", + "- Nothing returned. Allowed memeory by PHP process exausted\n", + "- Data returned but slow. MISP will concatenante the returned data in a temporary file on disk\n", + " - This behavior is only applicable for `/*/restSearch` endpoints" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r1 = misp.search(controller='attributes', pythonify=True)\n", + "print('Amount of Attributes', len(r1))\n", + "\n", + "r2 = misp.search(\n", + " controller='attributes',\n", + " page=1,\n", + " limit=5,\n", + " pythonify=True)\n", + "print('Amount of paginated Attributes', len(r2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Searching for Sightings" + ] + }, + { + "cell_type": "code", + "execution_count": 513, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[{'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '1441',\n", + " 'date_sighting': '1670924035',\n", + " 'event_id': '40',\n", + " 'id': '12',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '0',\n", + " 'uuid': '65bd7539-29eb-46eb-bf7b-4c02473062c7',\n", + " 'value': '398324'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '1441',\n", + " 'date_sighting': '1670924430',\n", + " 'event_id': '40',\n", + " 'id': '13',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '0',\n", + " 'uuid': '10857410-0033-4457-8a1d-c8331ee55d72',\n", + " 'value': '398324'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '1441',\n", + " 'date_sighting': '1670924454',\n", + " 'event_id': '40',\n", + " 'id': '14',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '1',\n", + " 'uuid': '1639fe60-0458-40f3-961b-7dc14eee9a7b',\n", + " 'value': '398324'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '1441',\n", + " 'date_sighting': '1670924455',\n", + " 'event_id': '40',\n", + " 'id': '15',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '1',\n", + " 'uuid': 'ee54ec70-3597-4455-bce9-c889202d533e',\n", + " 'value': '398324'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '1441',\n", + " 'date_sighting': '1670924456',\n", + " 'event_id': '40',\n", + " 'id': '16',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '1',\n", + " 'uuid': '2c1cf4d1-a6ce-474b-8878-0251ee2b6bc5',\n", + " 'value': '398324'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '1448',\n", + " 'date_sighting': '1671027299',\n", + " 'event_id': '41',\n", + " 'id': '17',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '0',\n", + " 'uuid': '39dff1d2-7082-48a9-8d30-ce29d412879b',\n", + " 'value': 'testtest'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '1448',\n", + " 'date_sighting': '1671027301',\n", + " 'event_id': '41',\n", + " 'id': '18',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '0',\n", + " 'uuid': '84a8e7d0-715b-453f-8cdb-07db0c208185',\n", + " 'value': 'testtest'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '77',\n", + " 'date_sighting': '1671027307',\n", + " 'event_id': '9',\n", + " 'id': '19',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '0',\n", + " 'uuid': '264e4a25-e072-46e5-8460-b8df72e3115c',\n", + " 'value': '5.4.2.1'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '77',\n", + " 'date_sighting': '1671027308',\n", + " 'event_id': '9',\n", + " 'id': '20',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '0',\n", + " 'uuid': 'b9f15aeb-54ea-44e5-90b8-22a418b973df',\n", + " 'value': '5.4.2.1'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '243',\n", + " 'date_sighting': '1671027309',\n", + " 'event_id': '9',\n", + " 'id': '21',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '0',\n", + " 'uuid': '4ef355f8-1cd3-476c-bccf-90a23b4eebfe',\n", + " 'value': 'test'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '1342',\n", + " 'date_sighting': '1671029412',\n", + " 'event_id': '29',\n", + " 'id': '22',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '0',\n", + " 'uuid': 'f0e76bec-2e04-4e88-a976-df831257c856',\n", + " 'value': 'malware.exe|70f3bc193dfa56b78f3e6e4f800f701f'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '1342',\n", + " 'date_sighting': '1671029413',\n", + " 'event_id': '29',\n", + " 'id': '23',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '0',\n", + " 'uuid': '803bb696-ae86-4a04-9793-5f54a45c99b7',\n", + " 'value': 'malware.exe|70f3bc193dfa56b78f3e6e4f800f701f'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '1342',\n", + " 'date_sighting': '1671029414',\n", + " 'event_id': '29',\n", + " 'id': '24',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '0',\n", + " 'uuid': 'fd8c4c0f-ebbb-4294-ade1-57493f1edc9a',\n", + " 'value': 'malware.exe|70f3bc193dfa56b78f3e6e4f800f701f'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '1441',\n", + " 'date_sighting': '1671030274',\n", + " 'event_id': '40',\n", + " 'id': '25',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '0',\n", + " 'uuid': 'c84dd497-ad48-4b82-8203-6135a9a924fc',\n", + " 'value': '398324'}}]\n" + ] + } + ], + "source": [ + "body = {\n", + " 'last': '7d'\n", + "}\n", + "\n", + "sightings = misp.direct_call('/sightings/restSearch', body)\n", + "pprint(sightings)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plotting data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Sightings over time" + ] + }, + { + "cell_type": "code", + "execution_count": 512, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": 514, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idattribute_idevent_idorg_iddate_sightinguuidsourcetypevalueOrganisationone
01214414012022-12-13 09:33:5565bd7539-29eb-46eb-bf7b-4c02473062c70398324{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
11314414012022-12-13 09:40:3010857410-0033-4457-8a1d-c8331ee55d720398324{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
21414414012022-12-13 09:40:541639fe60-0458-40f3-961b-7dc14eee9a7b1398324{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
31514414012022-12-13 09:40:55ee54ec70-3597-4455-bce9-c889202d533e1398324{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
41614414012022-12-13 09:40:562c1cf4d1-a6ce-474b-8878-0251ee2b6bc51398324{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
51714484112022-12-14 14:14:5939dff1d2-7082-48a9-8d30-ce29d412879b0testtest{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
61814484112022-12-14 14:15:0184a8e7d0-715b-453f-8cdb-07db0c2081850testtest{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
71977912022-12-14 14:15:07264e4a25-e072-46e5-8460-b8df72e3115c05.4.2.1{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
82077912022-12-14 14:15:08b9f15aeb-54ea-44e5-90b8-22a418b973df05.4.2.1{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
921243912022-12-14 14:15:094ef355f8-1cd3-476c-bccf-90a23b4eebfe0test{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
102213422912022-12-14 14:50:12f0e76bec-2e04-4e88-a976-df831257c8560malware.exe|70f3bc193dfa56b78f3e6e4f800f701f{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
112313422912022-12-14 14:50:13803bb696-ae86-4a04-9793-5f54a45c99b70malware.exe|70f3bc193dfa56b78f3e6e4f800f701f{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
122413422912022-12-14 14:50:14fd8c4c0f-ebbb-4294-ade1-57493f1edc9a0malware.exe|70f3bc193dfa56b78f3e6e4f800f701f{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
132514414012022-12-14 15:04:34c84dd497-ad48-4b82-8203-6135a9a924fc0398324{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
\n", + "
" + ], + "text/plain": [ + " id attribute_id event_id org_id date_sighting \\\n", + "0 12 1441 40 1 2022-12-13 09:33:55 \n", + "1 13 1441 40 1 2022-12-13 09:40:30 \n", + "2 14 1441 40 1 2022-12-13 09:40:54 \n", + "3 15 1441 40 1 2022-12-13 09:40:55 \n", + "4 16 1441 40 1 2022-12-13 09:40:56 \n", + "5 17 1448 41 1 2022-12-14 14:14:59 \n", + "6 18 1448 41 1 2022-12-14 14:15:01 \n", + "7 19 77 9 1 2022-12-14 14:15:07 \n", + "8 20 77 9 1 2022-12-14 14:15:08 \n", + "9 21 243 9 1 2022-12-14 14:15:09 \n", + "10 22 1342 29 1 2022-12-14 14:50:12 \n", + "11 23 1342 29 1 2022-12-14 14:50:13 \n", + "12 24 1342 29 1 2022-12-14 14:50:14 \n", + "13 25 1441 40 1 2022-12-14 15:04:34 \n", + "\n", + " uuid source type \\\n", + "0 65bd7539-29eb-46eb-bf7b-4c02473062c7 0 \n", + "1 10857410-0033-4457-8a1d-c8331ee55d72 0 \n", + "2 1639fe60-0458-40f3-961b-7dc14eee9a7b 1 \n", + "3 ee54ec70-3597-4455-bce9-c889202d533e 1 \n", + "4 2c1cf4d1-a6ce-474b-8878-0251ee2b6bc5 1 \n", + "5 39dff1d2-7082-48a9-8d30-ce29d412879b 0 \n", + "6 84a8e7d0-715b-453f-8cdb-07db0c208185 0 \n", + "7 264e4a25-e072-46e5-8460-b8df72e3115c 0 \n", + "8 b9f15aeb-54ea-44e5-90b8-22a418b973df 0 \n", + "9 4ef355f8-1cd3-476c-bccf-90a23b4eebfe 0 \n", + "10 f0e76bec-2e04-4e88-a976-df831257c856 0 \n", + "11 803bb696-ae86-4a04-9793-5f54a45c99b7 0 \n", + "12 fd8c4c0f-ebbb-4294-ade1-57493f1edc9a 0 \n", + "13 c84dd497-ad48-4b82-8203-6135a9a924fc 0 \n", + "\n", + " value \\\n", + "0 398324 \n", + "1 398324 \n", + "2 398324 \n", + "3 398324 \n", + "4 398324 \n", + "5 testtest \n", + "6 testtest \n", + "7 5.4.2.1 \n", + "8 5.4.2.1 \n", + "9 test \n", + "10 malware.exe|70f3bc193dfa56b78f3e6e4f800f701f \n", + "11 malware.exe|70f3bc193dfa56b78f3e6e4f800f701f \n", + "12 malware.exe|70f3bc193dfa56b78f3e6e4f800f701f \n", + "13 398324 \n", + "\n", + " Organisation one \n", + "0 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "1 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "2 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "3 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "4 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "5 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "6 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "7 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "8 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "9 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "10 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "11 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "12 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "13 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 " + ] + }, + "execution_count": 514, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Converting our data to Panda DataFrame\n", + "sighting_rearranged = [sighting['Sighting'] for sighting in sightings]\n", + "df = pd.DataFrame.from_dict(sighting_rearranged)\n", + "df[\"date_sighting\"] = pd.to_datetime(df[\"date_sighting\"], unit='s')\n", + "df['one'] = 1\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print('Min and Max:', df['date_sighting'].min(), df['date_sighting'].max())\n", + "print('Time delta:', df['date_sighting'].max() - df['date_sighting'].min())\n", + "print('Unique Event IDs:', df.event_id.unique())" + ] + }, + { + "cell_type": "code", + "execution_count": 515, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1441 6\n", + "1342 3\n", + "1448 2\n", + "77 2\n", + "243 1\n", + "Name: attribute_id, dtype: int64\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 515, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAhYAAAGyCAYAAAC4Io22AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8o6BhiAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAhQUlEQVR4nO3de1TUdf7H8dcAMmICKt6DVdrKBG/lLTLdLNNF87RtlrmWZrtdNS+UJZqarYa15SHbMu3mntZba2tlpXbZzDpo3kItjxomiGLaYjKKOQLz+f3hz1lJUQc/wzDD83HOnNMM32He9mGYJ9/5zozDGGMEAABgQVigBwAAAKGDsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1EVV9gx6PRwUFBYqOjpbD4ajqmwcAAJVgjNHhw4fVvHlzhYVVvF+iysOioKBACQkJVX2zAADAgvz8fMXHx1f49SoPi+joaEknBouJianqmwcAAJXgcrmUkJDgfRyvSJWHxcmnP2JiYggLAACCzLkOY+DgTQAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArPE5LPbu3as777xTcXFxioqKUtu2bbV+/Xp/zAYAAIKMT58V8vPPP6tbt27q2bOnli1bpkaNGun7779X/fr1/TUfAAAIIj6FxTPPPKOEhAS9+eab3ssSExOtDwUAAIKTT0+FvP/+++rUqZNuu+02NW7cWFdeeaVeffXVs17H7XbL5XKVOwEAgNDk0x6LH374QbNmzVJaWprGjx+vdevWaeTIkYqMjNTQoUPPeJ2MjAxNmTLFyrDnq+W4D6v09vwld3q/QI8AAIBPHMYYc74bR0ZGqlOnTsrKyvJeNnLkSK1bt06rV68+43Xcbrfcbrf3vMvlUkJCgoqKihQTE3MBo1eMsAAAwC6Xy6XY2NhzPn779FRIs2bNlJSUVO6y1q1ba/fu3RVex+l0KiYmptwJAACEJp/Colu3btq+fXu5y3bs2KEWLVpYHQoAAAQnn8JizJgxWrNmjZ5++mnl5ORo/vz5mjNnjoYPH+6v+QAAQBDxKSw6d+6sJUuWaMGCBWrTpo3++te/KjMzU4MHD/bXfAAAIIj49KoQSbrpppt00003+WMWAAAQ5PisEAAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGt8Cosnn3xSDoej3OmKK67w12wAACDIRPh6heTkZH366af/+wYRPn8LAAAQonyugoiICDVt2tQfswAAgCDn8zEW33//vZo3b65LLrlEgwcP1u7du8+6vdvtlsvlKncCAAChyaew6Nq1q+bOnavly5dr1qxZ2rVrl7p3767Dhw9XeJ2MjAzFxsZ6TwkJCRc8NAAAqJ4cxhhT2SsfOnRILVq00IwZM/TnP//5jNu43W653W7veZfLpYSEBBUVFSkmJqayN31WLcd96JfvW9Vyp/cL9AgAAEg68fgdGxt7zsfvCzrysl69err88suVk5NT4TZOp1NOp/NCbgYAAASJC3ofiyNHjmjnzp1q1qyZrXkAAEAQ8yksHn30UX3xxRfKzc1VVlaWbrnlFoWHh2vQoEH+mg8AAAQRn54K2bNnjwYNGqTCwkI1atRI1157rdasWaNGjRr5az4AABBEfAqLhQsX+msOAAAQAvisEAAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhzQWExffp0ORwOjR492tI4AAAgmFU6LNatW6fZs2erXbt2NucBAABBrFJhceTIEQ0ePFivvvqq6tevb3smAAAQpCoVFsOHD1e/fv3Uq1cv2/MAAIAgFuHrFRYuXKiNGzdq3bp157W92+2W2+32nne5XL7eJAAACBI+7bHIz8/XqFGjNG/ePNWuXfu8rpORkaHY2FjvKSEhoVKDAgCA6s9hjDHnu/G7776rW265ReHh4d7LysrK5HA4FBYWJrfbXe5r0pn3WCQkJKioqEgxMTEW/gmnaznuQ79836qWO71foEcAAEDSicfv2NjYcz5++/RUyA033KAtW7aUu2zYsGG64oor9Pjjj58WFZLkdDrldDp9uRkAABCkfAqL6OhotWnTptxlF110keLi4k67HAAA1Dy88yYAALDG51eF/NrKlSstjAEAAEIBeywAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACs8SksZs2apXbt2ikmJkYxMTFKSUnRsmXL/DUbAAAIMj6FRXx8vKZPn64NGzZo/fr1uv7663XzzTfru+++89d8AAAgiET4snH//v3LnZ82bZpmzZqlNWvWKDk52epgAAAg+PgUFqcqKyvTv/71LxUXFyslJaXC7dxut9xut/e8y+Wq7E0CAIBqzuew2LJli1JSUnTs2DHVrVtXS5YsUVJSUoXbZ2RkaMqUKRc0JIJby3EfBnqEC5Y7vV+gRwCAoODzq0JatWql7Oxsff3113rwwQc1dOhQbd26tcLt09PTVVRU5D3l5+df0MAAAKD68nmPRWRkpC699FJJUseOHbVu3Tq98MILmj179hm3dzqdcjqdFzYlAAAIChf8PhYej6fcMRQAAKDm8mmPRXp6ulJTU/Wb3/xGhw8f1vz587Vy5UqtWLHCX/MBAIAg4lNYHDhwQEOGDNG+ffsUGxurdu3aacWKFbrxxhv9NR8AAAgiPoXF66+/7q85AABACOCzQgAAgDWEBQAAsIawAAAA1hAWAADAGsICAABYQ1gAAABrCAsAAGANYQEAAKwhLAAAgDWEBQAAsIawAAAA1hAWAADAGsICAABYQ1gAAABrCAsAAGANYQEAAKwhLAAAgDWEBQAAsIawAAAA1hAWAADAGsICAABYQ1gAAABrCAsAAGANYQEAAKwhLAAAgDWEBQAAsIawAAAA1hAWAADAGsICAABYQ1gAAABrCAsAAGANYQEAAKwhLAAAgDWEBQAAsIawAAAA1hAWAADAGsICAABYQ1gAAABrCAsAAGANYQEAAKwhLAAAgDWEBQAAsIawAAAA1hAWAADAGsICAABYQ1gAAABrCAsAAGANYQEAAKzxKSwyMjLUuXNnRUdHq3HjxvrDH/6g7du3+2s2AAAQZHwKiy+++ELDhw/XmjVr9Mknn6ikpES9e/dWcXGxv+YDAABBJMKXjZcvX17u/Ny5c9W4cWNt2LBBPXr0sDoYAAAIPj6Fxa8VFRVJkho0aFDhNm63W26323ve5XJdyE0CAIBqrNJh4fF4NHr0aHXr1k1t2rSpcLuMjAxNmTKlsjcDwKKW4z4M9AgXLHd6v0CPYEUorIUUOusBeyr9qpDhw4fr22+/1cKFC8+6XXp6uoqKiryn/Pz8yt4kAACo5iq1x2LEiBH64IMPtGrVKsXHx591W6fTKafTWanhAABAcPEpLIwxevjhh7VkyRKtXLlSiYmJ/poLAAAEIZ/CYvjw4Zo/f77ee+89RUdH68cff5QkxcbGKioqyi8DAgCA4OHTMRazZs1SUVGRrrvuOjVr1sx7WrRokb/mAwAAQcTnp0IAAAAqwmeFAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBqfw2LVqlXq37+/mjdvLofDoXfffdcPYwEAgGDkc1gUFxerffv2eumll/wxDwAACGIRvl4hNTVVqamp/pgFAAAEOY6xAAAA1vi8x8JXbrdbbrfbe97lcvn7JgEAQID4PSwyMjI0ZcoUf98MAACV0nLch4EewYrc6f0CPYKkKngqJD09XUVFRd5Tfn6+v28SAAAEiN/3WDidTjmdTn/fDAAAqAZ8DosjR44oJyfHe37Xrl3Kzs5WgwYN9Jvf/MbqcAAAILj4HBbr169Xz549vefT0tIkSUOHDtXcuXOtDQYAAIKPz2Fx3XXXyRjjj1kAAECQ430sAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArKlUWLz00ktq2bKlateura5du2rt2rW25wIAAEHI57BYtGiR0tLSNHnyZG3cuFHt27dXnz59dODAAX/MBwAAgojPYTFjxgzde++9GjZsmJKSkvTKK6+oTp06euONN/wxHwAACCIRvmx8/PhxbdiwQenp6d7LwsLC1KtXL61evfqM13G73XK73d7zRUVFkiSXy1WZec+Lx33Ub9+7Kvnz/1FVCoX1YC2qD9aiegmF9WAtfPv+xpizb2h8sHfvXiPJZGVllbt87NixpkuXLme8zuTJk40kTpw4ceLEiVMInPLz88/aCj7tsaiM9PR0paWlec97PB4dPHhQcXFxcjgc/r55v3C5XEpISFB+fr5iYmICPU6NxlpUL6xH9cFaVB+hshbGGB0+fFjNmzc/63Y+hUXDhg0VHh6u/fv3l7t8//79atq06Rmv43Q65XQ6y11Wr149X2622oqJiQnqH5JQwlpUL6xH9cFaVB+hsBaxsbHn3MangzcjIyPVsWNHffbZZ97LPB6PPvvsM6WkpPg+IQAACCk+PxWSlpamoUOHqlOnTurSpYsyMzNVXFysYcOG+WM+AAAQRHwOi4EDB+qnn37SpEmT9OOPP6pDhw5avny5mjRp4o/5qiWn06nJkyef9hQPqh5rUb2wHtUHa1F91LS1cJhzvm4EAADg/PBZIQAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAIESd+gGQQFUhLFAj8Krq6sXj8QR6hJC3fft2TZgwQSUlJYEeBecQar+f/P4hZDWRx+NRWBjNFkjHjh3T8ePHvZ9V43A4ZIwJ2g++C3YFBQXatm2bCgsLddtttyksLIz7iR9t3rxZXbt2ldvtVs+ePdWvX79Aj4T/l5ubqyVLlujIkSNq1aqVbr/99pD7vcQbZFmSk5OjefPmaezYsapTpw6/NANo69ateuKJJ7Rjxw61aNFC/fv31wMPPBDosWqsLVu2aMCAAYqMjFReXp7atWunr776KtBjhaxNmzYpJSVF99xzjwoLCxUeHq45c+YoKioq5B7Ags3mzZuVmpqq1q1bq7i4WLm5ucrMzNTAgQMDPZpVPPJZkJOTo27duunFF1/UxIkTdfToUe9fZKhaW7duVY8ePdSsWTM99NBDio2N1bx587Rx48ZAj1Yj7dq1S3369NHgwYP1/vvva9myZcrLy9P69esDPVpI2rhxo7p37660tDT9/e9/19VXX62lS5eqoKDAu9cOgbFjxw717dtXQ4YM0YoVK/T222+rbdu2OnLkSKBHs449FheoqKhId999tyIiIpSYmKhVq1YpJSVF06ZNY89FFSssLNStt96qDh06KDMzU5J08OBBXXXVVRo+fLjGjh0b2AFroNmzZ+vf//63li5dqsjISJWWlurGG2/UQw89pEOHDql///5q2rRpoMcMCS6XS82bN9d9992nGTNmSJJKSkp0zTXXKCkpSXPnzmWPRYCUlJTogQceUGlpqV5//XVFRJw4CuH2229XVFSU6tevrxYtWmjMmDEBntQOHvEuUN26dZWUlKQBAwZo6tSp6tevn1avXq3x48efcc8FHec/eXl5atKkiW699VZJUmlpqRo0aKC+ffuqsLBQkliLKpafn69t27YpMjJSkpSZmamsrCxlZmZq+vTpuuqqq7x7L1iPytuzZ492796tjRs3eqPCGKOwsDD17t1bGzZs0H//+1/v5ahatWrV0iOPPKJ7773XGxUZGRlavHixysrK9Msvv+ixxx4LnadsDS5YSUmJ97+PHj1qpkyZYrp27WpGjRpljh49aowx5tixY4Ear8bYv3+/WbBggfe8x+MxxhjzwAMPmGHDhgVqrBpt8+bNpmnTpua3v/2tue2220ytWrXMihUrTFFRkTHGmJ49e5oePXoEeMrgtmXLFpOQkGDS0tKMMcaUlpYaY/73879//34THR1tnnrqqYDNWFOd+thwquzsbJOSkmI+/PBD72Vvv/22qVu3rvn222+rajy/YY9FJRQUFGjfvn3e8ycLtLS0VFFRUXr88cfVt29fff311xo/frwOHTqk4cOHe/+Shj0FBQUqKCiQJDVu3Fh33HGHJJ32CpBTX8//zDPPaOrUqVU7aA3x6/tGq1at9Omnn2rUqFG6/PLLdffdd6t3797ej4/u06ePSkpKdPz48UCNHNQ2bdqkrl27KiIiQvPnz9eBAwcUHh4uSXI4HCorK1Pjxo11//33a/ny5dq9e3eAJ645tm/frieeeEI5OTmnfa19+/ZavHix+vbt673M4/EoMTFRzZo1q8ox/YKw8FFeXp4SEhJ05513eh/QToqIiJDH45HT6dTjjz+u1NRUrV27Vl26dNGiRYt4jt+yk2tx1113ae/eveW+dmpUNGzYUNHR0ZKk8ePHa9KkSerfv3+VzloTnOm+ERkZqeTkZD388MM6duyY9uzZI0nesNi5c6eaN2/O7vlKOPnqj9GjR2vt2rWKi4vTq6++KmOM9//nycjo3bu3tmzZwkHMVcAYo19++UV33XWXnn32WT3//PPKz8/3fr20tFSSTguIDRs2qEWLFqpVq1aVzusXAd1fEoQ2bdpkWrRoYRo1amRSUlJMQUGB92sndz2e3P1VVFRk2rZta+rXr282b94ckHlD2fmshTHGjB071owYMcI89dRTpnbt2mb9+vWBGDfk/Xo99u7dW+7rS5cuNcnJyWbSpElm5cqVJi0tzcTFxYXErt+qtmnTJuN0Os348eONMcaUlZWZAQMGmM6dO3u3OfU+YIwxqamppnv37qasrOy0r8G+8ePHm2HDhpmoqCgzaNAgs2vXrjNut2/fPjNhwgRTr169kHmcICx84PF4zM6dO03fvn3Ntm3bTFJSkunWrZspLCw0xhizbds277Zut9uMHj3a1KlTJ2R+WKoTX9ZizJgxxuFwmIsuuoio8JNzrccPP/xgXC6XGTdunImPjzeXXnqp6dKli8nOzg7w5MFp7dq1ZuLEicaYE1FhzImf+djYWPPyyy+f8TpLliwxOTk5VTZjTXVyPUaNGmVeeukl89133xmn02mGDBliiouLzd/+9jeTm5trjDFm9erV5i9/+Ytp2bKl+eabbwI4tV283LQSevXqpRkzZqh27drq16+f4uPj1aRJE0nS66+/rosuukiSNHLkSA0dOlQdO3YM5Lgh7Wxr8dprr6lu3bqaOXOmZs6cqffee0/JyckBnji0VbQeHo9Hb7/9to4ePaqff/5ZR48eVaNGjVSvXr1AjxwSjDFyuVy6++67FRkZqfnz5yssLIyXlwbQ8uXLtXjxYr322mtat26dunfvrmbNmqmkpERffvmlEhMTlZubq+zsbLVv316JiYmBHtmeAIdNUPF4PKa0tNRcf/315pVXXjHGnHi6IzY21oSFhZmPP/44wBPWHGVlZee9Fvn5+SY/Pz9Qo9YI57pvrFixIsAT1gzvvPOOcTgc5quvvgr0KDXSqU8xffbZZ6ZVq1beVwampqaasLAwk5qaavbt23fG64QKDt48i4MHD+qnn34qd1l4eLh69uypY8eOSZJGjBghp9Op+Ph4Pf30096D02CXMUZlZWXe82FhYedci5MHTMXHxys+Pj4gc4cqX+8bGRkZ3DeqwE033aQbb7xRs2bN0i+//BLocWqE4uJiHT58WC6Xq9weotatW+uyyy5TVFSU7rnnHm3ZskVvvPGGvvzyS91///3e+0Mo7lUiLCrwww8/qHPnznrxxRe9R7if/AFo3LixsrKyNGTIEH388cf6/PPPlZWVpc2bN+vee+8t9wCIC7djxw6NGTNGN998s5566invm11JZ1+L++67z3sENuzhvlF9RUZGqmfPnlq6dKmKiooCPU7I27p1q/74xz/qd7/7nVq3bq158+Z5v9a4cWMdPnxYzZs310cffaQlS5Zo6NCh+uijj/T111+H9Dsy8+mmFfjkk0+0a9cuffDBB6pdu7buuece71sPJyUlacKECWrQoIE++ugjJSUlSZK++eYbHT9+3PsSL1y4LVu2qFevXurRo4fi4+M1bdo0GWM0efJkSVJycrImTpyoevXqnXEtTr7HCOzhvlE9mf9/75b7779fixcv9u45gn+c/FyiIUOGqFOnTtqwYYOGDRum5ORkdejQQcYYde/eXQ6HQ88//7yuuuoqlZWVqXv37srNzVXt2rUD/U/wGw7erMDmzZs1Y8YMXXbZZXr55Zf14IMPasSIEd6DzebMmaNrrrlGbdq0CeygIWzXrl26/vrrNWjQID399NOSpClTpujAgQPKzMxUrVq1VFZWptmzZ+vaa69Vu3btAjxxzcB9o3ozxujo0aPeg8hh38GDBzVo0CBdccUVeuGFF7yX9+zZU23bttXMmTMlST/++KOMMae9Z4X51Rv4hRr+nKuAMUZZWVl68803VVZWpjlz5ig6Olr/+c9/1KVLF02YMCHQI4a0srIyvfPOO0pNTdW4ceO8l+/Zs0ffffedunXrpg4dOmjgwIF66KGHAjhpzcN9o3pzOBxEhZ+VlJTo0KFDGjBggCR5P2wyMTFRBw8e9F5W0QfshXJUSIRFhU6+/CcvL0+TJk1SVFSUJkyYoIiICB7IqkB4eLjuuOMO7dmzRzExMZKkqVOn6s0339S4cePUpEkTvfXWW9q5c6eSk5P5hMwqxH0DNV2TJk30z3/+U5dddpmkE38IhYWF6eKLL1ZeXp4keY+hOHLkiOrWrRuwWQMhdI8e8UFFB5QdP35cq1atknTifd/Dw8MVFRWlzZs3n/Z23rDj1LWIj4/X1VdfLenER6IXFhbqgw8+0NSpU/Xwww/rH//4hz7//HNlZ2cHaNrQx30DOLOTUeHxeLxvw22M0YEDB7zbZGRkaM6cOTXuIPIaHxY7duxQZmZmuQ9OKikpkSR17dpVYWFhGjlypJYtW6bs7GyNHDlSTz75pBYuXMgR7padaS1OiouL07Rp0/T73/9exhh5PB6Vlpbqyiuv1MUXXxyAaUMf9w3g3MLCwsp91s3JPRWTJk3ShAkTdMMNN9S4g8hr1r/2V3JycpSSkqKff/5ZhYWFSktLU8OGDb312apVKw0ZMkRNmzbV+++/r8TERKWnpys8PFz9+/fnCHeLKloL6X8HOkVFRUk68fykw+HQwoULVatWLZ4G8QPuG8D5O/k7KiIiQgkJCXruuef07LPPav369Wrfvn2gx6tyNfZVIcXFxRo5cqQ8Ho86d+6sESNG6NFHH9Vjjz3mfUDbsWOH3nrrLd16663q0KGD9wAd2HU+a3GqrVu3asGCBZo5c6a+/PJLXg1iGfcNoHKmTZumiRMnKiYmRp9++qk6deoU6JECosbusQgLC1PHjh0VFxengQMHqmHDhrrjjjskyfsL9PLLL1d6errq1KkjKfSP5A2U81mLk3bv3q0nnnhC27Zt06pVq4gKP+C+AVROnz59NHHiRGVlZXnfw6UmqrF7LKQTf5md+rKsRYsWadCgQXrkkUf02GOPqVGjRvJ4PMrLywutD4iphs62FuPGjVNcXJzKyspUWFio48ePSxJv0+1H3DeAyvn1facmqrF7LCR5F//kS4UGDhwoY4z+9Kc/yeFwaPTo0XruueeUl5ent956y/vXGew737XYtWuXFixYENLvWlcdcN8AKqemR4VUw/dYnMoYI2OMwsLCtGjRIt1111265JJLtHPnTq1bt04dOnQI9Ig1xtnWYu3atbryyisDPWKNwn0DgC8Ii1Oc/F/hcDh0ww03KDs7WytXrlTbtm0DPFnNw1pUL6wHgPNVo58K+TWHw6GysjKNHTvW+8ZL/OIMDNaiemE9AJwvXh92BsnJydq4cSOvOKgGWIvqhfUAcC48FXIGof7Jc8GEtaheWA8A50JYAAAAa3gqBAAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAa/4PFusAbA7eHkYAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Grouping by Attribute value\n", + "value_count = df['attribute_id'].value_counts()\n", + "print(value_count)\n", + "value_count.plot(kind='bar', rot=45)" + ] + }, + { + "cell_type": "code", + "execution_count": 516, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2 9\n", + "1 5\n", + "Name: date_sighting, dtype: int64\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 516, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAhYAAAGdCAYAAABO2DpVAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8o6BhiAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAQG0lEQVR4nO3dX2jVdR/A8c9UOlnMPWrNFFdKBOafytKihCiKIizyJgoMzCCiZmZC5C4shtQSQgYl9geqXaTWjRU9ZIgwRUoqrciLNKmnRmEaxWYLTuL2XDw02KOrfvNzth19veB3cb7+fvw+QafefH/neGp6e3t7AwAgwajhHgAAOH0ICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgzZihvmFPT0/8+OOPUVtbGzU1NUN9ewBgEHp7e+Po0aMxZcqUGDVq4H2JIQ+LH3/8MRoaGob6tgBAgo6Ojpg6deqAfz7kYVFbWxsR/xts3LhxQ317AGAQurq6oqGhoe//4wMZ8rD48/HHuHHjhAUAVJm/+xiDD28CAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQZsh/Nv1MNm3Vv4d7BIbQf55dONwjAAw5OxYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkKRQWx48fj9WrV8f06dNj7NixcfHFF8eaNWuit7e3UvMBAFVkTJGT165dGxs2bIi2traYNWtWfPrpp7F06dKoq6uL5cuXV2pGAKBKFAqLDz/8MO68885YuHBhRERMmzYtNm3aFB9//HFFhgMAqkuhRyHXXXddbN++PQ4cOBAREV988UXs2rUrbrvttgGvKZfL0dXV1e8AAE5PhXYsVq1aFV1dXTFjxowYPXp0HD9+PJ5++ulYvHjxgNe0tLREc3PzKQ8KAIx8hXYs3nrrrXjjjTdi48aNsXfv3mhra4vnnnsu2traBrymqakpOjs7+46Ojo5THhoAGJkK7Vg8/vjjsWrVqrjnnnsiImLOnDnx3XffRUtLSyxZsuSk15RKpSiVSqc+KQAw4hXasfj9999j1Kj+l4wePTp6enpShwIAqlOhHYs77rgjnn766bjwwgtj1qxZ8dlnn8W6devi/vvvr9R8AEAVKRQWzz//fKxevToefvjhOHz4cEyZMiUefPDBePLJJys1HwBQRQqFRW1tbbS2tkZra2uFxgEAqpnfCgEA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0hQOix9++CHuvffemDhxYowdOzbmzJkTn376aSVmAwCqzJgiJ//666+xYMGCuPHGG+P999+P888/P77++usYP358peYDAKpIobBYu3ZtNDQ0xGuvvda3Nn369PShAIDqVOhRyLvvvhvz5s2Lu+66K+rr62Pu3LnxyiuvVGo2AKDKFAqLb775JjZs2BCXXHJJfPDBB/HQQw/F8uXLo62tbcBryuVydHV19TsAgNNToUchPT09MW/evHjmmWciImLu3Lmxb9++ePHFF2PJkiUnvaalpSWam5tPfVIAYMQrtGMxefLkmDlzZr+1Sy+9NL7//vsBr2lqaorOzs6+o6OjY3CTAgAjXqEdiwULFsT+/fv7rR04cCAuuuiiAa8plUpRKpUGNx0AUFUK7Vg89thjsXv37njmmWfi4MGDsXHjxnj55ZejsbGxUvMBAFWkUFjMnz8/tmzZEps2bYrZs2fHmjVrorW1NRYvXlyp+QCAKlLoUUhExO233x633357JWYBAKqc3woBANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANKMGe4BAE4H01b9e7hHYAj959mFwz3CiGXHAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDSnFBbPPvts1NTUxIoVK5LGAQCq2aDD4pNPPomXXnopLrvsssx5AIAqNqiw+O2332Lx4sXxyiuvxPjx47NnAgCq1KDCorGxMRYuXBg333zz355bLpejq6ur3wEAnJ7GFL1g8+bNsXfv3vjkk0/+0fktLS3R3NxceDAAoPoU2rHo6OiIRx99NN544404++yz/9E1TU1N0dnZ2Xd0dHQMalAAYOQrtGOxZ8+eOHz4cFx55ZV9a8ePH4+dO3fGCy+8EOVyOUaPHt3vmlKpFKVSKWdaAGBEKxQWN910U3z55Zf91pYuXRozZsyIJ5544oSoAADOLIXCora2NmbPnt1v7dxzz42JEyeesA4AnHn8zZsAQJrC3wr5f+3t7QljAACnAzsWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAECaQmHR0tIS8+fPj9ra2qivr49FixbF/v37KzUbAFBlCoXFjh07orGxMXbv3h3btm2LY8eOxS233BLd3d2Vmg8AqCJjipy8devWfq9ff/31qK+vjz179sT111+fOhgAUH0KhcX/6+zsjIiICRMmDHhOuVyOcrnc97qrq+tUbgkAjGCD/vBmT09PrFixIhYsWBCzZ88e8LyWlpaoq6vrOxoaGgZ7SwBghBt0WDQ2Nsa+ffti8+bNf3leU1NTdHZ29h0dHR2DvSUAMMIN6lHIsmXL4r333oudO3fG1KlT//LcUqkUpVJpUMMBANWlUFj09vbGI488Elu2bIn29vaYPn16peYCAKpQobBobGyMjRs3xjvvvBO1tbVx6NChiIioq6uLsWPHVmRAAKB6FPqMxYYNG6KzszNuuOGGmDx5ct/x5ptvVmo+AKCKFH4UAgAwEL8VAgCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkGVRYrF+/PqZNmxZnn312XHPNNfHxxx9nzwUAVKHCYfHmm2/GypUr46mnnoq9e/fG5ZdfHrfeemscPny4EvMBAFWkcFisW7cuHnjggVi6dGnMnDkzXnzxxTjnnHPi1VdfrcR8AEAVGVPk5D/++CP27NkTTU1NfWujRo2Km2++OT766KOTXlMul6NcLve97uzsjIiIrq6uwcxb1XrKvw/3CAyhM/Hf8TOZ9/eZ5Ux8f//5z9zb2/uX5xUKi59//jmOHz8ekyZN6rc+adKk+Oqrr056TUtLSzQ3N5+w3tDQUOTWUHXqWod7AqBSzuT399GjR6Ourm7APy8UFoPR1NQUK1eu7Hvd09MTv/zyS0ycODFqamoqfXuGWVdXVzQ0NERHR0eMGzduuMcBEnl/n1l6e3vj6NGjMWXKlL88r1BYnHfeeTF69Oj46aef+q3/9NNPccEFF5z0mlKpFKVSqd/av/71ryK35TQwbtw4/+GB05T395njr3Yq/lTow5tnnXVWXHXVVbF9+/a+tZ6enti+fXtce+21xScEAE4rhR+FrFy5MpYsWRLz5s2Lq6++OlpbW6O7uzuWLl1aifkAgCpSOCzuvvvuOHLkSDz55JNx6NChuOKKK2Lr1q0nfKATIv73KOypp5464XEYUP28vzmZmt6/+94IAMA/5LdCAIA0wgIASCMsAIA0wgIASCMsSNfS0hLz58+P2traqK+vj0WLFsX+/fuHeywgyc6dO+OOO+6IKVOmRE1NTbz99tvDPRIjiLAg3Y4dO6KxsTF2794d27Zti2PHjsUtt9wS3d3dwz0akKC7uzsuv/zyWL9+/XCPwgjk66ZU3JEjR6K+vj527NgR119//XCPAySqqamJLVu2xKJFi4Z7FEYIOxZUXGdnZ0RETJgwYZgnAaDShAUV1dPTEytWrIgFCxbE7Nmzh3scACqs4j+bzpmtsbEx9u3bF7t27RruUQAYAsKCilm2bFm89957sXPnzpg6depwjwPAEBAWpOvt7Y1HHnkktmzZEu3t7TF9+vThHgmAISIsSNfY2BgbN26Md955J2pra+PQoUMREVFXVxdjx44d5umAU/Xbb7/FwYMH+15/++238fnnn8eECRPiwgsvHMbJGAl83ZR0NTU1J11/7bXX4r777hvaYYB07e3tceONN56wvmTJknj99deHfiBGFGEBAKTxdVMAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADS/BfCDdsjZARtJAAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Grouping by weekday (0-indexed)\n", + "amount_per_weekday = df['date_sighting'].dt.weekday.value_counts()\n", + "print(amount_per_weekday)\n", + "amount_per_weekday.plot(kind='bar', rot=0)" + ] + }, + { + "cell_type": "code", + "execution_count": 517, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "date_sighting\n", + "9 5\n", + "14 8\n", + "15 1\n", + "Name: one, dtype: int64\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 517, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAhYAAAGxCAYAAAA+tv8YAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8o6BhiAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAeUklEQVR4nO3dfZBV9XnA8ee6KxeU3VUQBHQRRAsCASMYRnyJVhS2hNFOmraWmAXUUYsaJBjZcTQySBaaxMFm7NJkUkB8QROrJhpDhQnYaIiAwUhSUSzKqiS0FHcBk9Xu3v6RceOW17v8lt0Ln8/MmfGce849z05O9Ms5d7mZXC6XCwCABI5p7wEAgCOHsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGSKD/cJm5qa4r333ouSkpLIZDKH+/QAQCvkcrnYuXNn9OnTJ445Zt/3JQ57WLz33ntRXl5+uE8LACRQW1sbp5566j5fP+xhUVJSEhF/HKy0tPRwnx4AaIX6+vooLy9v/u/4vhz2sPj48UdpaamwAIACc6CPMfjwJgCQjLAAAJIRFgBAMsICAEhGWAAAyQgLACAZYQEAJCMsAIBkhAUAkIywAACSySssGhsb484774z+/ftHly5dYsCAATF79uzI5XJtNR8AUEDy+q6QefPmRU1NTSxevDiGDBkSa9eujcmTJ0dZWVnccsstbTUjAFAg8gqLF198Ma644ooYP358RET069cvHnnkkXjppZfaZDgAoLDk9Shk9OjRsWLFinj99dcjIuKVV16Jn/3sZ1FRUdEmwwEAhSWvOxYzZ86M+vr6GDRoUBQVFUVjY2PMmTMnJk6cuM9jGhoaoqGhoXm9vr6+9dMCAB1aXmHx2GOPxUMPPRQPP/xwDBkyJNavXx/Tpk2LPn36RGVl5V6Pqa6ujlmzZiUZFo5m/WY+094jHDHemju+vUeAI1Yml8evdJSXl8fMmTNj6tSpzdvuueeeePDBB+O1117b6zF7u2NRXl4edXV1UVpaegijw9FFWKQjLCB/9fX1UVZWdsD/fud1x+KDDz6IY45p+bGMoqKiaGpq2ucx2Ww2stlsPqcBAApUXmExYcKEmDNnTvTt2zeGDBkSv/zlL+Pee++NKVOmtNV8AEABySssvv3tb8edd94Zf//3fx/btm2LPn36xPXXXx933XVXW80HABSQvMKipKQk5s+fH/Pnz2+jcQCAQua7QgCAZIQFAJCMsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGSEBQCQjLAAAJIRFgBAMsICAEhGWAAAyQgLACAZYQEAJCMsAIBkhAUAkIywAACSERYAQDLCAgBIRlgAAMkICwAgGWEBACQjLACAZIQFAJCMsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGTyCot+/fpFJpPZY5k6dWpbzQcAFJDifHZes2ZNNDY2Nq9v2LAhLrvssvjCF76QfDAAoPDkFRY9evRosT537twYMGBAfPazn006FABQmFr9GYsPP/wwHnzwwZgyZUpkMpmUMwEABSqvOxaf9OSTT8b7778fkyZN2u9+DQ0N0dDQ0LxeX1/f2lMCAB1cq+9YfO9734uKioro06fPfverrq6OsrKy5qW8vLy1pwQAOrhWhcXbb78dy5cvj2uvvfaA+1ZVVUVdXV3zUltb25pTAgAFoFWPQhYuXBg9e/aM8ePHH3DfbDYb2Wy2NacBAApM3ncsmpqaYuHChVFZWRnFxa3+iAYAcATKOyyWL18eW7ZsiSlTprTFPABAAcv7lsPll18euVyuLWYBAAqc7woBAJIRFgBAMsICAEhGWAAAyQgLACAZYQEAJCMsAIBkhAUAkIywAACSERYAQDLCAgBIRlgAAMkICwAgGWEBACQjLACAZIQFAJCMsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGSEBQCQjLAAAJIRFgBAMsICAEhGWAAAyQgLACAZYQEAJCMsAIBkhAUAkIywAACSyTss3n333fjiF78Y3bt3jy5dusSnPvWpWLt2bVvMBgAUmOJ8dt6xY0ecf/75cckll8Szzz4bPXr0iDfeeCNOPPHEtpoPACggeYXFvHnzory8PBYuXNi8rX///smHAgAKU16PQn74wx/GyJEj4wtf+EL07NkzPv3pT8d3v/vdtpoNACgweYXFf/7nf0ZNTU2ceeaZsWzZsrjxxhvjlltuicWLF+/zmIaGhqivr2+xAABHprwehTQ1NcXIkSPj61//ekREfPrTn44NGzbEggULorKycq/HVFdXx6xZsw59UgCgw8vrjkXv3r1j8ODBLbadddZZsWXLln0eU1VVFXV1dc1LbW1t6yYFADq8vO5YnH/++bFx48YW215//fU47bTT9nlMNpuNbDbbuukAgIKS1x2LW2+9NVavXh1f//rXY9OmTfHwww/Hd77znZg6dWpbzQcAFJC8wuLcc8+NJ554Ih555JEYOnRozJ49O+bPnx8TJ05sq/kAgAKS16OQiIjPfe5z8bnPfa4tZgEACpzvCgEAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGSEBQCQjLAAAJIRFgBAMsICAEhGWAAAyQgLACAZYQEAJCMsAIBkhAUAkIywAACSERYAQDLCAgBIRlgAAMkICwAgGWEBACQjLACAZIQFAJCMsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGSEBQCQjLAAAJLJKyzuvvvuyGQyLZZBgwa11WwAQIEpzveAIUOGxPLly//0BsV5vwUAcITKuwqKi4ujV69ebTELAFDg8v6MxRtvvBF9+vSJ008/PSZOnBhbtmxpi7kAgAKU1x2LUaNGxaJFi2LgwIGxdevWmDVrVlx44YWxYcOGKCkp2esxDQ0N0dDQ0LxeX19/aBMDAB1WXmFRUVHR/M/Dhg2LUaNGxWmnnRaPPfZYXHPNNXs9prq6OmbNmnVoU7aDfjOfae8RjhhvzR3f3iMAcJgc0q+bnnDCCfFnf/ZnsWnTpn3uU1VVFXV1dc1LbW3toZwSAOjADiksdu3aFW+++Wb07t17n/tks9koLS1tsQAAR6a8wmLGjBmxatWqeOutt+LFF1+Mv/zLv4yioqK46qqr2mo+AKCA5PUZi3feeSeuuuqq2L59e/To0SMuuOCCWL16dfTo0aOt5gMACkheYbF06dK2mgMAOAL4rhAAIBlhAQAkIywAgGSEBQCQjLAAAJIRFgBAMsICAEhGWAAAyQgLACAZYQEAJCMsAIBkhAUAkIywAACSERYAQDLCAgBIRlgAAMkICwAgGWEBACQjLACAZIQFAJCMsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGSEBQCQjLAAAJIRFgBAMsICAEhGWAAAyQgLACCZQwqLuXPnRiaTiWnTpiUaBwAoZK0OizVr1sQ///M/x7Bhw1LOAwAUsFaFxa5du2LixInx3e9+N0488cTUMwEABapVYTF16tQYP358jBkzJvU8AEABK873gKVLl8bLL78ca9asOaj9GxoaoqGhoXm9vr4+31MCAAUirzsWtbW18eUvfzkeeuih6Ny580EdU11dHWVlZc1LeXl5qwYFADq+vMJi3bp1sW3btjjnnHOiuLg4iouLY9WqVfGP//iPUVxcHI2NjXscU1VVFXV1dc1LbW1tsuEBgI4lr0chl156abz66qsttk2ePDkGDRoUt99+exQVFe1xTDabjWw2e2hTAgAFIa+wKCkpiaFDh7bYdvzxx0f37t332A4AHH38zZsAQDJ5/1bI/7dy5coEYwAARwJ3LACAZIQFAJCMsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGSEBQCQjLAAAJIRFgBAMsICAEhGWAAAyQgLACAZYQEAJCMsAIBkhAUAkIywAACSERYAQDLCAgBIRlgAAMkICwAgGWEBACQjLACAZIQFAJCMsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGTyCouampoYNmxYlJaWRmlpaZx33nnx7LPPttVsAECBySssTj311Jg7d26sW7cu1q5dG3/+538eV1xxRfz6179uq/kAgAJSnM/OEyZMaLE+Z86cqKmpidWrV8eQIUOSDgYAFJ68wuKTGhsb4/vf/37s3r07zjvvvJQzAQAFKu+wePXVV+O8886LP/zhD9G1a9d44oknYvDgwfvcv6GhIRoaGprX6+vrWzcpANDh5f1bIQMHDoz169fHL37xi7jxxhujsrIyfvOb3+xz/+rq6igrK2teysvLD2lgAKDjyjssOnXqFGeccUaMGDEiqqurY/jw4XHfffftc/+qqqqoq6trXmpraw9pYACg42r1Zyw+1tTU1OJRx/+XzWYjm80e6mkAgAKQV1hUVVVFRUVF9O3bN3bu3BkPP/xwrFy5MpYtW9ZW8wEABSSvsNi2bVt86Utfiq1bt0ZZWVkMGzYsli1bFpdddllbzQcAFJC8wuJ73/teW80BABwBfFcIAJCMsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGSEBQCQjLAAAJIRFgBAMsICAEhGWAAAyQgLACAZYQEAJCMsAIBkhAUAkIywAACSERYAQDLCAgBIRlgAAMkICwAgGWEBACQjLACAZIQFAJCMsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGSEBQCQTF5hUV1dHeeee26UlJREz54948orr4yNGze21WwAQIHJKyxWrVoVU6dOjdWrV8dzzz0XH330UVx++eWxe/futpoPACggxfns/JOf/KTF+qJFi6Jnz56xbt26uOiii5IOBgAUnkP6jEVdXV1ERHTr1i3JMABAYcvrjsUnNTU1xbRp0+L888+PoUOH7nO/hoaGaGhoaF6vr69v7SkBgA6u1Xcspk6dGhs2bIilS5fud7/q6uooKytrXsrLy1t7SgCgg2tVWNx0003x9NNPx09/+tM49dRT97tvVVVV1NXVNS+1tbWtGhQA6PjyehSSy+Xi5ptvjieeeCJWrlwZ/fv3P+Ax2Ww2stlsqwcEAApHXmExderUePjhh+Opp56KkpKS+O1vfxsREWVlZdGlS5c2GRAAKBx5PQqpqamJurq6uPjii6N3797Ny6OPPtpW8wEABSTvRyEAAPviu0IAgGSEBQCQjLAAAJIRFgBAMsICAEhGWAAAyQgLACAZYQEAJCMsAIBkhAUAkIywAACSERYAQDLCAgBIRlgAAMkICwAgGWEBACQjLACAZIQFAJCMsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGSEBQCQjLAAAJIRFgBAMsICAEhGWAAAyQgLACAZYQEAJCMsAIBk8g6L559/PiZMmBB9+vSJTCYTTz75ZBuMBQAUorzDYvfu3TF8+PC4//7722IeAKCAFed7QEVFRVRUVLTFLABAgfMZCwAgmbzvWOSroaEhGhoamtfr6+vb+pQAQDtp87Corq6OWbNmtfVpAGgH/WY+094jHBHemju+vUdIps0fhVRVVUVdXV3zUltb29anBADaSZvfschms5HNZtv6NABAB5B3WOzatSs2bdrUvL558+ZYv359dOvWLfr27Zt0OACgsOQdFmvXro1LLrmkeX369OkREVFZWRmLFi1KNhgAUHjyDouLL744crlcW8wCABQ4f48FAJCMsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGSEBQCQjLAAAJIRFgBAMsICAEhGWAAAyQgLACAZYQEAJCMsAIBkhAUAkIywAACSERYAQDLCAgBIRlgAAMkICwAgGWEBACQjLACAZIQFAJCMsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGSEBQCQTKvC4v77749+/fpF586dY9SoUfHSSy+lngsAKEB5h8Wjjz4a06dPj6997Wvx8ssvx/Dhw2Ps2LGxbdu2tpgPACggeYfFvffeG9ddd11Mnjw5Bg8eHAsWLIjjjjsu/uVf/qUt5gMACkheYfHhhx/GunXrYsyYMX96g2OOiTFjxsTPf/7z5MMBAIWlOJ+d//u//zsaGxvj5JNPbrH95JNPjtdee22vxzQ0NERDQ0Pzel1dXURE1NfX5zvrYdXU8EF7j3DE6Oj/WxcK12Q6rsl0XJdpFMI1+fGMuVxuv/vlFRatUV1dHbNmzdpje3l5eVufmg6ibH57TwAtuSbpaArpmty5c2eUlZXt8/W8wuKkk06KoqKi+N3vftdi++9+97vo1avXXo+pqqqK6dOnN683NTXF//zP/0T37t0jk8nkc3o+ob6+PsrLy6O2tjZKS0vbexyICNclHY9rMp1cLhc7d+6MPn367He/vMKiU6dOMWLEiFixYkVceeWVEfHHUFixYkXcdNNNez0mm81GNpttse2EE07I57TsR2lpqf+z0OG4LuloXJNp7O9OxcfyfhQyffr0qKysjJEjR8ZnPvOZmD9/fuzevTsmT57cqiEBgCNH3mHxN3/zN/Ff//Vfcdddd8Vvf/vbOPvss+MnP/nJHh/oBACOPq368OZNN920z0cfHB7ZbDa+9rWv7fGYCdqT65KOxjV5+GVyB/q9EQCAg+RLyACAZIQFAJCMsAAAkhEWBWjnzp0xbdq0OO2006JLly4xevToWLNmTXuPxVHi+eefjwkTJkSfPn0ik8nEk08+uc99b7jhhshkMjF//vzDNh9HpwNdl5MmTYpMJtNiGTduXPsMe4QTFgXo2muvjeeeey6WLFkSr776alx++eUxZsyYePfdd9t7NI4Cu3fvjuHDh8f999+/3/2eeOKJWL169QH/lj5I4WCuy3HjxsXWrVubl0ceeeQwTnj0aPPvCiGt3//+9/H444/HU089FRdddFFERNx9993xox/9KGpqauKee+5p5wk50lVUVERFRcV+93n33Xfj5ptvjmXLlsX48eMP02QczQ7musxms/v8+gnScceiwPzv//5vNDY2RufOnVts79KlS/zsZz9rp6ngT5qamuLqq6+O2267LYYMGdLe40CzlStXRs+ePWPgwIFx4403xvbt29t7pCOSsCgwJSUlcd5558Xs2bPjvffei8bGxnjwwQfj5z//eWzdurW9x4OYN29eFBcXxy233NLeo0CzcePGxQMPPBArVqyIefPmxapVq6KioiIaGxvbe7QjjkchBWjJkiUxZcqUOOWUU6KoqCjOOeecuOqqq2LdunXtPRpHuXXr1sV9990XL7/8sm8vpkP527/92+Z//tSnPhXDhg2LAQMGxMqVK+PSSy9tx8mOPO5YFKABAwbEqlWrYteuXVFbWxsvvfRSfPTRR3H66ae392gc5f793/89tm3bFn379o3i4uIoLi6Ot99+O77yla9Ev3792ns8aHb66afHSSedFJs2bWrvUY447lgUsOOPPz6OP/742LFjRyxbtiz+4R/+ob1H4ih39dVXx5gxY1psGzt2bFx99dW+AZkO5Z133ont27dH796923uUI46wKEDLli2LXC4XAwcOjE2bNsVtt90WgwYN8i9uDotdu3a1+FPe5s2bY/369dGtW7fo27dvdO/evcX+xx57bPTq1SsGDhx4uEflKLK/67Jbt24xa9as+PznPx+9evWKN998M7761a/GGWecEWPHjm3HqY9MwqIA1dXVRVVVVbzzzjvRrVu3+PznPx9z5syJY489tr1H4yiwdu3auOSSS5rXp0+fHhERlZWVsWjRonaaiqPd/q7Lmpqa+NWvfhWLFy+O999/P/r06ROXX355zJ4927eetgHfbgoAJOPDmwBAMsICAEhGWAAAyQgLACAZYQEAJCMsAIBkhAUAkIywAACSERZQQC6++OKYNm1ae4+xV2+99VZkMplYv379QR+zaNGiOOGEEw7LuYDDQ1jAEWrlypWRyWTi/fffPyznKy8vj61bt8bQoUOTvu+kSZPiyiuvPCznAg6d7woBkigqKopevXodcecC8uOOBXRQu3fvji996UvRtWvX6N27d3zrW99q8fqSJUti5MiRUVJSEr169Yq/+7u/i23btkXEHx8VfPyFTCeeeGJkMpmYNGlSREQ0NTVFdXV19O/fP7p06RLDhw+PH/zgBwc1044dO2LixInRo0eP6NKlS5x55pmxcOHC5nP+/8cTP/zhD+PMM8+Mzp07xyWXXBKLFy/e612UZcuWxVlnnRVdu3aNcePGxdatWyMi4u67747FixfHU089FZlMJjKZTKxcuXKPc318d2bFihUxcuTIOO6442L06NGxcePGFue55557omfPnlFSUhLXXnttzJw5M84+++yD+tmBgyMsoIO67bbbYtWqVfHUU0/Fv/3bv8XKlSvj5Zdfbn79o48+itmzZ8crr7wSTz75ZLz11lvN8VBeXh6PP/54RERs3Lgxtm7dGvfdd19ERFRXV8cDDzwQCxYsiF//+tdx6623xhe/+MVYtWrVAWe688474ze/+U08++yz8R//8R9RU1MTJ5100l733bx5c/zVX/1VXHnllfHKK6/E9ddfH3fcccce+33wwQfxzW9+M5YsWRLPP/98bNmyJWbMmBERETNmzIi//uu/bo6NrVu3xujRo/c53x133BHf+ta3Yu3atVFcXBxTpkxpfu2hhx6KOXPmxLx582LdunXRt2/fqKmpOeDPDOQpB3Q4O3fuzHXq1Cn32GOPNW/bvn17rkuXLrkvf/nLez1mzZo1uYjI7dy5M5fL5XI//elPcxGR27FjR/M+f/jDH3LHHXdc7sUXX2xx7DXXXJO76qqrDjjXhAkTcpMnT97ra5s3b85FRO6Xv/xlLpfL5W6//fbc0KFDW+xzxx13tJhp4cKFuYjIbdq0qXmf+++/P3fyySc3r1dWVuauuOKK/Z7r4591+fLlzfs888wzuYjI/f73v8/lcrncqFGjclOnTm3xPueff35u+PDhB/y5gYPnjgV0QG+++WZ8+OGHMWrUqOZt3bp1i4EDBzavr1u3LiZMmBB9+/aNkpKS+OxnPxsREVu2bNnn+27atCk++OCDuOyyy6Jr167NywMPPBBvvvnmAee68cYbY+nSpXH22WfHV7/61XjxxRf3ue/GjRvj3HPPbbHtM5/5zB77HXfccTFgwIDm9d69ezc/0snXsGHDWrxPRDS/18aNG/c4/97mAQ6ND29CAdq9e3eMHTs2xo4dGw899FD06NEjtmzZEmPHjo0PP/xwn8ft2rUrIiKeeeaZOOWUU1q8ls1mD3jeioqKePvtt+PHP/5xPPfcc3HppZfG1KlT45vf/Garf5Zjjz22xXomk4lcLnfI75XJZCLij58pAQ4fdyygAxowYEAce+yx8Ytf/KJ5244dO+L111+PiIjXXnsttm/fHnPnzo0LL7wwBg0atMef8jt16hQREY2Njc3bBg8eHNlsNrZs2RJnnHFGi6W8vPygZuvRo0dUVlbGgw8+GPPnz4/vfOc7e91v4MCBsXbt2hbb1qxZc1Dn+P8/xyd/htYaOHDgHudvzTzA/rljAR1Q165d45prronbbrstunfvHj179ow77rgjjjnmj38W6Nu3b3Tq1Cm+/e1vxw033BAbNmyI2bNnt3iP0047LTKZTDz99NPxF3/xF9GlS5coKSmJGTNmxK233hpNTU1xwQUXRF1dXbzwwgtRWloalZWV+53rrrvuihEjRsSQIUOioaEhnn766TjrrLP2uu/1118f9957b9x+++1xzTXXxPr162PRokUR8ae7CQejX79+sWzZsti4cWN07949ysrKDvrYT7r55pvjuuuui5EjR8bo0aPj0UcfjV/96ldx+umnt+r9gL1zxwI6qG984xtx4YUXxoQJE2LMmDFxwQUXxIgRIyLij3cNFi1aFN///vdj8ODBMXfu3D0eR5xyyikxa9asmDlzZpx88slx0003RUTE7Nmz484774zq6uo466yzYty4cfHMM89E//79DzhTp06doqqqKoYNGxYXXXRRFBUVxdKlS/e6b//+/eMHP/hB/Ou//msMGzYsampqmn8r5GAeu3zsuuuui4EDB8bIkSOjR48e8cILLxz0sZ80ceLEqKqqihkzZsQ555wTmzdvjkmTJkXnzp1b9X7A3mVyrX2YCZCnOXPmxIIFC6K2tra9R4mIiMsuuyx69eoVS5Ysae9R4IjhUQjQZv7pn/4pzj333OjevXu88MIL8Y1vfKP5zsnh9sEHH8SCBQti7NixUVRUFI888kgsX748nnvuuXaZB45UHoUAzW644YYWv4b6yeWGG27I+/3eeOONuOKKK2Lw4MExe/bs+MpXvhJ33313+sEPQiaTiR//+Mdx0UUXxYgRI+JHP/pRPP744zFmzJh2mQeOVB6FAM22bdsW9fX1e32ttLQ0evbseZgnAgqNsAAAkvEoBABIRlgAAMkICwAgGWEBACQjLACAZIQFAJCMsAAAkhEWAEAy/weQUvStHTG7NQAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "amount_per_weekday_for_each_attribute = df.groupby([df['date_sighting'].dt.hour])['one'].sum()\n", + "print(amount_per_weekday_for_each_attribute)\n", + "amount_per_weekday_for_each_attribute.plot(kind='bar', rot=0)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.8.10 ('venv': venv)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "99e19f785595e5572f3a0434505ffd496bc893a60c3b4501be593ee9ddcf6bde" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/add_attributes_from_csv.py b/examples/add_attributes_from_csv.py new file mode 100644 index 0000000..e05bb1b --- /dev/null +++ b/examples/add_attributes_from_csv.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import csv +from pymisp import PyMISP +from pymisp import ExpandedPyMISP, MISPAttribute +from keys import misp_url, misp_key, misp_verifycert +from requests.packages.urllib3.exceptions import InsecureRequestWarning +import argparse +import urllib3 +import requests +requests.packages.urllib3.disable_warnings() + + +""" + +Sample usage: + +python3 add_filetype_object_from_csv.py -e -f .csv + + +Attribute CSV file (aach line is an entry): + +value;category;type;comment;to_ids;first_seen;last_seen;tag1;tag2 +test.pdf;Payload delivery;filename;Email attachment;0;1970-01-01;1970-01-01;tlp:green;ransomware +127.0.0.1;Network activity;ip-dst;C2 server;1;;;tlp:white; + +value = IOC's value +category = its MISP category (https://www.circl.lu/doc/misp/categories-and-types/) +type = its MISP type (https://www.circl.lu/doc/misp/categories-and-types/) +comment = IOC's description +to_ids = Boolean expected (0 = IDS flag not checked // 1 = IDS flag checked) +first_seen = First seen date, if any (left empty if not) +last_seen = Last seen date, if any (left empty if not) +tag = IOC tag, if any + +""" + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Add attributes to a MISP event from a semi-colon formated csv file') + parser.add_argument("-e", "--event_uuid", required=True, help="Event UUID to update") + parser.add_argument("-f", "--attr_file", required=True, help="Attribute CSV file path") + args = parser.parse_args() + + pymisp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert) + + f = open(args.attr_file, newline='') + csv_reader = csv.reader(f, delimiter=";") + + for line in csv_reader: + value = line[0] + category = line[1] + type = line[2] + comment = line[3] + ids = line[4] + fseen = line[5] + lseen = line[6] + tags = line[7:] + + misp_attribute = MISPAttribute() + misp_attribute.value = str(value) + misp_attribute.category = str(category) + misp_attribute.type = str(type) + misp_attribute.comment = str(comment) + misp_attribute.to_ids = str(ids) + if fseen != '': + misp_attribute.first_seen = str(fseen) + if lseen != '': + misp_attribute.last_seen = str(lseen) + for x in tags: + misp_attribute.add_tag(x) + r = pymisp.add_attribute(args.event_uuid, misp_attribute) + print(line) + print("\nAttributes successfully saved :)") diff --git a/examples/add_email_object.py b/examples/add_email_object.py index 756a562..5f190ae 100755 --- a/examples/add_email_object.py +++ b/examples/add_email_object.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from pymisp import ExpandedPyMISP +from pymisp import PyMISP from pymisp.tools import EMailObject import traceback from keys import misp_url, misp_key, misp_verifycert # type: ignore @@ -15,7 +14,7 @@ if __name__ == '__main__': parser.add_argument("-p", "--path", required=True, help="Path to process (expanded using glob).") args = parser.parse_args() - pymisp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert, debug=True) + pymisp = PyMISP(misp_url, misp_key, misp_verifycert, debug=True) for f in glob.glob(args.path): try: diff --git a/examples/feed-generator/settings.default.py b/examples/feed-generator/settings.default.py index 408b6c8..3b994e8 100755 --- a/examples/feed-generator/settings.default.py +++ b/examples/feed-generator/settings.default.py @@ -16,7 +16,7 @@ outputdir = 'output' # you can use on the event index, such as organisation, tags, etc. # It uses the same joining and condition rules as the API parameters # For example: -# filters = {'tag':'tlp:white|feed-export|!privint','org':'CIRCL', 'published':1} +# filters = {'tags':['tlp:white','feed-export','!privint'],'org':'CIRCL', 'published':1} # the above would generate a feed for all published events created by CIRCL, # tagged tlp:white and/or feed-export but exclude anything tagged privint filters = {'published':'true'} @@ -53,4 +53,4 @@ exclude_attribute_types = [] with_distribution = False # Include the exportable local tags along with the global tags. The default is True. -with_local_tags = True \ No newline at end of file +with_local_tags = True diff --git a/examples/load_csv.py b/examples/load_csv.py index 580791a..7732196 100755 --- a/examples/load_csv.py +++ b/examples/load_csv.py @@ -10,7 +10,7 @@ from pymisp import MISPEvent try: from keys import misp_url, misp_key, misp_verifycert - from pymisp import ExpandedPyMISP + from pymisp import PyMISP offline = False except ImportError as e: offline = True @@ -66,7 +66,7 @@ if __name__ == '__main__': if offline: print('You are in offline mode, quitting.') else: - misp = ExpandedPyMISP(url=misp_url, key=misp_key, ssl=misp_verifycert) + misp = PyMISP(url=misp_url, key=misp_key, ssl=misp_verifycert) if args.new_event: event = MISPEvent() event.info = args.new_event @@ -80,7 +80,7 @@ if __name__ == '__main__': else: print('Something went wrong:') print(new_event) - else: + elif args.update_event: for o in objects: new_object = misp.add_object(args.update_event, o, pythonify=True) if isinstance(new_object, str): @@ -90,3 +90,5 @@ if __name__ == '__main__': else: print('Something went wrong:') print(new_event) + else: + print('you need to pass either a event info field (flag -i), or the event ID you want to update (flag -u)') diff --git a/mypy.ini b/mypy.ini index 78ad923..3f7aec0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,6 +1,15 @@ [mypy] -ignore_errors = False - +strict = True +warn_return_any = False show_error_context = True pretty = True -exclude = pymisp/data|example|docs +exclude = tests/testlive_comprehensive.py|tests/testlive_sync.py|feed-generator|examples|pymisp/data|docs|pymisp/tools/openioc.py|pymisp/tools/reportlab_generator.py|tests/test_reportlab.py + +# Stuff to remove gradually +disallow_untyped_defs = False +disallow_untyped_calls = False +disable_error_code = arg-type,return-value,assignment,call-overload,union-attr + + +[mypy-docs.source.*] +ignore_errors = True diff --git a/poetry.lock b/poetry.lock index b95c4d2..0137565 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,1844 +1,76 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + [[package]] name = "alabaster" -version = "0.7.12" -description = "A configurable sidebar-enabled Sphinx theme" -category = "main" +version = "0.7.16" +description = "A light, configurable Sphinx theme" optional = true -python-versions = "*" +python-versions = ">=3.9" +files = [ + {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, + {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, +] [[package]] name = "anyio" -version = "3.6.2" +version = "4.4.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "dev" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.8" +files = [ + {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, + {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, +] [package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] -doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] -trio = ["trio (>=0.16,<0.22)"] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] [[package]] name = "appnope" -version = "0.1.3" +version = "0.1.4" description = "Disable App Nap on macOS >= 10.9" -category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" +files = [ + {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, + {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, +] [[package]] name = "argon2-cffi" -version = "21.3.0" -description = "The secure Argon2 password hashing algorithm." -category = "dev" +version = "23.1.0" +description = "Argon2 for Python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +files = [ + {file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"}, + {file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"}, +] [package.dependencies] argon2-cffi-bindings = "*" -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] -dev = ["cogapp", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "pre-commit", "pytest", "sphinx", "sphinx-notfound-page", "tomli"] -docs = ["furo", "sphinx", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pytest"] +dev = ["argon2-cffi[tests,typing]", "tox (>4)"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-copybutton", "sphinx-notfound-page"] +tests = ["hypothesis", "pytest"] +typing = ["mypy"] [[package]] name = "argon2-cffi-bindings" version = "21.2.0" description = "Low-level CFFI bindings for Argon2" -category = "dev" optional = false python-versions = ">=3.6" - -[package.dependencies] -cffi = ">=1.0.1" - -[package.extras] -dev = ["cogapp", "pre-commit", "pytest", "wheel"] -tests = ["pytest"] - -[[package]] -name = "attrs" -version = "22.1.0" -description = "Classes Without Boilerplate" -category = "main" -optional = false -python-versions = ">=3.5" - -[package.extras] -dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] -docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] -tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] - -[[package]] -name = "babel" -version = "2.11.0" -description = "Internationalization utilities" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -pytz = ">=2015.7" - -[[package]] -name = "backcall" -version = "0.2.0" -description = "Specifications for callback functions passed in to an API" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "backports-zoneinfo" -version = "0.2.1" -description = "Backport of the standard library zoneinfo module" -category = "main" -optional = true -python-versions = ">=3.6" - -[package.extras] -tzdata = ["tzdata"] - -[[package]] -name = "beautifulsoup4" -version = "4.11.1" -description = "Screen-scraping library" -category = "main" -optional = false -python-versions = ">=3.6.0" - -[package.dependencies] -soupsieve = ">1.2" - -[package.extras] -html5lib = ["html5lib"] -lxml = ["lxml"] - -[[package]] -name = "bleach" -version = "5.0.1" -description = "An easy safelist-based HTML-sanitizing tool." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -six = ">=1.9.0" -webencodings = "*" - -[package.extras] -css = ["tinycss2 (>=1.1.0,<1.2)"] -dev = ["Sphinx (==4.3.2)", "black (==22.3.0)", "build (==0.8.0)", "flake8 (==4.0.1)", "hashin (==0.17.0)", "mypy (==0.961)", "pip-tools (==6.6.2)", "pytest (==7.1.2)", "tox (==3.25.0)", "twine (==4.0.1)", "wheel (==0.37.1)"] - -[[package]] -name = "brotli" -version = "1.0.9" -description = "Python bindings for the Brotli compression library" -category = "main" -optional = true -python-versions = "*" - -[[package]] -name = "brotlicffi" -version = "1.0.9.2" -description = "Python CFFI bindings to the Brotli library" -category = "main" -optional = true -python-versions = "*" - -[package.dependencies] -cffi = ">=1.0.0" - -[[package]] -name = "certifi" -version = "2022.9.24" -description = "Python package for providing Mozilla's CA Bundle." -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "cffi" -version = "1.15.1" -description = "Foreign Function Interface for Python calling C code." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -pycparser = "*" - -[[package]] -name = "chardet" -version = "5.0.0" -description = "Universal encoding detector for Python 3" -category = "main" -optional = true -python-versions = ">=3.6" - -[[package]] -name = "charset-normalizer" -version = "2.1.1" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" -optional = false -python-versions = ">=3.6.0" - -[package.extras] -unicode-backport = ["unicodedata2"] - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" - -[[package]] -name = "colorclass" -version = "2.2.2" -description = "Colorful worry-free console applications for Linux, Mac OS X, and Windows." -category = "main" -optional = true -python-versions = ">=2.6" - -[[package]] -name = "commonmark" -version = "0.9.1" -description = "Python parser for the CommonMark Markdown spec" -category = "main" -optional = true -python-versions = "*" - -[package.extras] -test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] - -[[package]] -name = "compressed-rtf" -version = "1.0.6" -description = "Compressed Rich Text Format (RTF) compression and decompression package" -category = "main" -optional = true -python-versions = "*" - -[[package]] -name = "coverage" -version = "6.5.0" -description = "Code coverage measurement for Python" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "cryptography" -version = "38.0.4" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" -optional = true -python-versions = ">=3.6" - -[package.dependencies] -cffi = ">=1.12" - -[package.extras] -docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] -docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] -pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] -sdist = ["setuptools-rust (>=0.11.4)"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] - -[[package]] -name = "debugpy" -version = "1.6.3" -description = "An implementation of the Debug Adapter Protocol for Python" -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "decorator" -version = "5.1.1" -description = "Decorators for Humans" -category = "main" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "defusedxml" -version = "0.7.1" -description = "XML bomb protection for Python stdlib modules" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "deprecated" -version = "1.2.13" -description = "Python @deprecated decorator to deprecate old python classes, functions or methods." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -wrapt = ">=1.10,<2" - -[package.extras] -dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"] - -[[package]] -name = "docutils" -version = "0.19" -description = "Docutils -- Python Documentation Utilities" -category = "main" -optional = true -python-versions = ">=3.7" - -[[package]] -name = "easygui" -version = "0.98.3" -description = "EasyGUI is a module for very simple, very easy GUI programming in Python. EasyGUI is different from other GUI generators in that EasyGUI is NOT event-driven. Instead, all GUI interactions are invoked by simple function calls." -category = "main" -optional = true -python-versions = "*" - -[[package]] -name = "ebcdic" -version = "1.1.1" -description = "Additional EBCDIC codecs" -category = "main" -optional = true -python-versions = "*" - -[[package]] -name = "entrypoints" -version = "0.4" -description = "Discover and load entry points from installed packages." -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "exceptiongroup" -version = "1.0.4" -description = "Backport of PEP 654 (exception groups)" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "extract-msg" -version = "0.37.1" -description = "Extracts emails and attachments saved in Microsoft Outlook's .msg files" -category = "main" -optional = true -python-versions = ">=3.6" - -[package.dependencies] -beautifulsoup4 = ">=4.11.1" -chardet = ">=4.0.0" -compressed-rtf = ">=1.0.6" -ebcdic = ">=1.1.1" -imapclient = ">=2.1.0" -olefile = ">=0.46" -RTFDE = ">=0.0.2" -tzlocal = ">=4.2" - -[package.extras] -all = ["extract-msg[mime]"] -mime = ["python-magic (>=0.4.27,<0.5.0)"] - -[[package]] -name = "fastjsonschema" -version = "2.16.2" -description = "Fastest Python implementation of JSON schema" -category = "dev" -optional = false -python-versions = "*" - -[package.extras] -devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] - -[[package]] -name = "idna" -version = "3.4" -description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "imagesize" -version = "1.4.1" -description = "Getting image size from png/jpeg/jpeg2000/gif file" -category = "main" -optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "imapclient" -version = "2.3.1" -description = "Easy-to-use, Pythonic and complete IMAP client library" -category = "main" -optional = true -python-versions = "*" - -[package.dependencies] -six = "*" - -[package.extras] -doc = ["sphinx"] -test = ["mock (>=1.3.0)"] - -[[package]] -name = "importlib-metadata" -version = "5.1.0" -description = "Read metadata from Python packages" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] -perf = ["ipython"] -testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] - -[[package]] -name = "importlib-resources" -version = "5.10.0" -description = "Read resources from Python packages" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] -testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - -[[package]] -name = "iniconfig" -version = "1.1.1" -description = "iniconfig: brain-dead simple config-ini parsing" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "ipykernel" -version = "6.16.2" -description = "IPython Kernel for Jupyter" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -appnope = {version = "*", markers = "platform_system == \"Darwin\""} -debugpy = ">=1.0" -ipython = ">=7.23.1" -jupyter-client = ">=6.1.12" -matplotlib-inline = ">=0.1" -nest-asyncio = "*" -packaging = "*" -psutil = "*" -pyzmq = ">=17" -tornado = ">=6.1" -traitlets = ">=5.1.0" - -[package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt"] -test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-cov", "pytest-timeout"] - -[[package]] -name = "ipython" -version = "7.34.0" -description = "IPython: Productive Interactive Computing" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -appnope = {version = "*", markers = "sys_platform == \"darwin\""} -backcall = "*" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -decorator = "*" -jedi = ">=0.16" -matplotlib-inline = "*" -pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} -pickleshare = "*" -prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" -pygments = "*" -setuptools = ">=18.5" -traitlets = ">=4.2" - -[package.extras] -all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.17)", "pygments", "qtconsole", "requests", "testpath"] -doc = ["Sphinx (>=1.3)"] -kernel = ["ipykernel"] -nbconvert = ["nbconvert"] -nbformat = ["nbformat"] -notebook = ["ipywidgets", "notebook"] -parallel = ["ipyparallel"] -qtconsole = ["qtconsole"] -test = ["ipykernel", "nbformat", "nose (>=0.10.1)", "numpy (>=1.17)", "pygments", "requests", "testpath"] - -[[package]] -name = "ipython-genutils" -version = "0.2.0" -description = "Vestigial utilities from IPython" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "jedi" -version = "0.18.2" -description = "An autocompletion tool for Python that can be used for text editors." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -parso = ">=0.8.0,<0.9.0" - -[package.extras] -docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] -qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] -testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] - -[[package]] -name = "jinja2" -version = "3.1.2" -description = "A very fast and expressive template engine." -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "json5" -version = "0.9.10" -description = "A Python implementation of the JSON5 data format." -category = "dev" -optional = false -python-versions = "*" - -[package.extras] -dev = ["hypothesis"] - -[[package]] -name = "jsonschema" -version = "4.17.1" -description = "An implementation of JSON Schema validation for Python" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -attrs = ">=17.4.0" -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} -importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} -pkgutil-resolve-name = {version = ">=1.3.10", markers = "python_version < \"3.9\""} -pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2" -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - -[package.extras] -format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] - -[[package]] -name = "jupyter-client" -version = "7.4.7" -description = "Jupyter protocol implementation and client libraries" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -entrypoints = "*" -jupyter-core = ">=4.9.2" -nest-asyncio = ">=1.5.4" -python-dateutil = ">=2.8.2" -pyzmq = ">=23.0" -tornado = ">=6.2" -traitlets = "*" - -[package.extras] -doc = ["ipykernel", "myst-parser", "sphinx (>=1.3.6)", "sphinx-rtd-theme", "sphinxcontrib-github-alt"] -test = ["codecov", "coverage", "ipykernel (>=6.12)", "ipython", "mypy", "pre-commit", "pytest", "pytest-asyncio (>=0.18)", "pytest-cov", "pytest-timeout"] - -[[package]] -name = "jupyter-core" -version = "4.11.2" -description = "Jupyter core package. A base package on which Jupyter projects rely." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -pywin32 = {version = ">=1.0", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} -traitlets = "*" - -[package.extras] -test = ["ipykernel", "pre-commit", "pytest", "pytest-cov", "pytest-timeout"] - -[[package]] -name = "jupyter-server" -version = "1.23.3" -description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -anyio = ">=3.1.0,<4" -argon2-cffi = "*" -jinja2 = "*" -jupyter-client = ">=6.1.12" -jupyter-core = ">=4.7.0" -nbconvert = ">=6.4.4" -nbformat = ">=5.2.0" -packaging = "*" -prometheus-client = "*" -pywinpty = {version = "*", markers = "os_name == \"nt\""} -pyzmq = ">=17" -Send2Trash = "*" -terminado = ">=0.8.3" -tornado = ">=6.1.0" -traitlets = ">=5.1" -websocket-client = "*" - -[package.extras] -test = ["coverage", "ipykernel", "pre-commit", "pytest (>=7.0)", "pytest-console-scripts", "pytest-cov", "pytest-mock", "pytest-timeout", "pytest-tornasync", "requests"] - -[[package]] -name = "jupyterlab" -version = "3.5.0" -description = "JupyterLab computational environment" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -ipython = "*" -jinja2 = ">=2.1" -jupyter-core = "*" -jupyter-server = ">=1.16.0,<3" -jupyterlab-server = ">=2.10,<3.0" -nbclassic = "*" -notebook = "<7" -packaging = "*" -tomli = "*" -tornado = ">=6.1.0" - -[package.extras] -test = ["check-manifest", "coverage", "jupyterlab-server[test]", "pre-commit", "pytest (>=6.0)", "pytest-check-links (>=0.5)", "pytest-console-scripts", "pytest-cov", "requests", "requests-cache", "virtualenv"] -ui-tests = ["build"] - -[[package]] -name = "jupyterlab-pygments" -version = "0.2.2" -description = "Pygments theme using JupyterLab CSS variables" -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "jupyterlab-server" -version = "2.16.3" -description = "A set of server components for JupyterLab and JupyterLab like applications." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -babel = "*" -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} -jinja2 = ">=3.0.3" -json5 = "*" -jsonschema = ">=3.0.1" -jupyter-server = ">=1.8,<3" -packaging = "*" -requests = "*" - -[package.extras] -docs = ["autodoc-traits", "docutils (<0.19)", "jinja2 (<3.1.0)", "mistune (<1)", "myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-copybutton", "sphinxcontrib-openapi"] -openapi = ["openapi-core (>=0.14.2)", "ruamel-yaml"] -test = ["codecov", "ipykernel", "jupyter-server[test]", "openapi-core (>=0.14.2,<0.15.0)", "openapi-spec-validator (<0.5)", "pytest (>=7.0)", "pytest-console-scripts", "pytest-cov", "requests-mock", "ruamel-yaml", "strict-rfc3339"] - -[[package]] -name = "lark-parser" -version = "0.12.0" -description = "a modern parsing library" -category = "main" -optional = true -python-versions = "*" - -[package.extras] -atomic-cache = ["atomicwrites"] -nearley = ["js2py"] -regex = ["regex"] - -[[package]] -name = "lief" -version = "0.12.3" -description = "Library to instrument executable formats" -category = "main" -optional = true -python-versions = ">=3.6" - -[[package]] -name = "markupsafe" -version = "2.1.1" -description = "Safely add untrusted strings to HTML/XML markup." -category = "main" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "matplotlib-inline" -version = "0.1.6" -description = "Inline Matplotlib backend for Jupyter" -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.dependencies] -traitlets = "*" - -[[package]] -name = "mistune" -version = "2.0.4" -description = "A sane Markdown parser with useful plugins and renderers" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "msoffcrypto-tool" -version = "5.0.0" -description = "Python tool and library for decrypting MS Office files with passwords or other keys" -category = "main" -optional = true -python-versions = ">=3.6,<4.0" - -[package.dependencies] -cryptography = ">=2.3" -olefile = ">=0.45" - -[[package]] -name = "mypy" -version = "0.991" -description = "Optional static typing for Python" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -mypy-extensions = ">=0.4.3" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} -typing-extensions = ">=3.10" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -install-types = ["pip"] -python2 = ["typed-ast (>=1.4.0,<2)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "0.4.3" -description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "nbclassic" -version = "0.4.8" -description = "A web-based notebook environment for interactive computing" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -argon2-cffi = "*" -ipykernel = "*" -ipython-genutils = "*" -jinja2 = "*" -jupyter-client = ">=6.1.1" -jupyter-core = ">=4.6.1" -jupyter-server = ">=1.8" -nbconvert = ">=5" -nbformat = "*" -nest-asyncio = ">=1.5" -notebook-shim = ">=0.1.0" -prometheus-client = "*" -pyzmq = ">=17" -Send2Trash = ">=1.8.0" -terminado = ">=0.8.3" -tornado = ">=6.1" -traitlets = ">=4.2.1" - -[package.extras] -docs = ["myst-parser", "nbsphinx", "sphinx", "sphinx-rtd-theme", "sphinxcontrib-github-alt"] -json-logging = ["json-logging"] -test = ["coverage", "nbval", "pytest", "pytest-cov", "pytest-playwright", "pytest-tornasync", "requests", "requests-unixsocket", "testpath"] - -[[package]] -name = "nbclient" -version = "0.7.0" -description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." -category = "dev" -optional = false -python-versions = ">=3.7.0" - -[package.dependencies] -jupyter-client = ">=6.1.5" -nbformat = ">=5.0" -nest-asyncio = "*" -traitlets = ">=5.2.2" - -[package.extras] -sphinx = ["Sphinx (>=1.7)", "autodoc-traits", "mock", "moto", "myst-parser", "sphinx-book-theme"] -test = ["black", "check-manifest", "flake8", "ipykernel", "ipython", "ipywidgets", "mypy", "nbconvert", "pip (>=18.1)", "pre-commit", "pytest (>=4.1)", "pytest-asyncio", "pytest-cov (>=2.6.1)", "setuptools (>=60.0)", "testpath", "twine (>=1.11.0)", "xmltodict"] - -[[package]] -name = "nbconvert" -version = "7.2.5" -description = "Converting Jupyter Notebooks" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -beautifulsoup4 = "*" -bleach = "*" -defusedxml = "*" -importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} -jinja2 = ">=3.0" -jupyter-core = ">=4.7" -jupyterlab-pygments = "*" -markupsafe = ">=2.0" -mistune = ">=2.0.3,<3" -nbclient = ">=0.5.0" -nbformat = ">=5.1" -packaging = "*" -pandocfilters = ">=1.4.1" -pygments = ">=2.4.1" -tinycss2 = "*" -traitlets = ">=5.0" - -[package.extras] -all = ["ipykernel", "ipython", "ipywidgets (>=7)", "myst-parser", "nbsphinx (>=0.2.12)", "pre-commit", "pyppeteer (>=1,<1.1)", "pyqtwebengine (>=5.15)", "pytest", "pytest-cov", "pytest-dependency", "sphinx (==5.0.2)", "sphinx-rtd-theme", "tornado (>=6.1)"] -docs = ["ipython", "myst-parser", "nbsphinx (>=0.2.12)", "sphinx (==5.0.2)", "sphinx-rtd-theme"] -qtpdf = ["pyqtwebengine (>=5.15)"] -qtpng = ["pyqtwebengine (>=5.15)"] -serve = ["tornado (>=6.1)"] -test = ["ipykernel", "ipywidgets (>=7)", "pre-commit", "pyppeteer (>=1,<1.1)", "pytest", "pytest-cov", "pytest-dependency"] -webpdf = ["pyppeteer (>=1,<1.1)"] - -[[package]] -name = "nbformat" -version = "5.7.0" -description = "The Jupyter Notebook format" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -fastjsonschema = "*" -importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.8\""} -jsonschema = ">=2.6" -jupyter-core = "*" -traitlets = ">=5.1" - -[package.extras] -test = ["check-manifest", "pep440", "pre-commit", "pytest", "testpath"] - -[[package]] -name = "nest-asyncio" -version = "1.5.6" -description = "Patch asyncio to allow nested event loops" -category = "dev" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "notebook" -version = "6.5.2" -description = "A web-based notebook environment for interactive computing" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -argon2-cffi = "*" -ipykernel = "*" -ipython-genutils = "*" -jinja2 = "*" -jupyter-client = ">=5.3.4" -jupyter-core = ">=4.6.1" -nbclassic = ">=0.4.7" -nbconvert = ">=5" -nbformat = "*" -nest-asyncio = ">=1.5" -prometheus-client = "*" -pyzmq = ">=17" -Send2Trash = ">=1.8.0" -terminado = ">=0.8.3" -tornado = ">=6.1" -traitlets = ">=4.2.1" - -[package.extras] -docs = ["myst-parser", "nbsphinx", "sphinx", "sphinx-rtd-theme", "sphinxcontrib-github-alt"] -json-logging = ["json-logging"] -test = ["coverage", "nbval", "pytest", "pytest-cov", "requests", "requests-unixsocket", "selenium (==4.1.5)", "testpath"] - -[[package]] -name = "notebook-shim" -version = "0.2.2" -description = "A shim layer for notebook traits and config" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -jupyter-server = ">=1.8,<3" - -[package.extras] -test = ["pytest", "pytest-console-scripts", "pytest-tornasync"] - -[[package]] -name = "olefile" -version = "0.46" -description = "Python package to parse, read and write Microsoft OLE2 files (Structured Storage or Compound Document, Microsoft Office)" -category = "main" -optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "oletools" -version = "0.60.1" -description = "Python tools to analyze security characteristics of MS Office and OLE files (also called Structured Storage, Compound File Binary Format or Compound Document File Format), for Malware Analysis and Incident Response #DFIR" -category = "main" -optional = true -python-versions = "*" - -[package.dependencies] -colorclass = "*" -easygui = "*" -msoffcrypto-tool = {version = "*", markers = "platform_python_implementation != \"PyPy\" or python_version >= \"3\" and platform_system != \"Windows\" and platform_system != \"Darwin\""} -olefile = ">=0.46" -pcodedmp = ">=1.2.5" -pyparsing = ">=2.1.0,<3" - -[package.extras] -full = ["XLMMacroDeobfuscator"] - -[[package]] -name = "packaging" -version = "21.3" -description = "Core utilities for Python packages" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" - -[[package]] -name = "pandocfilters" -version = "1.5.0" -description = "Utilities for writing pandoc filters in python" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "parso" -version = "0.8.3" -description = "A Python Parser" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] -testing = ["docopt", "pytest (<6.0.0)"] - -[[package]] -name = "pcodedmp" -version = "1.2.6" -description = "A VBA p-code disassembler" -category = "main" -optional = true -python-versions = "*" - -[package.dependencies] -oletools = ">=0.54" -win-unicode-console = {version = "*", markers = "platform_system == \"Windows\" and platform_python_implementation != \"PyPy\""} - -[[package]] -name = "pexpect" -version = "4.8.0" -description = "Pexpect allows easy control of interactive console applications." -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -ptyprocess = ">=0.5" - -[[package]] -name = "pickleshare" -version = "0.7.5" -description = "Tiny 'shelve'-like database with concurrency support" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "pillow" -version = "9.3.0" -description = "Python Imaging Library (Fork)" -category = "main" -optional = true -python-versions = ">=3.7" - -[package.extras] -docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] -tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] - -[[package]] -name = "pkgutil-resolve-name" -version = "1.3.10" -description = "Resolve a name to an object." -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "pluggy" -version = "1.0.0" -description = "plugin and hook calling mechanisms for python" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "prometheus-client" -version = "0.15.0" -description = "Python client for the Prometheus monitoring system." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -twisted = ["twisted"] - -[[package]] -name = "prompt-toolkit" -version = "3.0.33" -description = "Library for building powerful interactive command lines in Python" -category = "dev" -optional = false -python-versions = ">=3.6.2" - -[package.dependencies] -wcwidth = "*" - -[[package]] -name = "psutil" -version = "5.9.4" -description = "Cross-platform lib for process and system monitoring in Python." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.extras] -test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] - -[[package]] -name = "ptyprocess" -version = "0.7.0" -description = "Run a subprocess in a pseudo terminal" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "publicsuffixlist" -version = "0.9.1" -description = "publicsuffixlist implement" -category = "main" -optional = true -python-versions = ">=2.6" - -[package.extras] -readme = ["pandoc"] -update = ["requests"] - -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "pycparser" -version = "2.21" -description = "C parser in Python" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pydeep2" -version = "0.5.1" -description = "Python bindings for ssdeep" -category = "main" -optional = true -python-versions = "*" - -[[package]] -name = "pyfaup" -version = "1.2" -description = "Python bindings for the faup library" -category = "main" -optional = true -python-versions = "*" - -[[package]] -name = "pygments" -version = "2.13.0" -description = "Pygments is a syntax highlighting package written in Python." -category = "main" -optional = false -python-versions = ">=3.6" - -[package.extras] -plugins = ["importlib-metadata"] - -[[package]] -name = "pyparsing" -version = "2.4.7" -description = "Python parsing module" -category = "main" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "pyrsistent" -version = "0.19.2" -description = "Persistent/Functional/Immutable data structures" -category = "main" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "pytest" -version = "7.2.0" -description = "pytest: simple powerful testing with Python" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -attrs = ">=19.2.0" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} - -[package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] - -[[package]] -name = "pytest-cov" -version = "4.0.0" -description = "Pytest plugin for measuring coverage." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] - -[[package]] -name = "python-dateutil" -version = "2.8.2" -description = "Extensions to the standard Python datetime module" -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "python-magic" -version = "0.4.27" -description = "File type identification using libmagic" -category = "main" -optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "pytz" -version = "2022.6" -description = "World timezone definitions, modern and historical" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "pytz-deprecation-shim" -version = "0.1.0.post0" -description = "Shims to make deprecation of pytz easier" -category = "main" -optional = true -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" - -[package.dependencies] -"backports.zoneinfo" = {version = "*", markers = "python_version >= \"3.6\" and python_version < \"3.9\""} -tzdata = {version = "*", markers = "python_version >= \"3.6\""} - -[[package]] -name = "pywin32" -version = "305" -description = "Python for Window Extensions" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "pywinpty" -version = "2.0.9" -description = "Pseudo terminal support for Windows from Python." -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "pyzmq" -version = "24.0.1" -description = "Python bindings for 0MQ" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -cffi = {version = "*", markers = "implementation_name == \"pypy\""} -py = {version = "*", markers = "implementation_name == \"pypy\""} - -[[package]] -name = "recommonmark" -version = "0.7.1" -description = "A docutils-compatibility bridge to CommonMark, enabling you to write CommonMark inside of Docutils & Sphinx projects." -category = "main" -optional = true -python-versions = "*" - -[package.dependencies] -commonmark = ">=0.8.1" -docutils = ">=0.11" -sphinx = ">=1.3.1" - -[[package]] -name = "reportlab" -version = "3.6.12" -description = "The Reportlab Toolkit" -category = "main" -optional = true -python-versions = ">=3.7,<4" - -[package.dependencies] -pillow = ">=9.0.0" - -[package.extras] -fttextpath = ["freetype-py (>=2.3.0,<2.4)"] -rlpycairo = ["rlPyCairo (>=0.1.0)"] - -[[package]] -name = "requests" -version = "2.28.1" -description = "Python HTTP for Humans." -category = "main" -optional = false -python-versions = ">=3.7, <4" - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<3" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<1.27" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "requests-mock" -version = "1.10.0" -description = "Mock out responses from the requests package" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -requests = ">=2.3,<3" -six = "*" - -[package.extras] -fixture = ["fixtures"] -test = ["fixtures", "mock", "purl", "pytest", "requests-futures", "sphinx", "testrepository (>=0.0.18)", "testtools"] - -[[package]] -name = "rtfde" -version = "0.0.2" -description = "A library for extracting HTML content from RTF encapsulated HTML as commonly found in the exchange MSG email format." -category = "main" -optional = true -python-versions = ">=3.6" - -[package.dependencies] -lark-parser = ">=0.11" -oletools = ">=0.56" - -[package.extras] -dev = ["lxml (>=4.6)"] -msg-parse = ["extract-msg (>=0.27)"] - -[[package]] -name = "send2trash" -version = "1.8.0" -description = "Send file to trash natively under Mac OS X, Windows and Linux." -category = "dev" -optional = false -python-versions = "*" - -[package.extras] -nativelib = ["pyobjc-framework-Cocoa", "pywin32"] -objc = ["pyobjc-framework-Cocoa"] -win32 = ["pywin32"] - -[[package]] -name = "setuptools" -version = "65.6.3" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "sniffio" -version = "1.3.0" -description = "Sniff out which async library your code is running under" -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "snowballstemmer" -version = "2.2.0" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -category = "main" -optional = true -python-versions = "*" - -[[package]] -name = "soupsieve" -version = "2.3.2.post1" -description = "A modern CSS selector implementation for Beautiful Soup." -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "sphinx" -version = "5.3.0" -description = "Python documentation generator" -category = "main" -optional = true -python-versions = ">=3.6" - -[package.dependencies] -alabaster = ">=0.7,<0.8" -babel = ">=2.9" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.14,<0.20" -imagesize = ">=1.3" -importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} -Jinja2 = ">=3.0" -packaging = ">=21.0" -Pygments = ">=2.12" -requests = ">=2.5.0" -snowballstemmer = ">=2.0" -sphinxcontrib-applehelp = "*" -sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = ">=2.0.0" -sphinxcontrib-jsmath = "*" -sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.5" - -[package.extras] -docs = ["sphinxcontrib-websupport"] -lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-bugbear", "flake8-comprehensions", "flake8-simplify", "isort", "mypy (>=0.981)", "sphinx-lint", "types-requests", "types-typed-ast"] -test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] - -[[package]] -name = "sphinx-autodoc-typehints" -version = "1.19.5" -description = "Type hints (PEP 484) support for the Sphinx autodoc extension" -category = "main" -optional = true -python-versions = ">=3.7" - -[package.dependencies] -sphinx = ">=5.3" - -[package.extras] -docs = ["furo (>=2022.9.29)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"] -testing = ["covdefaults (>=2.2)", "coverage (>=6.5)", "diff-cover (>=7.0.1)", "nptyping (>=2.3.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "sphobjinv (>=2.2.2)", "typing-extensions (>=4.4)"] -type-comment = ["typed-ast (>=1.5.4)"] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "1.0.2" -description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" -category = "main" -optional = true -python-versions = ">=3.5" - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "1.0.2" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." -category = "main" -optional = true -python-versions = ">=3.5" - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.0.0" -description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -category = "main" -optional = true -python-versions = ">=3.6" - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["html5lib", "pytest"] - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -description = "A sphinx extension which renders display math in HTML via JavaScript" -category = "main" -optional = true -python-versions = ">=3.5" - -[package.extras] -test = ["flake8", "mypy", "pytest"] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "1.0.3" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." -category = "main" -optional = true -python-versions = ">=3.5" - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "1.1.5" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." -category = "main" -optional = true -python-versions = ">=3.5" - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "terminado" -version = "0.17.0" -description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -ptyprocess = {version = "*", markers = "os_name != \"nt\""} -pywinpty = {version = ">=1.1.0", markers = "os_name == \"nt\""} -tornado = ">=6.1.0" - -[package.extras] -docs = ["pydata-sphinx-theme", "sphinx"] -test = ["pre-commit", "pytest (>=7.0)", "pytest-timeout"] - -[[package]] -name = "tinycss2" -version = "1.2.1" -description = "A tiny CSS parser" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -webencodings = ">=0.4" - -[package.extras] -doc = ["sphinx", "sphinx_rtd_theme"] -test = ["flake8", "isort", "pytest"] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "tornado" -version = "6.2" -description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." -category = "dev" -optional = false -python-versions = ">= 3.7" - -[[package]] -name = "traitlets" -version = "5.5.0" -description = "" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["pre-commit", "pytest"] - -[[package]] -name = "typed-ast" -version = "1.5.4" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "types-click" -version = "7.1.8" -description = "Typing stubs for click" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "types-flask" -version = "1.1.6" -description = "Typing stubs for Flask" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -types-click = "*" -types-Jinja2 = "*" -types-Werkzeug = "*" - -[[package]] -name = "types-jinja2" -version = "2.11.9" -description = "Typing stubs for Jinja2" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -types-MarkupSafe = "*" - -[[package]] -name = "types-markupsafe" -version = "1.1.10" -description = "Typing stubs for MarkupSafe" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "types-python-dateutil" -version = "2.8.19.4" -description = "Typing stubs for python-dateutil" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "types-redis" -version = "4.3.21.6" -description = "Typing stubs for redis" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "types-requests" -version = "2.28.11.5" -description = "Typing stubs for requests" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -types-urllib3 = "<1.27" - -[[package]] -name = "types-urllib3" -version = "1.26.25.4" -description = "Typing stubs for urllib3" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "types-werkzeug" -version = "1.0.9" -description = "Typing stubs for Werkzeug" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "typing-extensions" -version = "4.4.0" -description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "tzdata" -version = "2022.6" -description = "Provider of IANA time zone data" -category = "main" -optional = true -python-versions = ">=2" - -[[package]] -name = "tzlocal" -version = "4.2" -description = "tzinfo object for the local timezone" -category = "main" -optional = true -python-versions = ">=3.6" - -[package.dependencies] -"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} -pytz-deprecation-shim = "*" -tzdata = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -devenv = ["black", "pyroma", "pytest-cov", "zest.releaser"] -test = ["pytest (>=4.3)", "pytest-mock (>=3.3)"] - -[[package]] -name = "urllib3" -version = "1.26.13" -description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" - -[package.dependencies] -brotli = {version = ">=1.0.9", optional = true, markers = "(os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation == \"CPython\" and extra == \"brotli\""} -brotlicffi = {version = ">=0.8.0", optional = true, markers = "(os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation != \"CPython\" and extra == \"brotli\""} - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] - -[[package]] -name = "validators" -version = "0.20.0" -description = "Python Data Validation for Humans™." -category = "main" -optional = true -python-versions = ">=3.4" - -[package.dependencies] -decorator = ">=3.4.0" - -[package.extras] -test = ["flake8 (>=2.4.0)", "isort (>=4.2.2)", "pytest (>=2.2.3)"] - -[[package]] -name = "wcwidth" -version = "0.2.5" -description = "Measures the displayed width of unicode strings in a terminal" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "webencodings" -version = "0.5.1" -description = "Character encoding aliases for legacy web content" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "websocket-client" -version = "1.4.2" -description = "WebSocket client for Python with low level API options" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] -optional = ["python-socks", "wsaccel"] -test = ["websockets"] - -[[package]] -name = "win-unicode-console" -version = "0.5" -description = "Enable Unicode input and display when running Python from Windows console." -category = "main" -optional = true -python-versions = "*" - -[[package]] -name = "wrapt" -version = "1.14.1" -description = "Module for decorators, wrappers and monkey patching." -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" - -[[package]] -name = "zipp" -version = "3.11.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] -testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - -[extras] -brotli = ["urllib3"] -docs = ["sphinx-autodoc-typehints", "recommonmark"] -email = ["extract_msg", "RTFDE", "oletools"] -fileobjects = ["python-magic", "pydeep2", "lief"] -openioc = ["beautifulsoup4"] -pdfexport = ["reportlab"] -url = ["pyfaup", "chardet"] -virustotal = ["validators"] - -[metadata] -lock-version = "1.1" -python-versions = "^3.7" -content-hash = "2bfb1536579bdbdb073fbf22bfa79c0e56d7517dd60d3f6e844fa6623e666cf9" - -[metadata.files] -alabaster = [ - {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, - {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, -] -anyio = [ - {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, - {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, -] -appnope = [ - {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, - {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, -] -argon2-cffi = [ - {file = "argon2-cffi-21.3.0.tar.gz", hash = "sha256:d384164d944190a7dd7ef22c6aa3ff197da12962bd04b17f64d4e93d934dba5b"}, - {file = "argon2_cffi-21.3.0-py3-none-any.whl", hash = "sha256:8c976986f2c5c0e5000919e6de187906cfd81fb1c72bf9d88c01177e77da7f80"}, -] -argon2-cffi-bindings = [ +files = [ {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"}, @@ -1861,19 +93,119 @@ argon2-cffi-bindings = [ {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"}, {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"}, ] -attrs = [ - {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, - {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, + +[package.dependencies] +cffi = ">=1.0.1" + +[package.extras] +dev = ["cogapp", "pre-commit", "pytest", "wheel"] +tests = ["pytest"] + +[[package]] +name = "arrow" +version = "1.3.0" +description = "Better dates & times for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, + {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, ] -babel = [ - {file = "Babel-2.11.0-py3-none-any.whl", hash = "sha256:1ad3eca1c885218f6dce2ab67291178944f810a10a9b5f3cb8382a5a232b64fe"}, - {file = "Babel-2.11.0.tar.gz", hash = "sha256:5ef4b3226b0180dedded4229651c8b0e1a3a6a2837d45a073272f313e4cf97f6"}, + +[package.dependencies] +python-dateutil = ">=2.7.0" +types-python-dateutil = ">=2.8.10" + +[package.extras] +doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] +test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] + +[[package]] +name = "asttokens" +version = "2.4.1" +description = "Annotate AST trees with source code positions" +optional = false +python-versions = "*" +files = [ + {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, + {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, ] -backcall = [ + +[package.dependencies] +six = ">=1.12.0" + +[package.extras] +astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] +test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] + +[[package]] +name = "async-lru" +version = "2.0.4" +description = "Simple LRU cache for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "async-lru-2.0.4.tar.gz", hash = "sha256:b8a59a5df60805ff63220b2a0c5b5393da5521b113cd5465a44eb037d81a5627"}, + {file = "async_lru-2.0.4-py3-none-any.whl", hash = "sha256:ff02944ce3c288c5be660c42dbcca0742b32c3b279d6dceda655190240b99224"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "attrs" +version = "23.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] + +[[package]] +name = "babel" +version = "2.15.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +files = [ + {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, + {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, +] + +[package.dependencies] +pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + +[[package]] +name = "backcall" +version = "0.2.0" +description = "Specifications for callback functions passed in to an API" +optional = false +python-versions = "*" +files = [ {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, ] -backports-zoneinfo = [ + +[[package]] +name = "backports-zoneinfo" +version = "0.2.1" +description = "Backport of the standard library zoneinfo module" +optional = true +python-versions = ">=3.6" +files = [ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, @@ -1891,713 +223,2052 @@ backports-zoneinfo = [ {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, ] -beautifulsoup4 = [ - {file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"}, - {file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"}, + +[package.extras] +tzdata = ["tzdata"] + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, ] -bleach = [ - {file = "bleach-5.0.1-py3-none-any.whl", hash = "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a"}, - {file = "bleach-5.0.1.tar.gz", hash = "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c"}, + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "bleach" +version = "6.1.0" +description = "An easy safelist-based HTML-sanitizing tool." +optional = false +python-versions = ">=3.8" +files = [ + {file = "bleach-6.1.0-py3-none-any.whl", hash = "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6"}, + {file = "bleach-6.1.0.tar.gz", hash = "sha256:0a31f1837963c41d46bbf1331b8778e1308ea0791db03cc4e7357b97cf42a8fe"}, ] -brotli = [ - {file = "Brotli-1.0.9-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70"}, - {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b"}, - {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5913a1177fc36e30fcf6dc868ce23b0453952c78c04c266d3149b3d39e1410d6"}, - {file = "Brotli-1.0.9-cp27-cp27m-win32.whl", hash = "sha256:afde17ae04d90fbe53afb628f7f2d4ca022797aa093e809de5c3cf276f61bbfa"}, - {file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7cb81373984cc0e4682f31bc3d6be9026006d96eecd07ea49aafb06897746452"}, - {file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:db844eb158a87ccab83e868a762ea8024ae27337fc7ddcbfcddd157f841fdfe7"}, - {file = "Brotli-1.0.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9744a863b489c79a73aba014df554b0e7a0fc44ef3f8a0ef2a52919c7d155031"}, - {file = "Brotli-1.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a72661af47119a80d82fa583b554095308d6a4c356b2a554fdc2799bc19f2a43"}, - {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ee83d3e3a024a9618e5be64648d6d11c37047ac48adff25f12fa4226cf23d1c"}, - {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:19598ecddd8a212aedb1ffa15763dd52a388518c4550e615aed88dc3753c0f0c"}, - {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:44bb8ff420c1d19d91d79d8c3574b8954288bdff0273bf788954064d260d7ab0"}, - {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e23281b9a08ec338469268f98f194658abfb13658ee98e2b7f85ee9dd06caa91"}, - {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3496fc835370da351d37cada4cf744039616a6db7d13c430035e901443a34daa"}, - {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83bb06a0192cccf1eb8d0a28672a1b79c74c3a8a5f2619625aeb6f28b3a82bb"}, - {file = "Brotli-1.0.9-cp310-cp310-win32.whl", hash = "sha256:26d168aac4aaec9a4394221240e8a5436b5634adc3cd1cdf637f6645cecbf181"}, - {file = "Brotli-1.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:622a231b08899c864eb87e85f81c75e7b9ce05b001e59bbfbf43d4a71f5f32b2"}, - {file = "Brotli-1.0.9-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:c83aa123d56f2e060644427a882a36b3c12db93727ad7a7b9efd7d7f3e9cc2c4"}, - {file = "Brotli-1.0.9-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6b2ae9f5f67f89aade1fab0f7fd8f2832501311c363a21579d02defa844d9296"}, - {file = "Brotli-1.0.9-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:68715970f16b6e92c574c30747c95cf8cf62804569647386ff032195dc89a430"}, - {file = "Brotli-1.0.9-cp35-cp35m-win32.whl", hash = "sha256:defed7ea5f218a9f2336301e6fd379f55c655bea65ba2476346340a0ce6f74a1"}, - {file = "Brotli-1.0.9-cp35-cp35m-win_amd64.whl", hash = "sha256:88c63a1b55f352b02c6ffd24b15ead9fc0e8bf781dbe070213039324922a2eea"}, - {file = "Brotli-1.0.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:503fa6af7da9f4b5780bb7e4cbe0c639b010f12be85d02c99452825dd0feef3f"}, - {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:40d15c79f42e0a2c72892bf407979febd9cf91f36f495ffb333d1d04cebb34e4"}, - {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:93130612b837103e15ac3f9cbacb4613f9e348b58b3aad53721d92e57f96d46a"}, - {file = "Brotli-1.0.9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87fdccbb6bb589095f413b1e05734ba492c962b4a45a13ff3408fa44ffe6479b"}, - {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:6d847b14f7ea89f6ad3c9e3901d1bc4835f6b390a9c71df999b0162d9bb1e20f"}, - {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:495ba7e49c2db22b046a53b469bbecea802efce200dffb69b93dd47397edc9b6"}, - {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:4688c1e42968ba52e57d8670ad2306fe92e0169c6f3af0089be75bbac0c64a3b"}, - {file = "Brotli-1.0.9-cp36-cp36m-win32.whl", hash = "sha256:61a7ee1f13ab913897dac7da44a73c6d44d48a4adff42a5701e3239791c96e14"}, - {file = "Brotli-1.0.9-cp36-cp36m-win_amd64.whl", hash = "sha256:1c48472a6ba3b113452355b9af0a60da5c2ae60477f8feda8346f8fd48e3e87c"}, - {file = "Brotli-1.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b78a24b5fd13c03ee2b7b86290ed20efdc95da75a3557cc06811764d5ad1126"}, - {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:9d12cf2851759b8de8ca5fde36a59c08210a97ffca0eb94c532ce7b17c6a3d1d"}, - {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6c772d6c0a79ac0f414a9f8947cc407e119b8598de7621f39cacadae3cf57d12"}, - {file = "Brotli-1.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29d1d350178e5225397e28ea1b7aca3648fcbab546d20e7475805437bfb0a130"}, - {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7bbff90b63328013e1e8cb50650ae0b9bac54ffb4be6104378490193cd60f85a"}, - {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ec1947eabbaf8e0531e8e899fc1d9876c179fc518989461f5d24e2223395a9e3"}, - {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12effe280b8ebfd389022aa65114e30407540ccb89b177d3fbc9a4f177c4bd5d"}, - {file = "Brotli-1.0.9-cp37-cp37m-win32.whl", hash = "sha256:f909bbbc433048b499cb9db9e713b5d8d949e8c109a2a548502fb9aa8630f0b1"}, - {file = "Brotli-1.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:97f715cf371b16ac88b8c19da00029804e20e25f30d80203417255d239f228b5"}, - {file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e16eb9541f3dd1a3e92b89005e37b1257b157b7256df0e36bd7b33b50be73bcb"}, - {file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:160c78292e98d21e73a4cc7f76a234390e516afcd982fa17e1422f7c6a9ce9c8"}, - {file = "Brotli-1.0.9-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b663f1e02de5d0573610756398e44c130add0eb9a3fc912a09665332942a2efb"}, - {file = "Brotli-1.0.9-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5b6ef7d9f9c38292df3690fe3e302b5b530999fa90014853dcd0d6902fb59f26"}, - {file = "Brotli-1.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a674ac10e0a87b683f4fa2b6fa41090edfd686a6524bd8dedbd6138b309175c"}, - {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e2d9e1cbc1b25e22000328702b014227737756f4b5bf5c485ac1d8091ada078b"}, - {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b336c5e9cf03c7be40c47b5fd694c43c9f1358a80ba384a21969e0b4e66a9b17"}, - {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:85f7912459c67eaab2fb854ed2bc1cc25772b300545fe7ed2dc03954da638649"}, - {file = "Brotli-1.0.9-cp38-cp38-win32.whl", hash = "sha256:35a3edbe18e876e596553c4007a087f8bcfd538f19bc116917b3c7522fca0429"}, - {file = "Brotli-1.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:269a5743a393c65db46a7bb982644c67ecba4b8d91b392403ad8a861ba6f495f"}, - {file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2aad0e0baa04517741c9bb5b07586c642302e5fb3e75319cb62087bd0995ab19"}, - {file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5cb1e18167792d7d21e21365d7650b72d5081ed476123ff7b8cac7f45189c0c7"}, - {file = "Brotli-1.0.9-cp39-cp39-manylinux1_i686.whl", hash = "sha256:16d528a45c2e1909c2798f27f7bf0a3feec1dc9e50948e738b961618e38b6a7b"}, - {file = "Brotli-1.0.9-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:56d027eace784738457437df7331965473f2c0da2c70e1a1f6fdbae5402e0389"}, - {file = "Brotli-1.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bf919756d25e4114ace16a8ce91eb340eb57a08e2c6950c3cebcbe3dff2a5e7"}, - {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e4c4e92c14a57c9bd4cb4be678c25369bf7a092d55fd0866f759e425b9660806"}, - {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e48f4234f2469ed012a98f4b7874e7f7e173c167bed4934912a29e03167cf6b1"}, - {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9ed4c92a0665002ff8ea852353aeb60d9141eb04109e88928026d3c8a9e5433c"}, - {file = "Brotli-1.0.9-cp39-cp39-win32.whl", hash = "sha256:cfc391f4429ee0a9370aa93d812a52e1fee0f37a81861f4fdd1f4fb28e8547c3"}, - {file = "Brotli-1.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:854c33dad5ba0fbd6ab69185fec8dab89e13cda6b7d191ba111987df74f38761"}, - {file = "Brotli-1.0.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9749a124280a0ada4187a6cfd1ffd35c350fb3af79c706589d98e088c5044267"}, - {file = "Brotli-1.0.9-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:76ffebb907bec09ff511bb3acc077695e2c32bc2142819491579a695f77ffd4d"}, - {file = "Brotli-1.0.9.zip", hash = "sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438"}, + +[package.dependencies] +six = ">=1.9.0" +webencodings = "*" + +[package.extras] +css = ["tinycss2 (>=1.1.0,<1.3)"] + +[[package]] +name = "brotli" +version = "1.1.0" +description = "Python bindings for the Brotli compression library" +optional = true +python-versions = "*" +files = [ + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752"}, + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"}, + {file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"}, + {file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"}, + {file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"}, + {file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"}, + {file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"}, + {file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"}, + {file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"}, + {file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"}, + {file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"}, + {file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"}, + {file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"}, + {file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"}, + {file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"}, + {file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"}, + {file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"}, + {file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"}, + {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"}, ] -brotlicffi = [ - {file = "brotlicffi-1.0.9.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:408ec4359f9763280d5c4e0ad29c51d1240b25fdd18719067e972163b4125b98"}, - {file = "brotlicffi-1.0.9.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2e4629f7690ded66c8818715c6d4dd6a7ff6a4f10fad6186fe99850f781ce210"}, - {file = "brotlicffi-1.0.9.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:137c4635edcdf593de5ce9d0daa596bf499591b16b8fca5fd72a490deb54b2ee"}, - {file = "brotlicffi-1.0.9.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:af8a1b7bcfccf9c41a3c8654994d6a81821fdfe4caddcfe5045bfda936546ca3"}, - {file = "brotlicffi-1.0.9.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9078432af4785f35ab3840587eed7fb131e3fc77eb2a739282b649b343c584dd"}, - {file = "brotlicffi-1.0.9.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7bb913d5bf3b4ce2ec59872711dc9faaff5f320c3c3827cada2d8a7b793a7753"}, - {file = "brotlicffi-1.0.9.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:16a0c9392a1059e2e62839fbd037d2e7e03c8ae5da65e9746f582464f7fab1bb"}, - {file = "brotlicffi-1.0.9.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:94d2810efc5723f1447b332223b197466190518a3eeca93b9f357efb5b22c6dc"}, - {file = "brotlicffi-1.0.9.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:9e70f3e20f317d70912b10dbec48b29114d3dbd0e9d88475cb328e6c086f0546"}, - {file = "brotlicffi-1.0.9.2-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:586f0ea3c2eed455d5f2330b9ab4a591514c8de0ee53d445645efcfbf053c69f"}, - {file = "brotlicffi-1.0.9.2-cp35-abi3-manylinux1_i686.whl", hash = "sha256:4454c3baedc277fd6e65f983e3eb8e77f4bc15060f69370a0201746e2edeca81"}, - {file = "brotlicffi-1.0.9.2-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:52c1c12dad6eb1d44213a0a76acf5f18f64653bd801300bef5e2f983405bdde5"}, - {file = "brotlicffi-1.0.9.2-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:21cd400d24b344c218d8e32b394849e31b7c15784667575dbda9f65c46a64b0a"}, - {file = "brotlicffi-1.0.9.2-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:71061f8bc86335b652e442260c4367b782a92c6e295cf5a10eff84c7d19d8cf5"}, - {file = "brotlicffi-1.0.9.2-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:15e0db52c56056be6310fc116b3d7c6f34185594e261f23790b2fb6489998363"}, - {file = "brotlicffi-1.0.9.2-cp35-abi3-win32.whl", hash = "sha256:551305703d12a2dd1ae43d3dde35dee20b1cb49b5796279d4d34e2c6aec6be4d"}, - {file = "brotlicffi-1.0.9.2-cp35-abi3-win_amd64.whl", hash = "sha256:2be4fb8a7cb482f226af686cd06d2a2cab164ccdf99e460f8e3a5ec9a5337da2"}, - {file = "brotlicffi-1.0.9.2-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:8e7221d8a084d32d15c7b58e0ce0573972375c5038423dbe83f217cfe512e680"}, - {file = "brotlicffi-1.0.9.2-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:75a46bc5ed2753e1648cc211dcb2c1ac66116038766822dc104023f67ff4dfd8"}, - {file = "brotlicffi-1.0.9.2-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:1e27c43ef72a278f9739b12b2df80ee72048cd4cbe498f8bbe08aaaa67a5d5c8"}, - {file = "brotlicffi-1.0.9.2-pp27-pypy_73-win32.whl", hash = "sha256:feb942814285bdc5e97efc77a04e48283c17dfab9ea082d79c0a7b9e53ef1eab"}, - {file = "brotlicffi-1.0.9.2-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a6208d82c3172eeeb3be83ed4efd5831552c7cd47576468e50fcf0fb23fcf97f"}, - {file = "brotlicffi-1.0.9.2-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:408c810c599786fb806556ff17e844a903884e6370ca400bcec7fa286149f39c"}, - {file = "brotlicffi-1.0.9.2-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:a73099858ee343e8801710a08be8d194f47715ff21e98d92a19ac461058f52d1"}, - {file = "brotlicffi-1.0.9.2-pp36-pypy36_pp73-win32.whl", hash = "sha256:916b790f967a18a595e61f218c252f83718ac91f24157d622cf0fa710cd26ab7"}, - {file = "brotlicffi-1.0.9.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ba4a00263af40e875ec3d6c7f623cbf8c795b55705da18c64ec36b6bf0848bc5"}, - {file = "brotlicffi-1.0.9.2-pp37-pypy37_pp73-manylinux1_x86_64.whl", hash = "sha256:df78aa47741122b0d5463f1208b7bb18bc9706dee5152d9f56e0ead4865015cd"}, - {file = "brotlicffi-1.0.9.2-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:9030cd5099252d16bfa4e22659c84a89c102e94f8e81d30764788b72e2d7cfb7"}, - {file = "brotlicffi-1.0.9.2-pp37-pypy37_pp73-win32.whl", hash = "sha256:7e72978f4090a161885b114f87b784f538dcb77dafc6602592c1cf39ae8d243d"}, - {file = "brotlicffi-1.0.9.2.tar.gz", hash = "sha256:0c248a68129d8fc6a217767406c731e498c3e19a7be05ea0a90c3c86637b7d96"}, + +[[package]] +name = "brotlicffi" +version = "1.1.0.0" +description = "Python CFFI bindings to the Brotli library" +optional = true +python-versions = ">=3.7" +files = [ + {file = "brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-win32.whl", hash = "sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2e4aeb0bd2540cb91b069dbdd54d458da8c4334ceaf2d25df2f4af576d6766ca"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b7b0033b0d37bb33009fb2fef73310e432e76f688af76c156b3594389d81391"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54a07bb2374a1eba8ebb52b6fafffa2afd3c4df85ddd38fcc0511f2bb387c2a8"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7901a7dc4b88f1c1475de59ae9be59799db1007b7d059817948d8e4f12e24e35"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce01c7316aebc7fce59da734286148b1d1b9455f89cf2c8a4dfce7d41db55c2d"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:246f1d1a90279bb6069de3de8d75a8856e073b8ff0b09dcca18ccc14cec85979"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc4bc5d82bc56ebd8b514fb8350cfac4627d6b0743382e46d033976a5f80fab6"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c26ecb14386a44b118ce36e546ce307f4810bc9598a6e6cb4f7fca725ae7e6"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca72968ae4eaf6470498d5c2887073f7efe3b1e7d7ec8be11a06a79cc810e990"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:add0de5b9ad9e9aa293c3aa4e9deb2b61e99ad6c1634e01d01d98c03e6a354cc"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b6068e0f3769992d6b622a1cd2e7835eae3cf8d9da123d7f51ca9c1e9c333e5"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8557a8559509b61e65083f8782329188a250102372576093c88930c875a69838"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a7ae37e5d79c5bdfb5b4b99f2715a6035e6c5bf538c3746abc8e26694f92f33"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391151ec86bb1c683835980f4816272a87eaddc46bb91cbf44f62228b84d8cca"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2f3711be9290f0453de8eed5275d93d286abe26b08ab4a35d7452caa1fef532f"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a807d760763e398bbf2c6394ae9da5815901aa93ee0a37bca5efe78d4ee3171"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa8ca0623b26c94fccc3a1fdd895be1743b838f3917300506d04aa3346fd2a14"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3de0cf28a53a3238b252aca9fed1593e9d36c1d116748013339f0949bfc84112"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6be5ec0e88a4925c91f3dea2bb0013b3a2accda6f77238f76a34a1ea532a1cb0"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d9eb71bb1085d996244439154387266fd23d6ad37161f6f52f1cd41dd95a3808"}, + {file = "brotlicffi-1.1.0.0.tar.gz", hash = "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13"}, ] -certifi = [ - {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, - {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, + +[package.dependencies] +cffi = ">=1.0.0" + +[[package]] +name = "certifi" +version = "2024.7.4" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] -cffi = [ - {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, - {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, - {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, - {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, - {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, - {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, - {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, - {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, - {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, - {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, - {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, - {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, - {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, - {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, - {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, - {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, - {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, - {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, - {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, + +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, ] -chardet = [ - {file = "chardet-5.0.0-py3-none-any.whl", hash = "sha256:d3e64f022d254183001eccc5db4040520c0f23b1a3f33d6413e099eb7f126557"}, - {file = "chardet-5.0.0.tar.gz", hash = "sha256:0368df2bfd78b5fc20572bb4e9bb7fb53e2c094f60ae9993339e8671d0afb8aa"}, + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "chardet" +version = "5.2.0" +description = "Universal encoding detector for Python 3" +optional = true +python-versions = ">=3.7" +files = [ + {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, + {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, ] -charset-normalizer = [ - {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, - {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] -colorama = [ + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -colorclass = [ + +[[package]] +name = "colorclass" +version = "2.2.2" +description = "Colorful worry-free console applications for Linux, Mac OS X, and Windows." +optional = true +python-versions = ">=2.6" +files = [ {file = "colorclass-2.2.2-py2.py3-none-any.whl", hash = "sha256:6f10c273a0ef7a1150b1120b6095cbdd68e5cf36dfd5d0fc957a2500bbf99a55"}, {file = "colorclass-2.2.2.tar.gz", hash = "sha256:6d4fe287766166a98ca7bc6f6312daf04a0481b1eda43e7173484051c0ab4366"}, ] -commonmark = [ + +[[package]] +name = "comm" +version = "0.2.2" +description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." +optional = false +python-versions = ">=3.8" +files = [ + {file = "comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3"}, + {file = "comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e"}, +] + +[package.dependencies] +traitlets = ">=4" + +[package.extras] +test = ["pytest"] + +[[package]] +name = "commonmark" +version = "0.9.1" +description = "Python parser for the CommonMark Markdown spec" +optional = true +python-versions = "*" +files = [ {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, ] -compressed-rtf = [ + +[package.extras] +test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] + +[[package]] +name = "compressed-rtf" +version = "1.0.6" +description = "Compressed Rich Text Format (RTF) compression and decompression package" +optional = true +python-versions = "*" +files = [ {file = "compressed_rtf-1.0.6.tar.gz", hash = "sha256:c1c827f1d124d24608981a56e8b8691eb1f2a69a78ccad6440e7d92fde1781dd"}, ] -coverage = [ - {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, - {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, - {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, - {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, - {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, - {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, - {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, - {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, - {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, - {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, - {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, - {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, - {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, - {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, - {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, - {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, - {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, - {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, - {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, - {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, - {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, - {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, - {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, - {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, - {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, - {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, - {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, - {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, - {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, - {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, - {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, - {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, - {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, - {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, - {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, - {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, - {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, - {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, - {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, - {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, - {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, - {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, - {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, - {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, - {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, - {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, - {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, - {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, - {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, - {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, + +[[package]] +name = "coverage" +version = "7.5.4" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cfb5a4f556bb51aba274588200a46e4dd6b505fb1a5f8c5ae408222eb416f99"}, + {file = "coverage-7.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2174e7c23e0a454ffe12267a10732c273243b4f2d50d07544a91198f05c48f47"}, + {file = "coverage-7.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2214ee920787d85db1b6a0bd9da5f8503ccc8fcd5814d90796c2f2493a2f4d2e"}, + {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1137f46adb28e3813dec8c01fefadcb8c614f33576f672962e323b5128d9a68d"}, + {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b385d49609f8e9efc885790a5a0e89f2e3ae042cdf12958b6034cc442de428d3"}, + {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4a474f799456e0eb46d78ab07303286a84a3140e9700b9e154cfebc8f527016"}, + {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5cd64adedf3be66f8ccee418473c2916492d53cbafbfcff851cbec5a8454b136"}, + {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e564c2cf45d2f44a9da56f4e3a26b2236504a496eb4cb0ca7221cd4cc7a9aca9"}, + {file = "coverage-7.5.4-cp310-cp310-win32.whl", hash = "sha256:7076b4b3a5f6d2b5d7f1185fde25b1e54eb66e647a1dfef0e2c2bfaf9b4c88c8"}, + {file = "coverage-7.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:018a12985185038a5b2bcafab04ab833a9a0f2c59995b3cec07e10074c78635f"}, + {file = "coverage-7.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db14f552ac38f10758ad14dd7b983dbab424e731588d300c7db25b6f89e335b5"}, + {file = "coverage-7.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3257fdd8e574805f27bb5342b77bc65578e98cbc004a92232106344053f319ba"}, + {file = "coverage-7.5.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a6612c99081d8d6134005b1354191e103ec9705d7ba2754e848211ac8cacc6b"}, + {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d45d3cbd94159c468b9b8c5a556e3f6b81a8d1af2a92b77320e887c3e7a5d080"}, + {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed550e7442f278af76d9d65af48069f1fb84c9f745ae249c1a183c1e9d1b025c"}, + {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a892be37ca35eb5019ec85402c3371b0f7cda5ab5056023a7f13da0961e60da"}, + {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8192794d120167e2a64721d88dbd688584675e86e15d0569599257566dec9bf0"}, + {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:820bc841faa502e727a48311948e0461132a9c8baa42f6b2b84a29ced24cc078"}, + {file = "coverage-7.5.4-cp311-cp311-win32.whl", hash = "sha256:6aae5cce399a0f065da65c7bb1e8abd5c7a3043da9dceb429ebe1b289bc07806"}, + {file = "coverage-7.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:d2e344d6adc8ef81c5a233d3a57b3c7d5181f40e79e05e1c143da143ccb6377d"}, + {file = "coverage-7.5.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:54317c2b806354cbb2dc7ac27e2b93f97096912cc16b18289c5d4e44fc663233"}, + {file = "coverage-7.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:042183de01f8b6d531e10c197f7f0315a61e8d805ab29c5f7b51a01d62782747"}, + {file = "coverage-7.5.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bb74ed465d5fb204b2ec41d79bcd28afccf817de721e8a807d5141c3426638"}, + {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3d45ff86efb129c599a3b287ae2e44c1e281ae0f9a9bad0edc202179bcc3a2e"}, + {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5013ed890dc917cef2c9f765c4c6a8ae9df983cd60dbb635df8ed9f4ebc9f555"}, + {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1014fbf665fef86cdfd6cb5b7371496ce35e4d2a00cda501cf9f5b9e6fced69f"}, + {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3684bc2ff328f935981847082ba4fdc950d58906a40eafa93510d1b54c08a66c"}, + {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:581ea96f92bf71a5ec0974001f900db495488434a6928a2ca7f01eee20c23805"}, + {file = "coverage-7.5.4-cp312-cp312-win32.whl", hash = "sha256:73ca8fbc5bc622e54627314c1a6f1dfdd8db69788f3443e752c215f29fa87a0b"}, + {file = "coverage-7.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:cef4649ec906ea7ea5e9e796e68b987f83fa9a718514fe147f538cfeda76d7a7"}, + {file = "coverage-7.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdd31315fc20868c194130de9ee6bfd99755cc9565edff98ecc12585b90be882"}, + {file = "coverage-7.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:02ff6e898197cc1e9fa375581382b72498eb2e6d5fc0b53f03e496cfee3fac6d"}, + {file = "coverage-7.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d05c16cf4b4c2fc880cb12ba4c9b526e9e5d5bb1d81313d4d732a5b9fe2b9d53"}, + {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5986ee7ea0795a4095ac4d113cbb3448601efca7f158ec7f7087a6c705304e4"}, + {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df54843b88901fdc2f598ac06737f03d71168fd1175728054c8f5a2739ac3e4"}, + {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ab73b35e8d109bffbda9a3e91c64e29fe26e03e49addf5b43d85fc426dde11f9"}, + {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:aea072a941b033813f5e4814541fc265a5c12ed9720daef11ca516aeacd3bd7f"}, + {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:16852febd96acd953b0d55fc842ce2dac1710f26729b31c80b940b9afcd9896f"}, + {file = "coverage-7.5.4-cp38-cp38-win32.whl", hash = "sha256:8f894208794b164e6bd4bba61fc98bf6b06be4d390cf2daacfa6eca0a6d2bb4f"}, + {file = "coverage-7.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:e2afe743289273209c992075a5a4913e8d007d569a406ffed0bd080ea02b0633"}, + {file = "coverage-7.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b95c3a8cb0463ba9f77383d0fa8c9194cf91f64445a63fc26fb2327e1e1eb088"}, + {file = "coverage-7.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d7564cc09dd91b5a6001754a5b3c6ecc4aba6323baf33a12bd751036c998be4"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44da56a2589b684813f86d07597fdf8a9c6ce77f58976727329272f5a01f99f7"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e16f3d6b491c48c5ae726308e6ab1e18ee830b4cdd6913f2d7f77354b33f91c8"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbc5958cb471e5a5af41b0ddaea96a37e74ed289535e8deca404811f6cb0bc3d"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a04e990a2a41740b02d6182b498ee9796cf60eefe40cf859b016650147908029"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ddbd2f9713a79e8e7242d7c51f1929611e991d855f414ca9996c20e44a895f7c"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b1ccf5e728ccf83acd313c89f07c22d70d6c375a9c6f339233dcf792094bcbf7"}, + {file = "coverage-7.5.4-cp39-cp39-win32.whl", hash = "sha256:56b4eafa21c6c175b3ede004ca12c653a88b6f922494b023aeb1e836df953ace"}, + {file = "coverage-7.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:65e528e2e921ba8fd67d9055e6b9f9e34b21ebd6768ae1c1723f4ea6ace1234d"}, + {file = "coverage-7.5.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:79b356f3dd5b26f3ad23b35c75dbdaf1f9e2450b6bcefc6d0825ea0aa3f86ca5"}, + {file = "coverage-7.5.4.tar.gz", hash = "sha256:a44963520b069e12789d0faea4e9fdb1e410cdc4aab89d94f7f55cbb7fef0353"}, ] -cryptography = [ - {file = "cryptography-38.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:2fa36a7b2cc0998a3a4d5af26ccb6273f3df133d61da2ba13b3286261e7efb70"}, - {file = "cryptography-38.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:1f13ddda26a04c06eb57119caf27a524ccae20533729f4b1e4a69b54e07035eb"}, - {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:2ec2a8714dd005949d4019195d72abed84198d877112abb5a27740e217e0ea8d"}, - {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50a1494ed0c3f5b4d07650a68cd6ca62efe8b596ce743a5c94403e6f11bf06c1"}, - {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a10498349d4c8eab7357a8f9aa3463791292845b79597ad1b98a543686fb1ec8"}, - {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:10652dd7282de17990b88679cb82f832752c4e8237f0c714be518044269415db"}, - {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bfe6472507986613dc6cc00b3d492b2f7564b02b3b3682d25ca7f40fa3fd321b"}, - {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce127dd0a6a0811c251a6cddd014d292728484e530d80e872ad9806cfb1c5b3c"}, - {file = "cryptography-38.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:53049f3379ef05182864d13bb9686657659407148f901f3f1eee57a733fb4b00"}, - {file = "cryptography-38.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:8a4b2bdb68a447fadebfd7d24855758fe2d6fecc7fed0b78d190b1af39a8e3b0"}, - {file = "cryptography-38.0.4-cp36-abi3-win32.whl", hash = "sha256:1d7e632804a248103b60b16fb145e8df0bc60eed790ece0d12efe8cd3f3e7744"}, - {file = "cryptography-38.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:8e45653fb97eb2f20b8c96f9cd2b3a0654d742b47d638cf2897afbd97f80fa6d"}, - {file = "cryptography-38.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca57eb3ddaccd1112c18fc80abe41db443cc2e9dcb1917078e02dfa010a4f353"}, - {file = "cryptography-38.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:c9e0d79ee4c56d841bd4ac6e7697c8ff3c8d6da67379057f29e66acffcd1e9a7"}, - {file = "cryptography-38.0.4-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0e70da4bdff7601b0ef48e6348339e490ebfb0cbe638e083c9c41fb49f00c8bd"}, - {file = "cryptography-38.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:998cd19189d8a747b226d24c0207fdaa1e6658a1d3f2494541cb9dfbf7dcb6d2"}, - {file = "cryptography-38.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67461b5ebca2e4c2ab991733f8ab637a7265bb582f07c7c88914b5afb88cb95b"}, - {file = "cryptography-38.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4eb85075437f0b1fd8cd66c688469a0c4119e0ba855e3fef86691971b887caf6"}, - {file = "cryptography-38.0.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3178d46f363d4549b9a76264f41c6948752183b3f587666aff0555ac50fd7876"}, - {file = "cryptography-38.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6391e59ebe7c62d9902c24a4d8bcbc79a68e7c4ab65863536127c8a9cd94043b"}, - {file = "cryptography-38.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:78e47e28ddc4ace41dd38c42e6feecfdadf9c3be2af389abbfeef1ff06822285"}, - {file = "cryptography-38.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fb481682873035600b5502f0015b664abc26466153fab5c6bc92c1ea69d478b"}, - {file = "cryptography-38.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4367da5705922cf7070462e964f66e4ac24162e22ab0a2e9d31f1b270dd78083"}, - {file = "cryptography-38.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b4cad0cea995af760f82820ab4ca54e5471fc782f70a007f31531957f43e9dee"}, - {file = "cryptography-38.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:80ca53981ceeb3241998443c4964a387771588c4e4a5d92735a493af868294f9"}, - {file = "cryptography-38.0.4.tar.gz", hash = "sha256:175c1a818b87c9ac80bb7377f5520b7f31b3ef2a0004e2420319beadedb67290"}, + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "cryptography" +version = "42.0.8" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e"}, + {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7"}, + {file = "cryptography-42.0.8-cp37-abi3-win32.whl", hash = "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2"}, + {file = "cryptography-42.0.8-cp37-abi3-win_amd64.whl", hash = "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba"}, + {file = "cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14"}, + {file = "cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c"}, + {file = "cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad"}, + {file = "cryptography-42.0.8.tar.gz", hash = "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2"}, ] -debugpy = [ - {file = "debugpy-1.6.3-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:c4b2bd5c245eeb49824bf7e539f95fb17f9a756186e51c3e513e32999d8846f3"}, - {file = "debugpy-1.6.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b8deaeb779699350deeed835322730a3efec170b88927debc9ba07a1a38e2585"}, - {file = "debugpy-1.6.3-cp310-cp310-win32.whl", hash = "sha256:fc233a0160f3b117b20216f1169e7211b83235e3cd6749bcdd8dbb72177030c7"}, - {file = "debugpy-1.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:dda8652520eae3945833e061cbe2993ad94a0b545aebd62e4e6b80ee616c76b2"}, - {file = "debugpy-1.6.3-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:d5c814596a170a0a58fa6fad74947e30bfd7e192a5d2d7bd6a12156c2899e13a"}, - {file = "debugpy-1.6.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c4cd6f37e3c168080d61d698390dfe2cd9e74ebf80b448069822a15dadcda57d"}, - {file = "debugpy-1.6.3-cp37-cp37m-win32.whl", hash = "sha256:3c9f985944a30cfc9ae4306ac6a27b9c31dba72ca943214dad4a0ab3840f6161"}, - {file = "debugpy-1.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:5ad571a36cec137ae6ed951d0ff75b5e092e9af6683da084753231150cbc5b25"}, - {file = "debugpy-1.6.3-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:adcfea5ea06d55d505375995e150c06445e2b20cd12885bcae566148c076636b"}, - {file = "debugpy-1.6.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:daadab4403427abd090eccb38d8901afd8b393e01fd243048fab3f1d7132abb4"}, - {file = "debugpy-1.6.3-cp38-cp38-win32.whl", hash = "sha256:6efc30325b68e451118b795eff6fe8488253ca3958251d5158106d9c87581bc6"}, - {file = "debugpy-1.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:86d784b72c5411c833af1cd45b83d80c252b77c3bfdb43db17c441d772f4c734"}, - {file = "debugpy-1.6.3-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:4e255982552b0edfe3a6264438dbd62d404baa6556a81a88f9420d3ed79b06ae"}, - {file = "debugpy-1.6.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cca23cb6161ac89698d629d892520327dd1be9321c0960e610bbcb807232b45d"}, - {file = "debugpy-1.6.3-cp39-cp39-win32.whl", hash = "sha256:7c302095a81be0d5c19f6529b600bac971440db3e226dce85347cc27e6a61908"}, - {file = "debugpy-1.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:34d2cdd3a7c87302ba5322b86e79c32c2115be396f3f09ca13306d8a04fe0f16"}, - {file = "debugpy-1.6.3-py2.py3-none-any.whl", hash = "sha256:84c39940a0cac410bf6aa4db00ba174f973eef521fbe9dd058e26bcabad89c4f"}, - {file = "debugpy-1.6.3.zip", hash = "sha256:e8922090514a890eec99cfb991bab872dd2e353ebb793164d5f01c362b9a40bf"}, + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "debugpy" +version = "1.8.2" +description = "An implementation of the Debug Adapter Protocol for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "debugpy-1.8.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:7ee2e1afbf44b138c005e4380097d92532e1001580853a7cb40ed84e0ef1c3d2"}, + {file = "debugpy-1.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f8c3f7c53130a070f0fc845a0f2cee8ed88d220d6b04595897b66605df1edd6"}, + {file = "debugpy-1.8.2-cp310-cp310-win32.whl", hash = "sha256:f179af1e1bd4c88b0b9f0fa153569b24f6b6f3de33f94703336363ae62f4bf47"}, + {file = "debugpy-1.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:0600faef1d0b8d0e85c816b8bb0cb90ed94fc611f308d5fde28cb8b3d2ff0fe3"}, + {file = "debugpy-1.8.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8a13417ccd5978a642e91fb79b871baded925d4fadd4dfafec1928196292aa0a"}, + {file = "debugpy-1.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acdf39855f65c48ac9667b2801234fc64d46778021efac2de7e50907ab90c634"}, + {file = "debugpy-1.8.2-cp311-cp311-win32.whl", hash = "sha256:2cbd4d9a2fc5e7f583ff9bf11f3b7d78dfda8401e8bb6856ad1ed190be4281ad"}, + {file = "debugpy-1.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:d3408fddd76414034c02880e891ea434e9a9cf3a69842098ef92f6e809d09afa"}, + {file = "debugpy-1.8.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:5d3ccd39e4021f2eb86b8d748a96c766058b39443c1f18b2dc52c10ac2757835"}, + {file = "debugpy-1.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62658aefe289598680193ff655ff3940e2a601765259b123dc7f89c0239b8cd3"}, + {file = "debugpy-1.8.2-cp312-cp312-win32.whl", hash = "sha256:bd11fe35d6fd3431f1546d94121322c0ac572e1bfb1f6be0e9b8655fb4ea941e"}, + {file = "debugpy-1.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:15bc2f4b0f5e99bf86c162c91a74c0631dbd9cef3c6a1d1329c946586255e859"}, + {file = "debugpy-1.8.2-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:5a019d4574afedc6ead1daa22736c530712465c0c4cd44f820d803d937531b2d"}, + {file = "debugpy-1.8.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40f062d6877d2e45b112c0bbade9a17aac507445fd638922b1a5434df34aed02"}, + {file = "debugpy-1.8.2-cp38-cp38-win32.whl", hash = "sha256:c78ba1680f1015c0ca7115671fe347b28b446081dada3fedf54138f44e4ba031"}, + {file = "debugpy-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:cf327316ae0c0e7dd81eb92d24ba8b5e88bb4d1b585b5c0d32929274a66a5210"}, + {file = "debugpy-1.8.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:1523bc551e28e15147815d1397afc150ac99dbd3a8e64641d53425dba57b0ff9"}, + {file = "debugpy-1.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e24ccb0cd6f8bfaec68d577cb49e9c680621c336f347479b3fce060ba7c09ec1"}, + {file = "debugpy-1.8.2-cp39-cp39-win32.whl", hash = "sha256:7f8d57a98c5a486c5c7824bc0b9f2f11189d08d73635c326abef268f83950326"}, + {file = "debugpy-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:16c8dcab02617b75697a0a925a62943e26a0330da076e2a10437edd9f0bf3755"}, + {file = "debugpy-1.8.2-py2.py3-none-any.whl", hash = "sha256:16e16df3a98a35c63c3ab1e4d19be4cbc7fdda92d9ddc059294f18910928e0ca"}, + {file = "debugpy-1.8.2.zip", hash = "sha256:95378ed08ed2089221896b9b3a8d021e642c24edc8fef20e5d4342ca8be65c00"}, ] -decorator = [ + +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +optional = false +python-versions = ">=3.5" +files = [ {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, ] -defusedxml = [ + +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, ] -deprecated = [ - {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, - {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, + +[[package]] +name = "deprecated" +version = "1.2.14" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"}, + {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"}, ] -docutils = [ - {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"}, - {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] + +[[package]] +name = "docutils" +version = "0.21.2" +description = "Docutils -- Python Documentation Utilities" +optional = true +python-versions = ">=3.9" +files = [ + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, ] -easygui = [ + +[[package]] +name = "easygui" +version = "0.98.3" +description = "EasyGUI is a module for very simple, very easy GUI programming in Python. EasyGUI is different from other GUI generators in that EasyGUI is NOT event-driven. Instead, all GUI interactions are invoked by simple function calls." +optional = true +python-versions = "*" +files = [ {file = "easygui-0.98.3-py2.py3-none-any.whl", hash = "sha256:33498710c68b5376b459cd3fc48d1d1f33822139eb3ed01defbc0528326da3ba"}, {file = "easygui-0.98.3.tar.gz", hash = "sha256:d653ff79ee1f42f63b5a090f2f98ce02335d86ad8963b3ce2661805cafe99a04"}, ] -ebcdic = [ + +[[package]] +name = "ebcdic" +version = "1.1.1" +description = "Additional EBCDIC codecs" +optional = true +python-versions = "*" +files = [ {file = "ebcdic-1.1.1-py2.py3-none-any.whl", hash = "sha256:33b4cb729bc2d0bf46cc1847b0e5946897cb8d3f53520c5b9aa5fa98d7e735f1"}, ] -entrypoints = [ - {file = "entrypoints-0.4-py3-none-any.whl", hash = "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f"}, - {file = "entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4"}, + +[[package]] +name = "exceptiongroup" +version = "1.2.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, + {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, ] -exceptiongroup = [ - {file = "exceptiongroup-1.0.4-py3-none-any.whl", hash = "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828"}, - {file = "exceptiongroup-1.0.4.tar.gz", hash = "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec"}, + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "executing" +version = "2.0.1" +description = "Get the currently executing AST node of a frame, and other information" +optional = false +python-versions = ">=3.5" +files = [ + {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, + {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, ] -extract-msg = [ - {file = "extract_msg-0.37.1-py2.py3-none-any.whl", hash = "sha256:0bac3b8f25d81ac5d57fcfeed5e1350dcd29a438d5ac8247a247f0c217516377"}, - {file = "extract_msg-0.37.1.tar.gz", hash = "sha256:3ec88641d799065daebc02076cc17fdc381f79baf6e90994effd01523727bc7c"}, + +[package.extras] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] + +[[package]] +name = "extract-msg" +version = "0.48.5" +description = "Extracts emails and attachments saved in Microsoft Outlook's .msg files" +optional = true +python-versions = ">=3.8" +files = [ + {file = "extract_msg-0.48.5-py3-none-any.whl", hash = "sha256:36f89ee19521e1bc0f3f0f9628423f0285fde1180b62cc9e61f20d5b22e780f1"}, + {file = "extract_msg-0.48.5.tar.gz", hash = "sha256:16f097a6455d9d038d67d7a063bf391b33d7d1eb9684a2d04b56b13fdf3053ac"}, ] -fastjsonschema = [ - {file = "fastjsonschema-2.16.2-py3-none-any.whl", hash = "sha256:21f918e8d9a1a4ba9c22e09574ba72267a6762d47822db9add95f6454e51cc1c"}, - {file = "fastjsonschema-2.16.2.tar.gz", hash = "sha256:01e366f25d9047816fe3d288cbfc3e10541daf0af2044763f3d0ade42476da18"}, + +[package.dependencies] +beautifulsoup4 = ">=4.11.1,<4.13" +compressed-rtf = ">=1.0.6,<2" +ebcdic = ">=1.1.1,<2" +olefile = "0.47" +red-black-tree-mod = "1.20" +RTFDE = ">=0.1.1,<0.2" +tzlocal = ">=4.2,<6" + +[package.extras] +all = ["extract-msg[encoding]", "extract-msg[image]", "extract-msg[mime]"] +encoding = ["chardet (>=3.0.0,<6)"] +image = ["Pillow (>=9.5.0,<10)"] +mime = ["python-magic (>=0.4.27,<0.5)"] +readthedocs = ["sphinx-rtd-theme"] + +[[package]] +name = "fastjsonschema" +version = "2.20.0" +description = "Fastest Python implementation of JSON schema" +optional = false +python-versions = "*" +files = [ + {file = "fastjsonschema-2.20.0-py3-none-any.whl", hash = "sha256:5875f0b0fa7a0043a91e93a9b8f793bcbbba9691e7fd83dca95c28ba26d21f0a"}, + {file = "fastjsonschema-2.20.0.tar.gz", hash = "sha256:3d48fc5300ee96f5d116f10fe6f28d938e6008f59a6a025c2649475b87f76a23"}, ] -idna = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + +[package.extras] +devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] + +[[package]] +name = "fqdn" +version = "1.5.1" +description = "Validates fully-qualified domain names against RFC 1123, so that they are acceptable to modern bowsers" +optional = false +python-versions = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4, <4" +files = [ + {file = "fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014"}, + {file = "fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f"}, ] -imagesize = [ + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.5" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, + {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.26.0)"] + +[[package]] +name = "httpx" +version = "0.27.0" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, + {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "idna" +version = "3.7" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] -imapclient = [ - {file = "IMAPClient-2.3.1-py2.py3-none-any.whl", hash = "sha256:057f28025d2987c63e065afb0e4370b0b850b539b0e1494cea0427e88130108c"}, - {file = "IMAPClient-2.3.1.zip", hash = "sha256:26ea995664fae3a88b878ebce2aff7402931697b86658b7882043ddb01b0e6ba"}, + +[[package]] +name = "importlib-metadata" +version = "8.0.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"}, + {file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"}, ] -importlib-metadata = [ - {file = "importlib_metadata-5.1.0-py3-none-any.whl", hash = "sha256:d84d17e21670ec07990e1044a99efe8d615d860fd176fc29ef5c306068fda313"}, - {file = "importlib_metadata-5.1.0.tar.gz", hash = "sha256:d5059f9f1e8e41f80e9c56c2ee58811450c31984dfa625329ffd7c0dad88a73b"}, + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "importlib-resources" +version = "6.4.0" +description = "Read resources from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"}, + {file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"}, ] -importlib-resources = [ - {file = "importlib_resources-5.10.0-py3-none-any.whl", hash = "sha256:ee17ec648f85480d523596ce49eae8ead87d5631ae1551f913c0100b5edd3437"}, - {file = "importlib_resources-5.10.0.tar.gz", hash = "sha256:c01b1b94210d9849f286b86bb51bcea7cd56dde0600d8db721d7b81330711668"}, + +[package.dependencies] +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] -iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, + +[[package]] +name = "ipykernel" +version = "6.29.5" +description = "IPython Kernel for Jupyter" +optional = false +python-versions = ">=3.8" +files = [ + {file = "ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5"}, + {file = "ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215"}, ] -ipykernel = [ - {file = "ipykernel-6.16.2-py3-none-any.whl", hash = "sha256:67daf93e5b52456cd8eea87a8b59405d2bb80ae411864a1ea206c3631d8179af"}, - {file = "ipykernel-6.16.2.tar.gz", hash = "sha256:463f3d87a92e99969b1605cb7a5b4d7b36b7145a0e72d06e65918a6ddefbe630"}, + +[package.dependencies] +appnope = {version = "*", markers = "platform_system == \"Darwin\""} +comm = ">=0.1.1" +debugpy = ">=1.6.5" +ipython = ">=7.23.1" +jupyter-client = ">=6.1.12" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +matplotlib-inline = ">=0.1" +nest-asyncio = "*" +packaging = "*" +psutil = "*" +pyzmq = ">=24" +tornado = ">=6.1" +traitlets = ">=5.4.0" + +[package.extras] +cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"] +pyqt5 = ["pyqt5"] +pyside6 = ["pyside6"] +test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.23.5)", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "ipython" +version = "8.12.3" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.8" +files = [ + {file = "ipython-8.12.3-py3-none-any.whl", hash = "sha256:b0340d46a933d27c657b211a329d0be23793c36595acf9e6ef4164bc01a1804c"}, + {file = "ipython-8.12.3.tar.gz", hash = "sha256:3910c4b54543c2ad73d06579aa771041b7d5707b033bd488669b4cf544e3b363"}, ] -ipython = [ - {file = "ipython-7.34.0-py3-none-any.whl", hash = "sha256:c175d2440a1caff76116eb719d40538fbb316e214eda85c5515c303aacbfb23e"}, - {file = "ipython-7.34.0.tar.gz", hash = "sha256:af3bdb46aa292bce5615b1b2ebc76c2080c5f77f54bda2ec72461317273e7cd6"}, + +[package.dependencies] +appnope = {version = "*", markers = "sys_platform == \"darwin\""} +backcall = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +pickleshare = "*" +prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5" +typing-extensions = {version = "*", markers = "python_version < \"3.10\""} + +[package.extras] +all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +black = ["black"] +doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] + +[[package]] +name = "ipython" +version = "8.18.1" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.9" +files = [ + {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, + {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, ] -ipython-genutils = [ - {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, - {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +prompt-toolkit = ">=3.0.41,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5" +typing-extensions = {version = "*", markers = "python_version < \"3.10\""} + +[package.extras] +all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +black = ["black"] +doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"] + +[[package]] +name = "ipython" +version = "8.26.0" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.10" +files = [ + {file = "ipython-8.26.0-py3-none-any.whl", hash = "sha256:e6b347c27bdf9c32ee9d31ae85defc525755a1869f14057e900675b9e8d6e6ff"}, + {file = "ipython-8.26.0.tar.gz", hash = "sha256:1cec0fbba8404af13facebe83d04436a7434c7400e59f47acf467c64abd0956c"}, ] -jedi = [ - {file = "jedi-0.18.2-py2.py3-none-any.whl", hash = "sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e"}, - {file = "jedi-0.18.2.tar.gz", hash = "sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612"}, + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} +prompt-toolkit = ">=3.0.41,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5.13.0" +typing-extensions = {version = ">=4.6", markers = "python_version < \"3.12\""} + +[package.extras] +all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"] +black = ["black"] +doc = ["docrepr", "exceptiongroup", "intersphinx-registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli", "typing-extensions"] +kernel = ["ipykernel"] +matplotlib = ["matplotlib"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["packaging", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] + +[[package]] +name = "isoduration" +version = "20.11.0" +description = "Operations with ISO 8601 durations" +optional = false +python-versions = ">=3.7" +files = [ + {file = "isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042"}, + {file = "isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9"}, ] -jinja2 = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, + +[package.dependencies] +arrow = ">=0.15.0" + +[[package]] +name = "jedi" +version = "0.19.1" +description = "An autocompletion tool for Python that can be used for text editors." +optional = false +python-versions = ">=3.6" +files = [ + {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, + {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, ] -json5 = [ - {file = "json5-0.9.10-py2.py3-none-any.whl", hash = "sha256:993189671e7412e9cdd8be8dc61cf402e8e579b35f1d1bb20ae6b09baa78bbce"}, - {file = "json5-0.9.10.tar.gz", hash = "sha256:ad9f048c5b5a4c3802524474ce40a622fae789860a86f10cc4f7e5f9cf9b46ab"}, + +[package.dependencies] +parso = ">=0.8.3,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] + +[[package]] +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, ] -jsonschema = [ - {file = "jsonschema-4.17.1-py3-none-any.whl", hash = "sha256:410ef23dcdbca4eaedc08b850079179883c2ed09378bd1f760d4af4aacfa28d7"}, - {file = "jsonschema-4.17.1.tar.gz", hash = "sha256:05b2d22c83640cde0b7e0aa329ca7754fbd98ea66ad8ae24aa61328dfe057fa3"}, + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "json5" +version = "0.9.25" +description = "A Python implementation of the JSON5 data format." +optional = false +python-versions = ">=3.8" +files = [ + {file = "json5-0.9.25-py3-none-any.whl", hash = "sha256:34ed7d834b1341a86987ed52f3f76cd8ee184394906b6e22a1e0deb9ab294e8f"}, + {file = "json5-0.9.25.tar.gz", hash = "sha256:548e41b9be043f9426776f05df8635a00fe06104ea51ed24b67f908856e151ae"}, ] -jupyter-client = [ - {file = "jupyter_client-7.4.7-py3-none-any.whl", hash = "sha256:df56ae23b8e1da1b66f89dee1368e948b24a7f780fa822c5735187589fc4c157"}, - {file = "jupyter_client-7.4.7.tar.gz", hash = "sha256:330f6b627e0b4bf2f54a3a0dd9e4a22d2b649c8518168afedce2c96a1ceb2860"}, + +[[package]] +name = "jsonpointer" +version = "3.0.0" +description = "Identify specific nodes in a JSON document (RFC 6901)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, + {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, ] -jupyter-core = [ - {file = "jupyter_core-4.11.2-py3-none-any.whl", hash = "sha256:3815e80ec5272c0c19aad087a0d2775df2852cfca8f5a17069e99c9350cecff8"}, - {file = "jupyter_core-4.11.2.tar.gz", hash = "sha256:c2909b9bc7dca75560a6c5ae78c34fd305ede31cd864da3c0d0bb2ed89aa9337"}, + +[[package]] +name = "jsonschema" +version = "4.22.0" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema-4.22.0-py3-none-any.whl", hash = "sha256:ff4cfd6b1367a40e7bc6411caec72effadd3db0bbe5017de188f2d6108335802"}, + {file = "jsonschema-4.22.0.tar.gz", hash = "sha256:5b22d434a45935119af990552c862e5d6d564e8f6601206b305a61fdf661a2b7"}, ] -jupyter-server = [ - {file = "jupyter_server-1.23.3-py3-none-any.whl", hash = "sha256:438496cac509709cc85e60172e5538ca45b4c8a0862bb97cd73e49f2ace419cb"}, - {file = "jupyter_server-1.23.3.tar.gz", hash = "sha256:f7f7a2f9d36f4150ad125afef0e20b1c76c8ff83eb5e39fb02d3b9df0f9b79ab"}, + +[package.dependencies] +attrs = ">=22.2.0" +fqdn = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +idna = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} +isoduration = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format-nongpl\""} +jsonschema-specifications = ">=2023.03.6" +pkgutil-resolve-name = {version = ">=1.3.10", markers = "python_version < \"3.9\""} +referencing = ">=0.28.4" +rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +rfc3986-validator = {version = ">0.1.0", optional = true, markers = "extra == \"format-nongpl\""} +rpds-py = ">=0.7.1" +uri-template = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +webcolors = {version = ">=1.11", optional = true, markers = "extra == \"format-nongpl\""} + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] + +[[package]] +name = "jsonschema-specifications" +version = "2023.12.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, + {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, ] -jupyterlab = [ - {file = "jupyterlab-3.5.0-py3-none-any.whl", hash = "sha256:f433059fe0e12d75ea90a81a0b6721113bb132857e3ec2197780b6fe84cbcbde"}, - {file = "jupyterlab-3.5.0.tar.gz", hash = "sha256:e02556c8ea1b386963c4b464e4618aee153c5416b07ab481425c817a033323a2"}, + +[package.dependencies] +importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} +referencing = ">=0.31.0" + +[[package]] +name = "jupyter-client" +version = "8.6.2" +description = "Jupyter protocol implementation and client libraries" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_client-8.6.2-py3-none-any.whl", hash = "sha256:50cbc5c66fd1b8f65ecb66bc490ab73217993632809b6e505687de18e9dea39f"}, + {file = "jupyter_client-8.6.2.tar.gz", hash = "sha256:2bda14d55ee5ba58552a8c53ae43d215ad9868853489213f37da060ced54d8df"}, ] -jupyterlab-pygments = [ - {file = "jupyterlab_pygments-0.2.2-py2.py3-none-any.whl", hash = "sha256:2405800db07c9f770863bcf8049a529c3dd4d3e28536638bd7c1c01d2748309f"}, - {file = "jupyterlab_pygments-0.2.2.tar.gz", hash = "sha256:7405d7fde60819d905a9fa8ce89e4cd830e318cdad22a0030f7a901da705585d"}, + +[package.dependencies] +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +python-dateutil = ">=2.8.2" +pyzmq = ">=23.0" +tornado = ">=6.2" +traitlets = ">=5.3" + +[package.extras] +docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] + +[[package]] +name = "jupyter-core" +version = "5.7.2" +description = "Jupyter core package. A base package on which Jupyter projects rely." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409"}, + {file = "jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9"}, ] -jupyterlab-server = [ - {file = "jupyterlab_server-2.16.3-py3-none-any.whl", hash = "sha256:d18eb623428b4ee732c2258afaa365eedd70f38b609981ea040027914df32bc6"}, - {file = "jupyterlab_server-2.16.3.tar.gz", hash = "sha256:635a0b176a901f19351c02221a124e59317c476f511200409b7d867e8b2905c3"}, + +[package.dependencies] +platformdirs = ">=2.5" +pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} +traitlets = ">=5.3" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"] +test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "jupyter-events" +version = "0.10.0" +description = "Jupyter Event System library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_events-0.10.0-py3-none-any.whl", hash = "sha256:4b72130875e59d57716d327ea70d3ebc3af1944d3717e5a498b8a06c6c159960"}, + {file = "jupyter_events-0.10.0.tar.gz", hash = "sha256:670b8229d3cc882ec782144ed22e0d29e1c2d639263f92ca8383e66682845e22"}, ] -lark-parser = [ - {file = "lark-parser-0.12.0.tar.gz", hash = "sha256:15967db1f1214013dca65b1180745047b9be457d73da224fcda3d9dd4e96a138"}, - {file = "lark_parser-0.12.0-py2.py3-none-any.whl", hash = "sha256:0eaf30cb5ba787fe404d73a7d6e61df97b21d5a63ac26c5008c78a494373c675"}, + +[package.dependencies] +jsonschema = {version = ">=4.18.0", extras = ["format-nongpl"]} +python-json-logger = ">=2.0.4" +pyyaml = ">=5.3" +referencing = "*" +rfc3339-validator = "*" +rfc3986-validator = ">=0.1.1" +traitlets = ">=5.3" + +[package.extras] +cli = ["click", "rich"] +docs = ["jupyterlite-sphinx", "myst-parser", "pydata-sphinx-theme", "sphinxcontrib-spelling"] +test = ["click", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.19.0)", "pytest-console-scripts", "rich"] + +[[package]] +name = "jupyter-lsp" +version = "2.2.5" +description = "Multi-Language Server WebSocket proxy for Jupyter Notebook/Lab server" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter-lsp-2.2.5.tar.gz", hash = "sha256:793147a05ad446f809fd53ef1cd19a9f5256fd0a2d6b7ce943a982cb4f545001"}, + {file = "jupyter_lsp-2.2.5-py3-none-any.whl", hash = "sha256:45fbddbd505f3fbfb0b6cb2f1bc5e15e83ab7c79cd6e89416b248cb3c00c11da"}, ] -lief = [ - {file = "lief-0.12.3-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:6d18aafa2028587c98f6d4387bec94346e92f2b5a8a5002f70b1cf35b1c045cc"}, - {file = "lief-0.12.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c078d6230279ffd3bca717c79664fb8368666f610b577deb24b374607936e9c1"}, - {file = "lief-0.12.3-cp310-cp310-win32.whl", hash = "sha256:e3a6af926532d0aac9e7501946134513d63217bacba666e6f7f5a0b7e15ba236"}, - {file = "lief-0.12.3-cp310-cp310-win_amd64.whl", hash = "sha256:0750b72e3aa161e1fb0e2e7f571121ae05d2428aafd742ff05a7656ad2288447"}, - {file = "lief-0.12.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:8bc58fa26a830df6178e36f112cb2bbdd65deff593f066d2d51434ff78386ba5"}, - {file = "lief-0.12.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04eb6b70d646fb5bd6183575928ee23715550f161f2832cbcd8c6ff2071fb408"}, - {file = "lief-0.12.3-cp311-cp311-win32.whl", hash = "sha256:7e2d0a53c403769b04adcf8df92e83c5e25f9103a052aa7f17b0a9cf057735fb"}, - {file = "lief-0.12.3-cp311-cp311-win_amd64.whl", hash = "sha256:7f6395c12ee1bc4a5162f567cba96d0c72dfb660e7902e84d4f3029daf14fe33"}, - {file = "lief-0.12.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:71327fdc764fd2b1f3cd371d8ac5e0b801bde32b71cfcf7dccee506d46768539"}, - {file = "lief-0.12.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d320fb80ed5b42b354b8e4f251ab05a51929c162c57c377b5e95ad4b1c1b415d"}, - {file = "lief-0.12.3-cp36-cp36m-win32.whl", hash = "sha256:176fa6c342dd480195cda34a20f62ac76dfae103b22ca7583b762e0b434ee1f3"}, - {file = "lief-0.12.3-cp36-cp36m-win_amd64.whl", hash = "sha256:3a18fe108fb82a2640864deef933731afe77413b1226551796ef2c373a1b3a2a"}, - {file = "lief-0.12.3-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:c73e990cd2737d1060b8c1e8edcc128832806995b69d1d6bf191409e2cea7bde"}, - {file = "lief-0.12.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:5fa2b1c8ffe47ee66b2507c2bb4e3fd628965532b7888c0627d10e690b5ef20c"}, - {file = "lief-0.12.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f224e9a261e88099f86160f121d088d30894c2946e3e551cf11c678daadcf2b"}, - {file = "lief-0.12.3-cp37-cp37m-win32.whl", hash = "sha256:3481d7c9fb3d3a1acff53851f40efd1a5a05d354312d367294bc2e310b736826"}, - {file = "lief-0.12.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4e5173e1be5ebf43594f4eb187cbcb04758761942bc0a1e685ea1cb9047dc0d9"}, - {file = "lief-0.12.3-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:54d6a45e01260b9c8bf1c99f58257cff5338aee5c02eacfeee789f9d15cf38c6"}, - {file = "lief-0.12.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:4501dc399fb15dc7a3c8df4a76264a86be6d581d99098dafc3a67626149d8ff1"}, - {file = "lief-0.12.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c848aadac0816268aeb9dde7cefdb54bf24f78e664a19e97e74c92d3be1bb147"}, - {file = "lief-0.12.3-cp38-cp38-win32.whl", hash = "sha256:d7e35f9ee9dd6e79add3b343f83659b71c05189e5cb224e02a1902ddc7654e96"}, - {file = "lief-0.12.3-cp38-cp38-win_amd64.whl", hash = "sha256:b00667257b43e93d94166c959055b6147d46d302598f3ee55c194b40414c89cc"}, - {file = "lief-0.12.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:ae773196df814202c0c51056163a1478941b299512b09660a3c37be3c7fac81e"}, - {file = "lief-0.12.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:4a47f410032c63ac3be051d963d0337d6b47f0e94bfe8e946ab4b6c428f4d0f8"}, - {file = "lief-0.12.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbd11367c2259bd1131a6c8755dcde33314324de5ea029227bfbc7d3755871e6"}, - {file = "lief-0.12.3-cp39-cp39-win32.whl", hash = "sha256:2ce53e311918c3e5b54c815ef420a747208d2a88200c41cd476f3dd1eb876bcf"}, - {file = "lief-0.12.3-cp39-cp39-win_amd64.whl", hash = "sha256:446e53ccf0ebd1616c5d573470662ff71ca6df3cd62ec1764e303764f3f03cca"}, - {file = "lief-0.12.3.zip", hash = "sha256:62e81d2f1a827d43152aed12446a604627e8833493a51dca027026eed0ce7128"}, + +[package.dependencies] +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} +jupyter-server = ">=1.1.2" + +[[package]] +name = "jupyter-server" +version = "2.14.1" +description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_server-2.14.1-py3-none-any.whl", hash = "sha256:16f7177c3a4ea8fe37784e2d31271981a812f0b2874af17339031dc3510cc2a5"}, + {file = "jupyter_server-2.14.1.tar.gz", hash = "sha256:12558d158ec7a0653bf96cc272bc7ad79e0127d503b982ed144399346694f726"}, ] -markupsafe = [ - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, - {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, + +[package.dependencies] +anyio = ">=3.1.0" +argon2-cffi = ">=21.1" +jinja2 = ">=3.0.3" +jupyter-client = ">=7.4.4" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +jupyter-events = ">=0.9.0" +jupyter-server-terminals = ">=0.4.4" +nbconvert = ">=6.4.4" +nbformat = ">=5.3.0" +overrides = ">=5.0" +packaging = ">=22.0" +prometheus-client = ">=0.9" +pywinpty = {version = ">=2.0.1", markers = "os_name == \"nt\""} +pyzmq = ">=24" +send2trash = ">=1.8.2" +terminado = ">=0.8.3" +tornado = ">=6.2.0" +traitlets = ">=5.6.0" +websocket-client = ">=1.7" + +[package.extras] +docs = ["ipykernel", "jinja2", "jupyter-client", "myst-parser", "nbformat", "prometheus-client", "pydata-sphinx-theme", "send2trash", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-openapi (>=0.8.0)", "sphinxcontrib-spelling", "sphinxemoji", "tornado", "typing-extensions"] +test = ["flaky", "ipykernel", "pre-commit", "pytest (>=7.0,<9)", "pytest-console-scripts", "pytest-jupyter[server] (>=0.7)", "pytest-timeout", "requests"] + +[[package]] +name = "jupyter-server-terminals" +version = "0.5.3" +description = "A Jupyter Server Extension Providing Terminals." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_server_terminals-0.5.3-py3-none-any.whl", hash = "sha256:41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa"}, + {file = "jupyter_server_terminals-0.5.3.tar.gz", hash = "sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269"}, ] -matplotlib-inline = [ - {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, - {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, + +[package.dependencies] +pywinpty = {version = ">=2.0.3", markers = "os_name == \"nt\""} +terminado = ">=0.8.3" + +[package.extras] +docs = ["jinja2", "jupyter-server", "mistune (<4.0)", "myst-parser", "nbformat", "packaging", "pydata-sphinx-theme", "sphinxcontrib-github-alt", "sphinxcontrib-openapi", "sphinxcontrib-spelling", "sphinxemoji", "tornado"] +test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (>=0.5.3)", "pytest-timeout"] + +[[package]] +name = "jupyterlab" +version = "4.2.3" +description = "JupyterLab computational environment" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyterlab-4.2.3-py3-none-any.whl", hash = "sha256:0b59d11808e84bb84105c73364edfa867dd475492429ab34ea388a52f2e2e596"}, + {file = "jupyterlab-4.2.3.tar.gz", hash = "sha256:df6e46969ea51d66815167f23d92f105423b7f1f06fa604d4f44aeb018c82c7b"}, ] -mistune = [ - {file = "mistune-2.0.4-py2.py3-none-any.whl", hash = "sha256:182cc5ee6f8ed1b807de6b7bb50155df7b66495412836b9a74c8fbdfc75fe36d"}, - {file = "mistune-2.0.4.tar.gz", hash = "sha256:9ee0a66053e2267aba772c71e06891fa8f1af6d4b01d5e84e267b4570d4d9808"}, + +[package.dependencies] +async-lru = ">=1.0.0" +httpx = ">=0.25.0" +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} +importlib-resources = {version = ">=1.4", markers = "python_version < \"3.9\""} +ipykernel = ">=6.5.0" +jinja2 = ">=3.0.3" +jupyter-core = "*" +jupyter-lsp = ">=2.0.0" +jupyter-server = ">=2.4.0,<3" +jupyterlab-server = ">=2.27.1,<3" +notebook-shim = ">=0.2" +packaging = "*" +setuptools = ">=40.1.0" +tomli = {version = ">=1.2.2", markers = "python_version < \"3.11\""} +tornado = ">=6.2.0" +traitlets = "*" + +[package.extras] +dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.3.5)"] +docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<7.3.0)", "sphinx-copybutton"] +docs-screenshots = ["altair (==5.3.0)", "ipython (==8.16.1)", "ipywidgets (==8.1.2)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.1.post2)", "matplotlib (==3.8.3)", "nbconvert (>=7.0.0)", "pandas (==2.2.1)", "scipy (==1.12.0)", "vega-datasets (==0.9.0)"] +test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"] +upgrade-extension = ["copier (>=8,<10)", "jinja2-time (<0.3)", "pydantic (<2.0)", "pyyaml-include (<2.0)", "tomli-w (<2.0)"] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +description = "Pygments theme using JupyterLab CSS variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780"}, + {file = "jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d"}, ] -msoffcrypto-tool = [ - {file = "msoffcrypto-tool-5.0.0.tar.gz", hash = "sha256:34cbdb3efe62d9ca08aa59aadb1dc7d46a8ec2fb4befb89807f2d3c00b9c3ede"}, - {file = "msoffcrypto_tool-5.0.0-py3-none-any.whl", hash = "sha256:4fe95a7a4525d6261ff7373a2027b97308ec2302a40a6718b34dffbc738c00c9"}, + +[[package]] +name = "jupyterlab-server" +version = "2.27.2" +description = "A set of server components for JupyterLab and JupyterLab like applications." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyterlab_server-2.27.2-py3-none-any.whl", hash = "sha256:54aa2d64fd86383b5438d9f0c032f043c4d8c0264b8af9f60bd061157466ea43"}, + {file = "jupyterlab_server-2.27.2.tar.gz", hash = "sha256:15cbb349dc45e954e09bacf81b9f9bcb10815ff660fb2034ecd7417db3a7ea27"}, ] -mypy = [ - {file = "mypy-0.991-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab"}, - {file = "mypy-0.991-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d"}, - {file = "mypy-0.991-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6"}, - {file = "mypy-0.991-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb"}, - {file = "mypy-0.991-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305"}, - {file = "mypy-0.991-cp310-cp310-win_amd64.whl", hash = "sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c"}, - {file = "mypy-0.991-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372"}, - {file = "mypy-0.991-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f"}, - {file = "mypy-0.991-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33"}, - {file = "mypy-0.991-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05"}, - {file = "mypy-0.991-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad"}, - {file = "mypy-0.991-cp311-cp311-win_amd64.whl", hash = "sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297"}, - {file = "mypy-0.991-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813"}, - {file = "mypy-0.991-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711"}, - {file = "mypy-0.991-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd"}, - {file = "mypy-0.991-cp37-cp37m-win_amd64.whl", hash = "sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef"}, - {file = "mypy-0.991-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a"}, - {file = "mypy-0.991-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93"}, - {file = "mypy-0.991-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf"}, - {file = "mypy-0.991-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135"}, - {file = "mypy-0.991-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70"}, - {file = "mypy-0.991-cp38-cp38-win_amd64.whl", hash = "sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243"}, - {file = "mypy-0.991-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d"}, - {file = "mypy-0.991-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5"}, - {file = "mypy-0.991-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3"}, - {file = "mypy-0.991-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648"}, - {file = "mypy-0.991-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476"}, - {file = "mypy-0.991-cp39-cp39-win_amd64.whl", hash = "sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461"}, - {file = "mypy-0.991-py3-none-any.whl", hash = "sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb"}, - {file = "mypy-0.991.tar.gz", hash = "sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06"}, + +[package.dependencies] +babel = ">=2.10" +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} +jinja2 = ">=3.0.3" +json5 = ">=0.9.0" +jsonschema = ">=4.18.0" +jupyter-server = ">=1.21,<3" +packaging = ">=21.3" +requests = ">=2.31" + +[package.extras] +docs = ["autodoc-traits", "jinja2 (<3.2.0)", "mistune (<4)", "myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-copybutton", "sphinxcontrib-openapi (>0.8)"] +openapi = ["openapi-core (>=0.18.0,<0.19.0)", "ruamel-yaml"] +test = ["hatch", "ipykernel", "openapi-core (>=0.18.0,<0.19.0)", "openapi-spec-validator (>=0.6.0,<0.8.0)", "pytest (>=7.0,<8)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter[server] (>=0.6.2)", "pytest-timeout", "requests-mock", "ruamel-yaml", "sphinxcontrib-spelling", "strict-rfc3339", "werkzeug"] + +[[package]] +name = "lark" +version = "1.1.9" +description = "a modern parsing library" +optional = true +python-versions = ">=3.6" +files = [ + {file = "lark-1.1.9-py3-none-any.whl", hash = "sha256:a0dd3a87289f8ccbb325901e4222e723e7d745dbfc1803eaf5f3d2ace19cf2db"}, + {file = "lark-1.1.9.tar.gz", hash = "sha256:15fa5236490824c2c4aba0e22d2d6d823575dcaf4cdd1848e34b6ad836240fba"}, ] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, + +[package.extras] +atomic-cache = ["atomicwrites"] +interegular = ["interegular (>=0.3.1,<0.4.0)"] +nearley = ["js2py"] +regex = ["regex"] + +[[package]] +name = "lief" +version = "0.14.1" +description = "Library to instrument executable formats" +optional = true +python-versions = ">=3.8" +files = [ + {file = "lief-0.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a9a94882f9af110fb01b4558a58941d2352b9a4ae3fef15570a3fab921ff462"}, + {file = "lief-0.14.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:bcc06f24f64fa6f20372d625ce60c40a7a6f669e11bdd02c2f0b8c5c6d09a5ee"}, + {file = "lief-0.14.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:d22f804eee7f1b4a4b37e7a3d35e2003c4c054f3450d40389e54c8ac9fc2a5db"}, + {file = "lief-0.14.1-cp310-cp310-manylinux_2_28_x86_64.manylinux_2_27_x86_64.whl", hash = "sha256:26134815adecfd7f15dfbdf12cc64df25bcf3d0db917cf115fc3b296d09be496"}, + {file = "lief-0.14.1-cp310-cp310-win32.whl", hash = "sha256:6ca0220189698599df30b8044f43fb1fc7ba919fb9ef6047c892f9faee16393a"}, + {file = "lief-0.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:c321234b50997c217107c09e69f53518c37fac637f8735c968c258dd4c748fb2"}, + {file = "lief-0.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ca365c704c6b6b1ce631b92fea2eddaf93d66c897a0ec4ab51e9ab9e3345920"}, + {file = "lief-0.14.1-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:1f3c40eadff07a4c8fa74f1e268f9fa70b68f39b6795a00cd82160ca6782d5c3"}, + {file = "lief-0.14.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:c202ed13b641db2e1f8a24743fb0c85595b32ea92cc3c517d3f7a9977e16dcb4"}, + {file = "lief-0.14.1-cp311-cp311-manylinux_2_28_x86_64.manylinux_2_27_x86_64.whl", hash = "sha256:fd481bfdfef04e8be4d200bca771d0d9394d9146c6cd403f9e58c80c4196a24e"}, + {file = "lief-0.14.1-cp311-cp311-win32.whl", hash = "sha256:473e9a37beef8db8bab1a777271aa49cce44dfe35af65cb8fad576377518c0bd"}, + {file = "lief-0.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:24f687244e14d4a8307430babc5c712a1dd4e519172886ad4aeb9825f88f7569"}, + {file = "lief-0.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6df40e3750b8b26f88a6b28ac01db7338cdb6158f28363c755bf36452ce20d28"}, + {file = "lief-0.14.1-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:e7f7a55db2fcf269569f9e9fa5ea752620396de17bd9d29fc8b29e176975ecdb"}, + {file = "lief-0.14.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:50795b51884b76a78c481d6d069d992561c217180bd81cf12554180389eff0a3"}, + {file = "lief-0.14.1-cp312-cp312-manylinux_2_28_x86_64.manylinux_2_27_x86_64.whl", hash = "sha256:497b88f9c9aaae999766ba188744ee35c5f38b4b64016f7dbb7037e9bf325382"}, + {file = "lief-0.14.1-cp312-cp312-win32.whl", hash = "sha256:08bad88083f696915f8dcda4042a3bfc514e17462924ec8984085838b2261921"}, + {file = "lief-0.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:e131d6158a085f8a72124136816fefc29405c725cd3695ce22a904e471f0f815"}, + {file = "lief-0.14.1-cp313-cp313-manylinux_2_28_x86_64.manylinux_2_27_x86_64.whl", hash = "sha256:f9ff9a6959fb6d0e553cca41cd1027b609d27c5073e98d9fad8b774fbb5746c2"}, + {file = "lief-0.14.1-cp313-cp313-win32.whl", hash = "sha256:95f295a7cc68f4e14ce7ea4ff8082a04f5313c2e5e63cc2bbe9d059190b7e4d5"}, + {file = "lief-0.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:cdc1123c2e27970f8c8353505fd578e634ab33193c8d1dff36dc159e25599a40"}, + {file = "lief-0.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:df650fa05ca131e4dfeb42c77985e1eb239730af9944bc0aadb1dfac8576e0e8"}, + {file = "lief-0.14.1-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:b4e76eeb48ca2925c6ca6034d408582615f2faa855f9bb11482e7acbdecc4803"}, + {file = "lief-0.14.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:016e4fac91303466024154dd3c4b599e8b7c52882f72038b62a2be386d98c8f9"}, + {file = "lief-0.14.1-cp38-cp38-manylinux_2_28_x86_64.manylinux_2_27_x86_64.whl", hash = "sha256:9a5c7732a3ce53b306c8180ab64fdfb36d8cd9df91aedd9e2b4dad9faf47492b"}, + {file = "lief-0.14.1-cp38-cp38-win32.whl", hash = "sha256:7030c22a4446ea2ac673fd50128e9c639121c0a4dae11ca1cd8cc20d62d26e7e"}, + {file = "lief-0.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a35ceeee74bb9bb4c7171f4bca814576a3aa6dec16a0a9469e5743db0a9ba0c"}, + {file = "lief-0.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:abb15e4de34e70661fd35e87e2634abf0ae57a8c8ac78d02ad4259f5a5817e26"}, + {file = "lief-0.14.1-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:33d062340c709c1a33539d221ea3cb764cbb8d7c9ee8aae28bf9797bc8715a0b"}, + {file = "lief-0.14.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:66deb1b26de43acb2fd0b2fc5e6be70093eaaa93797332cc4613e163164c77e7"}, + {file = "lief-0.14.1-cp39-cp39-manylinux_2_28_x86_64.manylinux_2_27_x86_64.whl", hash = "sha256:c1c15bd3e5b15da6dcc0ba75d5549f15bfbf9214c0d8e3938f85877a40c352d9"}, + {file = "lief-0.14.1-cp39-cp39-win32.whl", hash = "sha256:ebcbe4eadd33d8cf2c6015f44d6c9b72f81388af745938e633c4bb90262b2036"}, + {file = "lief-0.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:2db3eb282a35daf51f89c6509226668a08fb6a6d1f507dd549dd9f077585db11"}, ] -nbclassic = [ - {file = "nbclassic-0.4.8-py3-none-any.whl", hash = "sha256:cbf05df5842b420d5cece0143462380ea9d308ff57c2dc0eb4d6e035b18fbfb3"}, - {file = "nbclassic-0.4.8.tar.gz", hash = "sha256:c74d8a500f8e058d46b576a41e5bc640711e1032cf7541dde5f73ea49497e283"}, + +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] -nbclient = [ - {file = "nbclient-0.7.0-py3-none-any.whl", hash = "sha256:434c91385cf3e53084185334d675a0d33c615108b391e260915d1aa8e86661b8"}, - {file = "nbclient-0.7.0.tar.gz", hash = "sha256:a1d844efd6da9bc39d2209bf996dbd8e07bf0f36b796edfabaa8f8a9ab77c3aa"}, + +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +description = "Inline Matplotlib backend for Jupyter" +optional = false +python-versions = ">=3.8" +files = [ + {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, + {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, ] -nbconvert = [ - {file = "nbconvert-7.2.5-py3-none-any.whl", hash = "sha256:3e90e108bb5637b5b8a1422af1156af1368b39dd25369ff7faa7dfdcdef18f81"}, - {file = "nbconvert-7.2.5.tar.gz", hash = "sha256:8fdc44fd7d9424db7fdc6e1e834a02f6b8620ffb653767388be2f9eb16f84184"}, + +[package.dependencies] +traitlets = "*" + +[[package]] +name = "mistune" +version = "3.0.2" +description = "A sane and fast Markdown parser with useful plugins and renderers" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mistune-3.0.2-py3-none-any.whl", hash = "sha256:71481854c30fdbc938963d3605b72501f5c10a9320ecd412c121c163a1c7d205"}, + {file = "mistune-3.0.2.tar.gz", hash = "sha256:fc7f93ded930c92394ef2cb6f04a8aabab4117a91449e72dcc8dfa646a508be8"}, ] -nbformat = [ - {file = "nbformat-5.7.0-py3-none-any.whl", hash = "sha256:1b05ec2c552c2f1adc745f4eddce1eac8ca9ffd59bb9fd859e827eaa031319f9"}, - {file = "nbformat-5.7.0.tar.gz", hash = "sha256:1d4760c15c1a04269ef5caf375be8b98dd2f696e5eb9e603ec2bf091f9b0d3f3"}, + +[[package]] +name = "msoffcrypto-tool" +version = "5.4.1" +description = "Python tool and library for decrypting and encrypting MS Office files using a password or other keys" +optional = true +python-versions = "<4.0,>=3.8" +files = [ + {file = "msoffcrypto_tool-5.4.1-py3-none-any.whl", hash = "sha256:08e06ca49ab00eabf0510bb52a7477c5000ae3000150d2dbe63555d770e39969"}, + {file = "msoffcrypto_tool-5.4.1.tar.gz", hash = "sha256:ae16c4979eb30ea02c8d9f0a20eae2a80652f426937be5776e31063c821e3439"}, ] -nest-asyncio = [ - {file = "nest_asyncio-1.5.6-py3-none-any.whl", hash = "sha256:b9a953fb40dceaa587d109609098db21900182b16440652454a146cffb06e8b8"}, - {file = "nest_asyncio-1.5.6.tar.gz", hash = "sha256:d267cc1ff794403f7df692964d1d2a3fa9418ffea2a3f6859a439ff482fef290"}, + +[package.dependencies] +cryptography = ">=35.0" +olefile = ">=0.46" + +[[package]] +name = "mypy" +version = "1.10.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"}, + {file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"}, + {file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"}, + {file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"}, + {file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"}, + {file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"}, + {file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"}, + {file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"}, + {file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"}, + {file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"}, + {file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"}, + {file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"}, + {file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"}, + {file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"}, + {file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"}, + {file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"}, + {file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"}, + {file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"}, + {file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"}, + {file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"}, + {file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"}, + {file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"}, + {file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"}, + {file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"}, + {file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"}, + {file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"}, + {file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"}, ] -notebook = [ - {file = "notebook-6.5.2-py3-none-any.whl", hash = "sha256:e04f9018ceb86e4fa841e92ea8fb214f8d23c1cedfde530cc96f92446924f0e4"}, - {file = "notebook-6.5.2.tar.gz", hash = "sha256:c1897e5317e225fc78b45549a6ab4b668e4c996fd03a04e938fe5e7af2bfffd0"}, + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] -notebook-shim = [ - {file = "notebook_shim-0.2.2-py3-none-any.whl", hash = "sha256:9c6c30f74c4fbea6fce55c1be58e7fd0409b1c681b075dcedceb005db5026949"}, - {file = "notebook_shim-0.2.2.tar.gz", hash = "sha256:090e0baf9a5582ff59b607af523ca2db68ff216da0c69956b62cab2ef4fc9c3f"}, + +[[package]] +name = "nbclient" +version = "0.10.0" +description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "nbclient-0.10.0-py3-none-any.whl", hash = "sha256:f13e3529332a1f1f81d82a53210322476a168bb7090a0289c795fe9cc11c9d3f"}, + {file = "nbclient-0.10.0.tar.gz", hash = "sha256:4b3f1b7dba531e498449c4db4f53da339c91d449dc11e9af3a43b4eb5c5abb09"}, ] -olefile = [ - {file = "olefile-0.46.zip", hash = "sha256:133b031eaf8fd2c9399b78b8bc5b8fcbe4c31e85295749bb17a87cba8f3c3964"}, + +[package.dependencies] +jupyter-client = ">=6.1.12" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +nbformat = ">=5.1" +traitlets = ">=5.4" + +[package.extras] +dev = ["pre-commit"] +docs = ["autodoc-traits", "mock", "moto", "myst-parser", "nbclient[test]", "sphinx (>=1.7)", "sphinx-book-theme", "sphinxcontrib-spelling"] +test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=7.0.0)", "pytest (>=7.0,<8)", "pytest-asyncio", "pytest-cov (>=4.0)", "testpath", "xmltodict"] + +[[package]] +name = "nbconvert" +version = "7.16.4" +description = "Converting Jupyter Notebooks (.ipynb files) to other formats. Output formats include asciidoc, html, latex, markdown, pdf, py, rst, script. nbconvert can be used both as a Python library (`import nbconvert`) or as a command line tool (invoked as `jupyter nbconvert ...`)." +optional = false +python-versions = ">=3.8" +files = [ + {file = "nbconvert-7.16.4-py3-none-any.whl", hash = "sha256:05873c620fe520b6322bf8a5ad562692343fe3452abda5765c7a34b7d1aa3eb3"}, + {file = "nbconvert-7.16.4.tar.gz", hash = "sha256:86ca91ba266b0a448dc96fa6c5b9d98affabde2867b363258703536807f9f7f4"}, ] -oletools = [ - {file = "oletools-0.60.1-py2.py3-none-any.whl", hash = "sha256:edef92374e688989a39269eb9a11142fb20a023629c23538c849c14d1d1144ea"}, - {file = "oletools-0.60.1.zip", hash = "sha256:67a796da4c4b8e2feb9a6b2495bef8798a3323a75512de4e5669d9dc9d1fae31"}, + +[package.dependencies] +beautifulsoup4 = "*" +bleach = "!=5.0.0" +defusedxml = "*" +importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} +jinja2 = ">=3.0" +jupyter-core = ">=4.7" +jupyterlab-pygments = "*" +markupsafe = ">=2.0" +mistune = ">=2.0.3,<4" +nbclient = ">=0.5.0" +nbformat = ">=5.7" +packaging = "*" +pandocfilters = ">=1.4.1" +pygments = ">=2.4.1" +tinycss2 = "*" +traitlets = ">=5.1" + +[package.extras] +all = ["flaky", "ipykernel", "ipython", "ipywidgets (>=7.5)", "myst-parser", "nbsphinx (>=0.2.12)", "playwright", "pydata-sphinx-theme", "pyqtwebengine (>=5.15)", "pytest (>=7)", "sphinx (==5.0.2)", "sphinxcontrib-spelling", "tornado (>=6.1)"] +docs = ["ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (==5.0.2)", "sphinxcontrib-spelling"] +qtpdf = ["pyqtwebengine (>=5.15)"] +qtpng = ["pyqtwebengine (>=5.15)"] +serve = ["tornado (>=6.1)"] +test = ["flaky", "ipykernel", "ipywidgets (>=7.5)", "pytest (>=7)"] +webpdf = ["playwright"] + +[[package]] +name = "nbformat" +version = "5.10.4" +description = "The Jupyter Notebook format" +optional = false +python-versions = ">=3.8" +files = [ + {file = "nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b"}, + {file = "nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a"}, ] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, + +[package.dependencies] +fastjsonschema = ">=2.15" +jsonschema = ">=2.6" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +traitlets = ">=5.1" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["pep440", "pre-commit", "pytest", "testpath"] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +description = "Patch asyncio to allow nested event loops" +optional = false +python-versions = ">=3.5" +files = [ + {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, + {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, ] -pandocfilters = [ - {file = "pandocfilters-1.5.0-py2.py3-none-any.whl", hash = "sha256:33aae3f25fd1a026079f5d27bdd52496f0e0803b3469282162bafdcbdf6ef14f"}, - {file = "pandocfilters-1.5.0.tar.gz", hash = "sha256:0b679503337d233b4339a817bfc8c50064e2eff681314376a47cb582305a7a38"}, + +[[package]] +name = "notebook-shim" +version = "0.2.4" +description = "A shim layer for notebook traits and config" +optional = false +python-versions = ">=3.7" +files = [ + {file = "notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef"}, + {file = "notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb"}, ] -parso = [ - {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, - {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, + +[package.dependencies] +jupyter-server = ">=1.8,<3" + +[package.extras] +test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync"] + +[[package]] +name = "olefile" +version = "0.47" +description = "Python package to parse, read and write Microsoft OLE2 files (Structured Storage or Compound Document, Microsoft Office)" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "olefile-0.47-py2.py3-none-any.whl", hash = "sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f"}, + {file = "olefile-0.47.zip", hash = "sha256:599383381a0bf3dfbd932ca0ca6515acd174ed48870cbf7fee123d698c192c1c"}, ] -pcodedmp = [ + +[package.extras] +tests = ["pytest", "pytest-cov"] + +[[package]] +name = "oletools" +version = "0.60.2" +description = "Python tools to analyze security characteristics of MS Office and OLE files (also called Structured Storage, Compound File Binary Format or Compound Document File Format), for Malware Analysis and Incident Response #DFIR" +optional = true +python-versions = "*" +files = [ + {file = "oletools-0.60.2-py2.py3-none-any.whl", hash = "sha256:72ad8bd748fd0c4e7b5b4733af770d11543ebb2bf2697455f99f975fcd50cc96"}, + {file = "oletools-0.60.2.zip", hash = "sha256:ad452099f4695ffd8855113f453348200d195ee9fa341a09e197d66ee7e0b2c3"}, +] + +[package.dependencies] +colorclass = "*" +easygui = "*" +msoffcrypto-tool = {version = "*", markers = "platform_python_implementation != \"PyPy\" or python_version >= \"3\" and (platform_system != \"Windows\" and platform_system != \"Darwin\")"} +olefile = ">=0.46" +pcodedmp = ">=1.2.5" +pyparsing = ">=2.1.0,<4" + +[package.extras] +full = ["XLMMacroDeobfuscator"] + +[[package]] +name = "overrides" +version = "7.7.0" +description = "A decorator to automatically detect mismatch when overriding a method." +optional = false +python-versions = ">=3.6" +files = [ + {file = "overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49"}, + {file = "overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a"}, +] + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pandocfilters" +version = "1.5.1" +description = "Utilities for writing pandoc filters in python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc"}, + {file = "pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e"}, +] + +[[package]] +name = "parso" +version = "0.8.4" +description = "A Python Parser" +optional = false +python-versions = ">=3.6" +files = [ + {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, + {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, +] + +[package.extras] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["docopt", "pytest"] + +[[package]] +name = "pcodedmp" +version = "1.2.6" +description = "A VBA p-code disassembler" +optional = true +python-versions = "*" +files = [ {file = "pcodedmp-1.2.6-py2.py3-none-any.whl", hash = "sha256:4441f7c0ab4cbda27bd4668db3b14f36261d86e5059ce06c0828602cbe1c4278"}, {file = "pcodedmp-1.2.6.tar.gz", hash = "sha256:025f8c809a126f45a082ffa820893e6a8d990d9d7ddb68694b5a9f0a6dbcd955"}, ] -pexpect = [ - {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, - {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, + +[package.dependencies] +oletools = ">=0.54" +win-unicode-console = {version = "*", markers = "platform_system == \"Windows\" and platform_python_implementation != \"PyPy\""} + +[[package]] +name = "pexpect" +version = "4.9.0" +description = "Pexpect allows easy control of interactive console applications." +optional = false +python-versions = "*" +files = [ + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, ] -pickleshare = [ + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pickleshare" +version = "0.7.5" +description = "Tiny 'shelve'-like database with concurrency support" +optional = false +python-versions = "*" +files = [ {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, ] -pillow = [ - {file = "Pillow-9.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:0b7257127d646ff8676ec8a15520013a698d1fdc48bc2a79ba4e53df792526f2"}, - {file = "Pillow-9.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b90f7616ea170e92820775ed47e136208e04c967271c9ef615b6fbd08d9af0e3"}, - {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68943d632f1f9e3dce98908e873b3a090f6cba1cbb1b892a9e8d97c938871fbe"}, - {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be55f8457cd1eac957af0c3f5ece7bc3f033f89b114ef30f710882717670b2a8"}, - {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d77adcd56a42d00cc1be30843d3426aa4e660cab4a61021dc84467123f7a00c"}, - {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:829f97c8e258593b9daa80638aee3789b7df9da5cf1336035016d76f03b8860c"}, - {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:801ec82e4188e935c7f5e22e006d01611d6b41661bba9fe45b60e7ac1a8f84de"}, - {file = "Pillow-9.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:871b72c3643e516db4ecf20efe735deb27fe30ca17800e661d769faab45a18d7"}, - {file = "Pillow-9.3.0-cp310-cp310-win32.whl", hash = "sha256:655a83b0058ba47c7c52e4e2df5ecf484c1b0b0349805896dd350cbc416bdd91"}, - {file = "Pillow-9.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:9f47eabcd2ded7698106b05c2c338672d16a6f2a485e74481f524e2a23c2794b"}, - {file = "Pillow-9.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:57751894f6618fd4308ed8e0c36c333e2f5469744c34729a27532b3db106ee20"}, - {file = "Pillow-9.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7db8b751ad307d7cf238f02101e8e36a128a6cb199326e867d1398067381bff4"}, - {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3033fbe1feb1b59394615a1cafaee85e49d01b51d54de0cbf6aa8e64182518a1"}, - {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22b012ea2d065fd163ca096f4e37e47cd8b59cf4b0fd47bfca6abb93df70b34c"}, - {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a65733d103311331875c1dca05cb4606997fd33d6acfed695b1232ba1df193"}, - {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:502526a2cbfa431d9fc2a079bdd9061a2397b842bb6bc4239bb176da00993812"}, - {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:90fb88843d3902fe7c9586d439d1e8c05258f41da473952aa8b328d8b907498c"}, - {file = "Pillow-9.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:89dca0ce00a2b49024df6325925555d406b14aa3efc2f752dbb5940c52c56b11"}, - {file = "Pillow-9.3.0-cp311-cp311-win32.whl", hash = "sha256:3168434d303babf495d4ba58fc22d6604f6e2afb97adc6a423e917dab828939c"}, - {file = "Pillow-9.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:18498994b29e1cf86d505edcb7edbe814d133d2232d256db8c7a8ceb34d18cef"}, - {file = "Pillow-9.3.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:772a91fc0e03eaf922c63badeca75e91baa80fe2f5f87bdaed4280662aad25c9"}, - {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa4107d1b306cdf8953edde0534562607fe8811b6c4d9a486298ad31de733b2"}, - {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4012d06c846dc2b80651b120e2cdd787b013deb39c09f407727ba90015c684f"}, - {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77ec3e7be99629898c9a6d24a09de089fa5356ee408cdffffe62d67bb75fdd72"}, - {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:6c738585d7a9961d8c2821a1eb3dcb978d14e238be3d70f0a706f7fa9316946b"}, - {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:828989c45c245518065a110434246c44a56a8b2b2f6347d1409c787e6e4651ee"}, - {file = "Pillow-9.3.0-cp37-cp37m-win32.whl", hash = "sha256:82409ffe29d70fd733ff3c1025a602abb3e67405d41b9403b00b01debc4c9a29"}, - {file = "Pillow-9.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:41e0051336807468be450d52b8edd12ac60bebaa97fe10c8b660f116e50b30e4"}, - {file = "Pillow-9.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:b03ae6f1a1878233ac620c98f3459f79fd77c7e3c2b20d460284e1fb370557d4"}, - {file = "Pillow-9.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4390e9ce199fc1951fcfa65795f239a8a4944117b5935a9317fb320e7767b40f"}, - {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40e1ce476a7804b0fb74bcfa80b0a2206ea6a882938eaba917f7a0f004b42502"}, - {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0a06a052c5f37b4ed81c613a455a81f9a3a69429b4fd7bb913c3fa98abefc20"}, - {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03150abd92771742d4a8cd6f2fa6246d847dcd2e332a18d0c15cc75bf6703040"}, - {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:15c42fb9dea42465dfd902fb0ecf584b8848ceb28b41ee2b58f866411be33f07"}, - {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:51e0e543a33ed92db9f5ef69a0356e0b1a7a6b6a71b80df99f1d181ae5875636"}, - {file = "Pillow-9.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3dd6caf940756101205dffc5367babf288a30043d35f80936f9bfb37f8355b32"}, - {file = "Pillow-9.3.0-cp38-cp38-win32.whl", hash = "sha256:f1ff2ee69f10f13a9596480335f406dd1f70c3650349e2be67ca3139280cade0"}, - {file = "Pillow-9.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:276a5ca930c913f714e372b2591a22c4bd3b81a418c0f6635ba832daec1cbcfc"}, - {file = "Pillow-9.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:73bd195e43f3fadecfc50c682f5055ec32ee2c933243cafbfdec69ab1aa87cad"}, - {file = "Pillow-9.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c7c8ae3864846fc95f4611c78129301e203aaa2af813b703c55d10cc1628535"}, - {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e0918e03aa0c72ea56edbb00d4d664294815aa11291a11504a377ea018330d3"}, - {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0915e734b33a474d76c28e07292f196cdf2a590a0d25bcc06e64e545f2d146c"}, - {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af0372acb5d3598f36ec0914deed2a63f6bcdb7b606da04dc19a88d31bf0c05b"}, - {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ad58d27a5b0262c0c19b47d54c5802db9b34d38bbf886665b626aff83c74bacd"}, - {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:97aabc5c50312afa5e0a2b07c17d4ac5e865b250986f8afe2b02d772567a380c"}, - {file = "Pillow-9.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9aaa107275d8527e9d6e7670b64aabaaa36e5b6bd71a1015ddd21da0d4e06448"}, - {file = "Pillow-9.3.0-cp39-cp39-win32.whl", hash = "sha256:bac18ab8d2d1e6b4ce25e3424f709aceef668347db8637c2296bcf41acb7cf48"}, - {file = "Pillow-9.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:b472b5ea442148d1c3e2209f20f1e0bb0eb556538690fa70b5e1f79fa0ba8dc2"}, - {file = "Pillow-9.3.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ab388aaa3f6ce52ac1cb8e122c4bd46657c15905904b3120a6248b5b8b0bc228"}, - {file = "Pillow-9.3.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbb8e7f2abee51cef77673be97760abff1674ed32847ce04b4af90f610144c7b"}, - {file = "Pillow-9.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca31dd6014cb8b0b2db1e46081b0ca7d936f856da3b39744aef499db5d84d02"}, - {file = "Pillow-9.3.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c7025dce65566eb6e89f56c9509d4f628fddcedb131d9465cacd3d8bac337e7e"}, - {file = "Pillow-9.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ebf2029c1f464c59b8bdbe5143c79fa2045a581ac53679733d3a91d400ff9efb"}, - {file = "Pillow-9.3.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b59430236b8e58840a0dfb4099a0e8717ffb779c952426a69ae435ca1f57210c"}, - {file = "Pillow-9.3.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12ce4932caf2ddf3e41d17fc9c02d67126935a44b86df6a206cf0d7161548627"}, - {file = "Pillow-9.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae5331c23ce118c53b172fa64a4c037eb83c9165aba3a7ba9ddd3ec9fa64a699"}, - {file = "Pillow-9.3.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0b07fffc13f474264c336298d1b4ce01d9c5a011415b79d4ee5527bb69ae6f65"}, - {file = "Pillow-9.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:073adb2ae23431d3b9bcbcff3fe698b62ed47211d0716b067385538a1b0f28b8"}, - {file = "Pillow-9.3.0.tar.gz", hash = "sha256:c935a22a557a560108d780f9a0fc426dd7459940dc54faa49d83249c8d3e760f"}, + +[[package]] +name = "pillow" +version = "10.4.0" +description = "Python Imaging Library (Fork)" +optional = true +python-versions = ">=3.8" +files = [ + {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, + {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"}, + {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"}, + {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"}, + {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"}, + {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"}, + {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"}, + {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"}, + {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"}, + {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"}, + {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"}, + {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"}, + {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"}, + {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"}, + {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"}, + {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"}, + {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"}, + {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"}, + {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"}, + {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"}, ] -pkgutil-resolve-name = [ + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + +[[package]] +name = "pkgutil-resolve-name" +version = "1.3.10" +description = "Resolve a name to an object." +optional = false +python-versions = ">=3.6" +files = [ {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"}, {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, ] -pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, + +[[package]] +name = "platformdirs" +version = "4.2.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, ] -prometheus-client = [ - {file = "prometheus_client-0.15.0-py3-none-any.whl", hash = "sha256:db7c05cbd13a0f79975592d112320f2605a325969b270a94b71dcabc47b931d2"}, - {file = "prometheus_client-0.15.0.tar.gz", hash = "sha256:be26aa452490cfcf6da953f9436e95a9f2b4d578ca80094b4458930e5f584ab1"}, + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] -prompt-toolkit = [ - {file = "prompt_toolkit-3.0.33-py3-none-any.whl", hash = "sha256:ced598b222f6f4029c0800cefaa6a17373fb580cd093223003475ce32805c35b"}, - {file = "prompt_toolkit-3.0.33.tar.gz", hash = "sha256:535c29c31216c77302877d5120aef6c94ff573748a5b5ca5b1b1f76f5e700c73"}, + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "prometheus-client" +version = "0.20.0" +description = "Python client for the Prometheus monitoring system." +optional = false +python-versions = ">=3.8" +files = [ + {file = "prometheus_client-0.20.0-py3-none-any.whl", hash = "sha256:cde524a85bce83ca359cc837f28b8c0db5cac7aa653a588fd7e84ba061c329e7"}, + {file = "prometheus_client-0.20.0.tar.gz", hash = "sha256:287629d00b147a32dcb2be0b9df905da599b2d82f80377083ec8463309a4bb89"}, ] -psutil = [ - {file = "psutil-5.9.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8"}, - {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe"}, - {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3ff89f9b835100a825b14c2808a106b6fdcc4b15483141482a12c725e7f78549"}, - {file = "psutil-5.9.4-cp27-cp27m-win32.whl", hash = "sha256:852dd5d9f8a47169fe62fd4a971aa07859476c2ba22c2254d4a1baa4e10b95ad"}, - {file = "psutil-5.9.4-cp27-cp27m-win_amd64.whl", hash = "sha256:9120cd39dca5c5e1c54b59a41d205023d436799b1c8c4d3ff71af18535728e94"}, - {file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6b92c532979bafc2df23ddc785ed116fced1f492ad90a6830cf24f4d1ea27d24"}, - {file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:efeae04f9516907be44904cc7ce08defb6b665128992a56957abc9b61dca94b7"}, - {file = "psutil-5.9.4-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:54d5b184728298f2ca8567bf83c422b706200bcbbfafdc06718264f9393cfeb7"}, - {file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16653106f3b59386ffe10e0bad3bb6299e169d5327d3f187614b1cb8f24cf2e1"}, - {file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54c0d3d8e0078b7666984e11b12b88af2db11d11249a8ac8920dd5ef68a66e08"}, - {file = "psutil-5.9.4-cp36-abi3-win32.whl", hash = "sha256:149555f59a69b33f056ba1c4eb22bb7bf24332ce631c44a319cec09f876aaeff"}, - {file = "psutil-5.9.4-cp36-abi3-win_amd64.whl", hash = "sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4"}, - {file = "psutil-5.9.4-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e"}, - {file = "psutil-5.9.4.tar.gz", hash = "sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62"}, + +[package.extras] +twisted = ["twisted"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.47" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, + {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, ] -ptyprocess = [ + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "psutil" +version = "6.0.0" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "psutil-6.0.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a021da3e881cd935e64a3d0a20983bda0bb4cf80e4f74fa9bfcb1bc5785360c6"}, + {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1287c2b95f1c0a364d23bc6f2ea2365a8d4d9b726a3be7294296ff7ba97c17f0"}, + {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a9a3dbfb4de4f18174528d87cc352d1f788b7496991cca33c6996f40c9e3c92c"}, + {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6ec7588fb3ddaec7344a825afe298db83fe01bfaaab39155fa84cf1c0d6b13c3"}, + {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e7c870afcb7d91fdea2b37c24aeb08f98b6d67257a5cb0a8bc3ac68d0f1a68c"}, + {file = "psutil-6.0.0-cp27-none-win32.whl", hash = "sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35"}, + {file = "psutil-6.0.0-cp27-none-win_amd64.whl", hash = "sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1"}, + {file = "psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132"}, + {file = "psutil-6.0.0-cp36-cp36m-win32.whl", hash = "sha256:fc8c9510cde0146432bbdb433322861ee8c3efbf8589865c8bf8d21cb30c4d14"}, + {file = "psutil-6.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:34859b8d8f423b86e4385ff3665d3f4d94be3cdf48221fbe476e883514fdb71c"}, + {file = "psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d"}, + {file = "psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3"}, + {file = "psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0"}, + {file = "psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +optional = false +python-versions = "*" +files = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, ] -publicsuffixlist = [ - {file = "publicsuffixlist-0.9.1-py2.py3-none-any.whl", hash = "sha256:2882fab94c2ff0c1eecb1206487ccdda8d09d04e1e3b4d64bd00fae0987c0939"}, - {file = "publicsuffixlist-0.9.1.tar.gz", hash = "sha256:0c3acbac87ce2e3b230e2123076c76278f827d4e6e78a536a01b7f9143466f36"}, + +[[package]] +name = "publicsuffixlist" +version = "1.0.1.20240702" +description = "publicsuffixlist implement" +optional = true +python-versions = ">=3.5" +files = [ + {file = "publicsuffixlist-1.0.1.20240702-py2.py3-none-any.whl", hash = "sha256:c31bd0cb7bc9f50d500c812b0aead6cb8fa53f7dfc66bdad5da730170d5b9c8e"}, + {file = "publicsuffixlist-1.0.1.20240702.tar.gz", hash = "sha256:79ab5c0f4a2a89556a717eaf0b7a5cfdf39e105cf718aed64ae7118be18e506c"}, ] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, + +[package.extras] +readme = ["pandoc"] +update = ["requests"] + +[[package]] +name = "pure-eval" +version = "0.2.2" +description = "Safely evaluate AST nodes without side effects" +optional = false +python-versions = "*" +files = [ + {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, + {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, ] -pycparser = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] -pydeep2 = [ + +[[package]] +name = "pydeep2" +version = "0.5.1" +description = "Python bindings for ssdeep" +optional = true +python-versions = "*" +files = [ {file = "pydeep2-0.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e14b310b820d895a7354be7fd025de874892df249cbfb3ad8a524459e1511fd8"}, {file = "pydeep2-0.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2283893e25826b547dd1e5c71a010e86ddfd7270e2f2b8c90973c1d7984c7eb7"}, + {file = "pydeep2-0.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f248e3161deb53d46a9368a7c164e36d83004faf2f11625d47a5cf23a6bdd2cb"}, + {file = "pydeep2-0.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a13fca9be89a9fa8d92a4f49d7b9191eef94555f8ddf030fb2be4c8c15ad618c"}, + {file = "pydeep2-0.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cb4757db97ac15ddf034c21cd6bab984f841586b6d53984e63c9a7803b2cd4"}, + {file = "pydeep2-0.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7809a1d6640bdbee68f075d53229d05229e11b4711f232728dd540f68e6483a4"}, {file = "pydeep2-0.5.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fedc1c9660cb5d0b73ad0b5f1dbffe16990e6721cbfc6454571a4b9882d0ea4"}, {file = "pydeep2-0.5.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca68f7d63e2ef510d410d20b223e8e97df41707fb50c4c526b6dd1d8698d9e6"}, {file = "pydeep2-0.5.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:199d05d8b4b7544509a2ba4802ead4b41dfe7859e0ecea9d9be9e41939f11660"}, @@ -2608,477 +2279,1309 @@ pydeep2 = [ {file = "pydeep2-0.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2063cbb053e5ce684cc45fff3e72c063b26aa85e41e6435cab0c658ad9e3e1e"}, {file = "pydeep2-0.5.1.tar.gz", hash = "sha256:44ce447e3253a69d3393f3cc53e3a87a48fe3ff9861793736a7bc218a1b95d77"}, ] -pyfaup = [ + +[[package]] +name = "pyfaup" +version = "1.2" +description = "Python bindings for the faup library" +optional = true +python-versions = "*" +files = [ {file = "pyfaup-1.2-py2.py3-none-any.whl", hash = "sha256:75f96f7da86ffb5402d3fcc2dbf98a511e792cf9100c159e34cdba8996ddc7f9"}, {file = "pyfaup-1.2.tar.gz", hash = "sha256:5648bc3ebd80239aec927aedfc218c3a6ff36de636cc53822bfeb70b0869b1e7"}, ] -pygments = [ - {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, - {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, + +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, ] -pyparsing = [ - {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, - {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyparsing" +version = "3.1.2" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = true +python-versions = ">=3.6.8" +files = [ + {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, + {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, ] -pyrsistent = [ - {file = "pyrsistent-0.19.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d6982b5a0237e1b7d876b60265564648a69b14017f3b5f908c5be2de3f9abb7a"}, - {file = "pyrsistent-0.19.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:187d5730b0507d9285a96fca9716310d572e5464cadd19f22b63a6976254d77a"}, - {file = "pyrsistent-0.19.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:055ab45d5911d7cae397dc418808d8802fb95262751872c841c170b0dbf51eed"}, - {file = "pyrsistent-0.19.2-cp310-cp310-win32.whl", hash = "sha256:456cb30ca8bff00596519f2c53e42c245c09e1a4543945703acd4312949bfd41"}, - {file = "pyrsistent-0.19.2-cp310-cp310-win_amd64.whl", hash = "sha256:b39725209e06759217d1ac5fcdb510e98670af9e37223985f330b611f62e7425"}, - {file = "pyrsistent-0.19.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2aede922a488861de0ad00c7630a6e2d57e8023e4be72d9d7147a9fcd2d30712"}, - {file = "pyrsistent-0.19.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879b4c2f4d41585c42df4d7654ddffff1239dc4065bc88b745f0341828b83e78"}, - {file = "pyrsistent-0.19.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c43bec251bbd10e3cb58ced80609c5c1eb238da9ca78b964aea410fb820d00d6"}, - {file = "pyrsistent-0.19.2-cp37-cp37m-win32.whl", hash = "sha256:d690b18ac4b3e3cab73b0b7aa7dbe65978a172ff94970ff98d82f2031f8971c2"}, - {file = "pyrsistent-0.19.2-cp37-cp37m-win_amd64.whl", hash = "sha256:3ba4134a3ff0fc7ad225b6b457d1309f4698108fb6b35532d015dca8f5abed73"}, - {file = "pyrsistent-0.19.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a178209e2df710e3f142cbd05313ba0c5ebed0a55d78d9945ac7a4e09d923308"}, - {file = "pyrsistent-0.19.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e371b844cec09d8dc424d940e54bba8f67a03ebea20ff7b7b0d56f526c71d584"}, - {file = "pyrsistent-0.19.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111156137b2e71f3a9936baf27cb322e8024dac3dc54ec7fb9f0bcf3249e68bb"}, - {file = "pyrsistent-0.19.2-cp38-cp38-win32.whl", hash = "sha256:e5d8f84d81e3729c3b506657dddfe46e8ba9c330bf1858ee33108f8bb2adb38a"}, - {file = "pyrsistent-0.19.2-cp38-cp38-win_amd64.whl", hash = "sha256:9cd3e9978d12b5d99cbdc727a3022da0430ad007dacf33d0bf554b96427f33ab"}, - {file = "pyrsistent-0.19.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f1258f4e6c42ad0b20f9cfcc3ada5bd6b83374516cd01c0960e3cb75fdca6770"}, - {file = "pyrsistent-0.19.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21455e2b16000440e896ab99e8304617151981ed40c29e9507ef1c2e4314ee95"}, - {file = "pyrsistent-0.19.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd880614c6237243ff53a0539f1cb26987a6dc8ac6e66e0c5a40617296a045e"}, - {file = "pyrsistent-0.19.2-cp39-cp39-win32.whl", hash = "sha256:71d332b0320642b3261e9fee47ab9e65872c2bd90260e5d225dabeed93cbd42b"}, - {file = "pyrsistent-0.19.2-cp39-cp39-win_amd64.whl", hash = "sha256:dec3eac7549869365fe263831f576c8457f6c833937c68542d08fde73457d291"}, - {file = "pyrsistent-0.19.2-py3-none-any.whl", hash = "sha256:ea6b79a02a28550c98b6ca9c35b9f492beaa54d7c5c9e9949555893c8a9234d0"}, - {file = "pyrsistent-0.19.2.tar.gz", hash = "sha256:bfa0351be89c9fcbcb8c9879b826f4353be10f58f8a677efab0c017bf7137ec2"}, + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "8.2.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, + {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, ] -pytest = [ - {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, - {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2.0" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, ] -pytest-cov = [ - {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, - {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] -python-dateutil = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-json-logger" +version = "2.0.7" +description = "A python library adding a json log formatter" +optional = false +python-versions = ">=3.6" +files = [ + {file = "python-json-logger-2.0.7.tar.gz", hash = "sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c"}, + {file = "python_json_logger-2.0.7-py3-none-any.whl", hash = "sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd"}, ] -python-magic = [ + +[[package]] +name = "python-magic" +version = "0.4.27" +description = "File type identification using libmagic" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ {file = "python-magic-0.4.27.tar.gz", hash = "sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b"}, {file = "python_magic-0.4.27-py2.py3-none-any.whl", hash = "sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3"}, ] -pytz = [ - {file = "pytz-2022.6-py2.py3-none-any.whl", hash = "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427"}, - {file = "pytz-2022.6.tar.gz", hash = "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2"}, + +[[package]] +name = "pytz" +version = "2024.1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, ] -pytz-deprecation-shim = [ - {file = "pytz_deprecation_shim-0.1.0.post0-py2.py3-none-any.whl", hash = "sha256:8314c9692a636c8eb3bda879b9f119e350e93223ae83e70e80c31675a0fdc1a6"}, - {file = "pytz_deprecation_shim-0.1.0.post0.tar.gz", hash = "sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d"}, + +[[package]] +name = "pywin32" +version = "306" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, + {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, ] -pywin32 = [ - {file = "pywin32-305-cp310-cp310-win32.whl", hash = "sha256:421f6cd86e84bbb696d54563c48014b12a23ef95a14e0bdba526be756d89f116"}, - {file = "pywin32-305-cp310-cp310-win_amd64.whl", hash = "sha256:73e819c6bed89f44ff1d690498c0a811948f73777e5f97c494c152b850fad478"}, - {file = "pywin32-305-cp310-cp310-win_arm64.whl", hash = "sha256:742eb905ce2187133a29365b428e6c3b9001d79accdc30aa8969afba1d8470f4"}, - {file = "pywin32-305-cp311-cp311-win32.whl", hash = "sha256:19ca459cd2e66c0e2cc9a09d589f71d827f26d47fe4a9d09175f6aa0256b51c2"}, - {file = "pywin32-305-cp311-cp311-win_amd64.whl", hash = "sha256:326f42ab4cfff56e77e3e595aeaf6c216712bbdd91e464d167c6434b28d65990"}, - {file = "pywin32-305-cp311-cp311-win_arm64.whl", hash = "sha256:4ecd404b2c6eceaca52f8b2e3e91b2187850a1ad3f8b746d0796a98b4cea04db"}, - {file = "pywin32-305-cp36-cp36m-win32.whl", hash = "sha256:48d8b1659284f3c17b68587af047d110d8c44837736b8932c034091683e05863"}, - {file = "pywin32-305-cp36-cp36m-win_amd64.whl", hash = "sha256:13362cc5aa93c2beaf489c9c9017c793722aeb56d3e5166dadd5ef82da021fe1"}, - {file = "pywin32-305-cp37-cp37m-win32.whl", hash = "sha256:a55db448124d1c1484df22fa8bbcbc45c64da5e6eae74ab095b9ea62e6d00496"}, - {file = "pywin32-305-cp37-cp37m-win_amd64.whl", hash = "sha256:109f98980bfb27e78f4df8a51a8198e10b0f347257d1e265bb1a32993d0c973d"}, - {file = "pywin32-305-cp38-cp38-win32.whl", hash = "sha256:9dd98384da775afa009bc04863426cb30596fd78c6f8e4e2e5bbf4edf8029504"}, - {file = "pywin32-305-cp38-cp38-win_amd64.whl", hash = "sha256:56d7a9c6e1a6835f521788f53b5af7912090674bb84ef5611663ee1595860fc7"}, - {file = "pywin32-305-cp39-cp39-win32.whl", hash = "sha256:9d968c677ac4d5cbdaa62fd3014ab241718e619d8e36ef8e11fb930515a1e918"}, - {file = "pywin32-305-cp39-cp39-win_amd64.whl", hash = "sha256:50768c6b7c3f0b38b7fb14dd4104da93ebced5f1a50dc0e834594bff6fbe1271"}, + +[[package]] +name = "pywinpty" +version = "2.0.13" +description = "Pseudo terminal support for Windows from Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pywinpty-2.0.13-cp310-none-win_amd64.whl", hash = "sha256:697bff211fb5a6508fee2dc6ff174ce03f34a9a233df9d8b5fe9c8ce4d5eaf56"}, + {file = "pywinpty-2.0.13-cp311-none-win_amd64.whl", hash = "sha256:b96fb14698db1284db84ca38c79f15b4cfdc3172065b5137383910567591fa99"}, + {file = "pywinpty-2.0.13-cp312-none-win_amd64.whl", hash = "sha256:2fd876b82ca750bb1333236ce98488c1be96b08f4f7647cfdf4129dfad83c2d4"}, + {file = "pywinpty-2.0.13-cp38-none-win_amd64.whl", hash = "sha256:61d420c2116c0212808d31625611b51caf621fe67f8a6377e2e8b617ea1c1f7d"}, + {file = "pywinpty-2.0.13-cp39-none-win_amd64.whl", hash = "sha256:71cb613a9ee24174730ac7ae439fd179ca34ccb8c5349e8d7b72ab5dea2c6f4b"}, + {file = "pywinpty-2.0.13.tar.gz", hash = "sha256:c34e32351a3313ddd0d7da23d27f835c860d32fe4ac814d372a3ea9594f41dde"}, ] -pywinpty = [ - {file = "pywinpty-2.0.9-cp310-none-win_amd64.whl", hash = "sha256:30a7b371446a694a6ce5ef906d70ac04e569de5308c42a2bdc9c3bc9275ec51f"}, - {file = "pywinpty-2.0.9-cp311-none-win_amd64.whl", hash = "sha256:d78ef6f4bd7a6c6f94dc1a39ba8fb028540cc39f5cb593e756506db17843125f"}, - {file = "pywinpty-2.0.9-cp37-none-win_amd64.whl", hash = "sha256:5ed36aa087e35a3a183f833631b3e4c1ae92fe2faabfce0fa91b77ed3f0f1382"}, - {file = "pywinpty-2.0.9-cp38-none-win_amd64.whl", hash = "sha256:2352f44ee913faaec0a02d3c112595e56b8af7feeb8100efc6dc1a8685044199"}, - {file = "pywinpty-2.0.9-cp39-none-win_amd64.whl", hash = "sha256:ba75ec55f46c9e17db961d26485b033deb20758b1731e8e208e1e8a387fcf70c"}, - {file = "pywinpty-2.0.9.tar.gz", hash = "sha256:01b6400dd79212f50a2f01af1c65b781290ff39610853db99bf03962eb9a615f"}, + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] -pyzmq = [ - {file = "pyzmq-24.0.1-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:28b119ba97129d3001673a697b7cce47fe6de1f7255d104c2f01108a5179a066"}, - {file = "pyzmq-24.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bcbebd369493d68162cddb74a9c1fcebd139dfbb7ddb23d8f8e43e6c87bac3a6"}, - {file = "pyzmq-24.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae61446166983c663cee42c852ed63899e43e484abf080089f771df4b9d272ef"}, - {file = "pyzmq-24.0.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87f7ac99b15270db8d53f28c3c7b968612993a90a5cf359da354efe96f5372b4"}, - {file = "pyzmq-24.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dca7c3956b03b7663fac4d150f5e6d4f6f38b2462c1e9afd83bcf7019f17913"}, - {file = "pyzmq-24.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8c78bfe20d4c890cb5580a3b9290f700c570e167d4cdcc55feec07030297a5e3"}, - {file = "pyzmq-24.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:48f721f070726cd2a6e44f3c33f8ee4b24188e4b816e6dd8ba542c8c3bb5b246"}, - {file = "pyzmq-24.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:afe1f3bc486d0ce40abb0a0c9adb39aed3bbac36ebdc596487b0cceba55c21c1"}, - {file = "pyzmq-24.0.1-cp310-cp310-win32.whl", hash = "sha256:3e6192dbcefaaa52ed81be88525a54a445f4b4fe2fffcae7fe40ebb58bd06bfd"}, - {file = "pyzmq-24.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:86de64468cad9c6d269f32a6390e210ca5ada568c7a55de8e681ca3b897bb340"}, - {file = "pyzmq-24.0.1-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:838812c65ed5f7c2bd11f7b098d2e5d01685a3f6d1f82849423b570bae698c00"}, - {file = "pyzmq-24.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dfb992dbcd88d8254471760879d48fb20836d91baa90f181c957122f9592b3dc"}, - {file = "pyzmq-24.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7abddb2bd5489d30ffeb4b93a428130886c171b4d355ccd226e83254fcb6b9ef"}, - {file = "pyzmq-24.0.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94010bd61bc168c103a5b3b0f56ed3b616688192db7cd5b1d626e49f28ff51b3"}, - {file = "pyzmq-24.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8242543c522d84d033fe79be04cb559b80d7eb98ad81b137ff7e0a9020f00ace"}, - {file = "pyzmq-24.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ccb94342d13e3bf3ffa6e62f95b5e3f0bc6bfa94558cb37f4b3d09d6feb536ff"}, - {file = "pyzmq-24.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6640f83df0ae4ae1104d4c62b77e9ef39be85ebe53f636388707d532bee2b7b8"}, - {file = "pyzmq-24.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a180dbd5ea5d47c2d3b716d5c19cc3fb162d1c8db93b21a1295d69585bfddac1"}, - {file = "pyzmq-24.0.1-cp311-cp311-win32.whl", hash = "sha256:624321120f7e60336be8ec74a172ae7fba5c3ed5bf787cc85f7e9986c9e0ebc2"}, - {file = "pyzmq-24.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:1724117bae69e091309ffb8255412c4651d3f6355560d9af312d547f6c5bc8b8"}, - {file = "pyzmq-24.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:15975747462ec49fdc863af906bab87c43b2491403ab37a6d88410635786b0f4"}, - {file = "pyzmq-24.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b947e264f0e77d30dcbccbb00f49f900b204b922eb0c3a9f0afd61aaa1cedc3d"}, - {file = "pyzmq-24.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ec91f1bad66f3ee8c6deb65fa1fe418e8ad803efedd69c35f3b5502f43bd1dc"}, - {file = "pyzmq-24.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:db03704b3506455d86ec72c3358a779e9b1d07b61220dfb43702b7b668edcd0d"}, - {file = "pyzmq-24.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e7e66b4e403c2836ac74f26c4b65d8ac0ca1eef41dfcac2d013b7482befaad83"}, - {file = "pyzmq-24.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:7a23ccc1083c260fa9685c93e3b170baba45aeed4b524deb3f426b0c40c11639"}, - {file = "pyzmq-24.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:fa0ae3275ef706c0309556061185dd0e4c4cd3b7d6f67ae617e4e677c7a41e2e"}, - {file = "pyzmq-24.0.1-cp36-cp36m-win32.whl", hash = "sha256:f01de4ec083daebf210531e2cca3bdb1608dbbbe00a9723e261d92087a1f6ebc"}, - {file = "pyzmq-24.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:de4217b9eb8b541cf2b7fde4401ce9d9a411cc0af85d410f9d6f4333f43640be"}, - {file = "pyzmq-24.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:78068e8678ca023594e4a0ab558905c1033b2d3e806a0ad9e3094e231e115a33"}, - {file = "pyzmq-24.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77c2713faf25a953c69cf0f723d1b7dd83827b0834e6c41e3fb3bbc6765914a1"}, - {file = "pyzmq-24.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bb4af15f305056e95ca1bd086239b9ebc6ad55e9f49076d27d80027f72752f6"}, - {file = "pyzmq-24.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0f14cffd32e9c4c73da66db97853a6aeceaac34acdc0fae9e5bbc9370281864c"}, - {file = "pyzmq-24.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0108358dab8c6b27ff6b985c2af4b12665c1bc659648284153ee501000f5c107"}, - {file = "pyzmq-24.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d66689e840e75221b0b290b0befa86f059fb35e1ee6443bce51516d4d61b6b99"}, - {file = "pyzmq-24.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ae08ac90aa8fa14caafc7a6251bd218bf6dac518b7bff09caaa5e781119ba3f2"}, - {file = "pyzmq-24.0.1-cp37-cp37m-win32.whl", hash = "sha256:8421aa8c9b45ea608c205db9e1c0c855c7e54d0e9c2c2f337ce024f6843cab3b"}, - {file = "pyzmq-24.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54d8b9c5e288362ec8595c1d98666d36f2070fd0c2f76e2b3c60fbad9bd76227"}, - {file = "pyzmq-24.0.1-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:acbd0a6d61cc954b9f535daaa9ec26b0a60a0d4353c5f7c1438ebc88a359a47e"}, - {file = "pyzmq-24.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:47b11a729d61a47df56346283a4a800fa379ae6a85870d5a2e1e4956c828eedc"}, - {file = "pyzmq-24.0.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:abe6eb10122f0d746a0d510c2039ae8edb27bc9af29f6d1b05a66cc2401353ff"}, - {file = "pyzmq-24.0.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:07bec1a1b22dacf718f2c0e71b49600bb6a31a88f06527dfd0b5aababe3fa3f7"}, - {file = "pyzmq-24.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d945a85b70da97ae86113faf9f1b9294efe66bd4a5d6f82f2676d567338b66"}, - {file = "pyzmq-24.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1b7928bb7580736ffac5baf814097be342ba08d3cfdfb48e52773ec959572287"}, - {file = "pyzmq-24.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b946da90dc2799bcafa682692c1d2139b2a96ec3c24fa9fc6f5b0da782675330"}, - {file = "pyzmq-24.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c8840f064b1fb377cffd3efeaad2b190c14d4c8da02316dae07571252d20b31f"}, - {file = "pyzmq-24.0.1-cp38-cp38-win32.whl", hash = "sha256:4854f9edc5208f63f0841c0c667260ae8d6846cfa233c479e29fdc85d42ebd58"}, - {file = "pyzmq-24.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:42d4f97b9795a7aafa152a36fe2ad44549b83a743fd3e77011136def512e6c2a"}, - {file = "pyzmq-24.0.1-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:52afb0ac962963fff30cf1be775bc51ae083ef4c1e354266ab20e5382057dd62"}, - {file = "pyzmq-24.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bad8210ad4df68c44ff3685cca3cda448ee46e20d13edcff8909eba6ec01ca4"}, - {file = "pyzmq-24.0.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dabf1a05318d95b1537fd61d9330ef4313ea1216eea128a17615038859da3b3b"}, - {file = "pyzmq-24.0.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5bd3d7dfd9cd058eb68d9a905dec854f86649f64d4ddf21f3ec289341386c44b"}, - {file = "pyzmq-24.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8012bce6836d3f20a6c9599f81dfa945f433dab4dbd0c4917a6fb1f998ab33d"}, - {file = "pyzmq-24.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c31805d2c8ade9b11feca4674eee2b9cce1fec3e8ddb7bbdd961a09dc76a80ea"}, - {file = "pyzmq-24.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:3104f4b084ad5d9c0cb87445cc8cfd96bba710bef4a66c2674910127044df209"}, - {file = "pyzmq-24.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:df0841f94928f8af9c7a1f0aaaffba1fb74607af023a152f59379c01c53aee58"}, - {file = "pyzmq-24.0.1-cp39-cp39-win32.whl", hash = "sha256:a435ef8a3bd95c8a2d316d6e0ff70d0db524f6037411652803e118871d703333"}, - {file = "pyzmq-24.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:2032d9cb994ce3b4cba2b8dfae08c7e25bc14ba484c770d4d3be33c27de8c45b"}, - {file = "pyzmq-24.0.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bb5635c851eef3a7a54becde6da99485eecf7d068bd885ac8e6d173c4ecd68b0"}, - {file = "pyzmq-24.0.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:83ea1a398f192957cb986d9206ce229efe0ee75e3c6635baff53ddf39bd718d5"}, - {file = "pyzmq-24.0.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:941fab0073f0a54dc33d1a0460cb04e0d85893cb0c5e1476c785000f8b359409"}, - {file = "pyzmq-24.0.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e8f482c44ccb5884bf3f638f29bea0f8dc68c97e38b2061769c4cb697f6140d"}, - {file = "pyzmq-24.0.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:613010b5d17906c4367609e6f52e9a2595e35d5cc27d36ff3f1b6fa6e954d944"}, - {file = "pyzmq-24.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:65c94410b5a8355cfcf12fd600a313efee46ce96a09e911ea92cf2acf6708804"}, - {file = "pyzmq-24.0.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:20e7eeb1166087db636c06cae04a1ef59298627f56fb17da10528ab52a14c87f"}, - {file = "pyzmq-24.0.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2712aee7b3834ace51738c15d9ee152cc5a98dc7d57dd93300461b792ab7b43"}, - {file = "pyzmq-24.0.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a7c280185c4da99e0cc06c63bdf91f5b0b71deb70d8717f0ab870a43e376db8"}, - {file = "pyzmq-24.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:858375573c9225cc8e5b49bfac846a77b696b8d5e815711b8d4ba3141e6e8879"}, - {file = "pyzmq-24.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:80093b595921eed1a2cead546a683b9e2ae7f4a4592bb2ab22f70d30174f003a"}, - {file = "pyzmq-24.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f3f3154fde2b1ff3aa7b4f9326347ebc89c8ef425ca1db8f665175e6d3bd42f"}, - {file = "pyzmq-24.0.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abb756147314430bee5d10919b8493c0ccb109ddb7f5dfd2fcd7441266a25b75"}, - {file = "pyzmq-24.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e706bac34e9f50779cb8c39f10b53a4d15aebb97235643d3112ac20bd577b4"}, - {file = "pyzmq-24.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:687700f8371643916a1d2c61f3fdaa630407dd205c38afff936545d7b7466066"}, - {file = "pyzmq-24.0.1.tar.gz", hash = "sha256:216f5d7dbb67166759e59b0479bca82b8acf9bed6015b526b8eb10143fb08e77"}, + +[[package]] +name = "pyzmq" +version = "26.0.3" +description = "Python bindings for 0MQ" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyzmq-26.0.3-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:44dd6fc3034f1eaa72ece33588867df9e006a7303725a12d64c3dff92330f625"}, + {file = "pyzmq-26.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:acb704195a71ac5ea5ecf2811c9ee19ecdc62b91878528302dd0be1b9451cc90"}, + {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dbb9c997932473a27afa93954bb77a9f9b786b4ccf718d903f35da3232317de"}, + {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bcb34f869d431799c3ee7d516554797f7760cb2198ecaa89c3f176f72d062be"}, + {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ece17ec5f20d7d9b442e5174ae9f020365d01ba7c112205a4d59cf19dc38ee"}, + {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ba6e5e6588e49139a0979d03a7deb9c734bde647b9a8808f26acf9c547cab1bf"}, + {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3bf8b000a4e2967e6dfdd8656cd0757d18c7e5ce3d16339e550bd462f4857e59"}, + {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2136f64fbb86451dbbf70223635a468272dd20075f988a102bf8a3f194a411dc"}, + {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e8918973fbd34e7814f59143c5f600ecd38b8038161239fd1a3d33d5817a38b8"}, + {file = "pyzmq-26.0.3-cp310-cp310-win32.whl", hash = "sha256:0aaf982e68a7ac284377d051c742610220fd06d330dcd4c4dbb4cdd77c22a537"}, + {file = "pyzmq-26.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:f1a9b7d00fdf60b4039f4455afd031fe85ee8305b019334b72dcf73c567edc47"}, + {file = "pyzmq-26.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:80b12f25d805a919d53efc0a5ad7c0c0326f13b4eae981a5d7b7cc343318ebb7"}, + {file = "pyzmq-26.0.3-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:a72a84570f84c374b4c287183debc776dc319d3e8ce6b6a0041ce2e400de3f32"}, + {file = "pyzmq-26.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ca684ee649b55fd8f378127ac8462fb6c85f251c2fb027eb3c887e8ee347bcd"}, + {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e222562dc0f38571c8b1ffdae9d7adb866363134299264a1958d077800b193b7"}, + {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f17cde1db0754c35a91ac00b22b25c11da6eec5746431d6e5092f0cd31a3fea9"}, + {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7c0c0b3244bb2275abe255d4a30c050d541c6cb18b870975553f1fb6f37527"}, + {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ac97a21de3712afe6a6c071abfad40a6224fd14fa6ff0ff8d0c6e6cd4e2f807a"}, + {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:88b88282e55fa39dd556d7fc04160bcf39dea015f78e0cecec8ff4f06c1fc2b5"}, + {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:72b67f966b57dbd18dcc7efbc1c7fc9f5f983e572db1877081f075004614fcdd"}, + {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4b6cecbbf3b7380f3b61de3a7b93cb721125dc125c854c14ddc91225ba52f83"}, + {file = "pyzmq-26.0.3-cp311-cp311-win32.whl", hash = "sha256:eed56b6a39216d31ff8cd2f1d048b5bf1700e4b32a01b14379c3b6dde9ce3aa3"}, + {file = "pyzmq-26.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:3191d312c73e3cfd0f0afdf51df8405aafeb0bad71e7ed8f68b24b63c4f36500"}, + {file = "pyzmq-26.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:b6907da3017ef55139cf0e417c5123a84c7332520e73a6902ff1f79046cd3b94"}, + {file = "pyzmq-26.0.3-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:068ca17214038ae986d68f4a7021f97e187ed278ab6dccb79f837d765a54d753"}, + {file = "pyzmq-26.0.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7821d44fe07335bea256b9f1f41474a642ca55fa671dfd9f00af8d68a920c2d4"}, + {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eeb438a26d87c123bb318e5f2b3d86a36060b01f22fbdffd8cf247d52f7c9a2b"}, + {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69ea9d6d9baa25a4dc9cef5e2b77b8537827b122214f210dd925132e34ae9b12"}, + {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7daa3e1369355766dea11f1d8ef829905c3b9da886ea3152788dc25ee6079e02"}, + {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6ca7a9a06b52d0e38ccf6bca1aeff7be178917893f3883f37b75589d42c4ac20"}, + {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1b7d0e124948daa4d9686d421ef5087c0516bc6179fdcf8828b8444f8e461a77"}, + {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e746524418b70f38550f2190eeee834db8850088c834d4c8406fbb9bc1ae10b2"}, + {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6b3146f9ae6af82c47a5282ac8803523d381b3b21caeae0327ed2f7ecb718798"}, + {file = "pyzmq-26.0.3-cp312-cp312-win32.whl", hash = "sha256:2b291d1230845871c00c8462c50565a9cd6026fe1228e77ca934470bb7d70ea0"}, + {file = "pyzmq-26.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:926838a535c2c1ea21c903f909a9a54e675c2126728c21381a94ddf37c3cbddf"}, + {file = "pyzmq-26.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:5bf6c237f8c681dfb91b17f8435b2735951f0d1fad10cc5dfd96db110243370b"}, + {file = "pyzmq-26.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c0991f5a96a8e620f7691e61178cd8f457b49e17b7d9cfa2067e2a0a89fc1d5"}, + {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dbf012d8fcb9f2cf0643b65df3b355fdd74fc0035d70bb5c845e9e30a3a4654b"}, + {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:01fbfbeb8249a68d257f601deb50c70c929dc2dfe683b754659569e502fbd3aa"}, + {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c8eb19abe87029c18f226d42b8a2c9efdd139d08f8bf6e085dd9075446db450"}, + {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5344b896e79800af86ad643408ca9aa303a017f6ebff8cee5a3163c1e9aec987"}, + {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:204e0f176fd1d067671157d049466869b3ae1fc51e354708b0dc41cf94e23a3a"}, + {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a42db008d58530efa3b881eeee4991146de0b790e095f7ae43ba5cc612decbc5"}, + {file = "pyzmq-26.0.3-cp37-cp37m-win32.whl", hash = "sha256:8d7a498671ca87e32b54cb47c82a92b40130a26c5197d392720a1bce1b3c77cf"}, + {file = "pyzmq-26.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:3b4032a96410bdc760061b14ed6a33613ffb7f702181ba999df5d16fb96ba16a"}, + {file = "pyzmq-26.0.3-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:2cc4e280098c1b192c42a849de8de2c8e0f3a84086a76ec5b07bfee29bda7d18"}, + {file = "pyzmq-26.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5bde86a2ed3ce587fa2b207424ce15b9a83a9fa14422dcc1c5356a13aed3df9d"}, + {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:34106f68e20e6ff253c9f596ea50397dbd8699828d55e8fa18bd4323d8d966e6"}, + {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ebbbd0e728af5db9b04e56389e2299a57ea8b9dd15c9759153ee2455b32be6ad"}, + {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6b1d1c631e5940cac5a0b22c5379c86e8df6a4ec277c7a856b714021ab6cfad"}, + {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e891ce81edd463b3b4c3b885c5603c00141151dd9c6936d98a680c8c72fe5c67"}, + {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9b273ecfbc590a1b98f014ae41e5cf723932f3b53ba9367cfb676f838038b32c"}, + {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b32bff85fb02a75ea0b68f21e2412255b5731f3f389ed9aecc13a6752f58ac97"}, + {file = "pyzmq-26.0.3-cp38-cp38-win32.whl", hash = "sha256:f6c21c00478a7bea93caaaef9e7629145d4153b15a8653e8bb4609d4bc70dbfc"}, + {file = "pyzmq-26.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:3401613148d93ef0fd9aabdbddb212de3db7a4475367f49f590c837355343972"}, + {file = "pyzmq-26.0.3-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:2ed8357f4c6e0daa4f3baf31832df8a33334e0fe5b020a61bc8b345a3db7a606"}, + {file = "pyzmq-26.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c1c8f2a2ca45292084c75bb6d3a25545cff0ed931ed228d3a1810ae3758f975f"}, + {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b63731993cdddcc8e087c64e9cf003f909262b359110070183d7f3025d1c56b5"}, + {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b3cd31f859b662ac5d7f4226ec7d8bd60384fa037fc02aee6ff0b53ba29a3ba8"}, + {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:115f8359402fa527cf47708d6f8a0f8234f0e9ca0cab7c18c9c189c194dbf620"}, + {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:715bdf952b9533ba13dfcf1f431a8f49e63cecc31d91d007bc1deb914f47d0e4"}, + {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e1258c639e00bf5e8a522fec6c3eaa3e30cf1c23a2f21a586be7e04d50c9acab"}, + {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:15c59e780be8f30a60816a9adab900c12a58d79c1ac742b4a8df044ab2a6d920"}, + {file = "pyzmq-26.0.3-cp39-cp39-win32.whl", hash = "sha256:d0cdde3c78d8ab5b46595054e5def32a755fc028685add5ddc7403e9f6de9879"}, + {file = "pyzmq-26.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:ce828058d482ef860746bf532822842e0ff484e27f540ef5c813d516dd8896d2"}, + {file = "pyzmq-26.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:788f15721c64109cf720791714dc14afd0f449d63f3a5487724f024345067381"}, + {file = "pyzmq-26.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c18645ef6294d99b256806e34653e86236eb266278c8ec8112622b61db255de"}, + {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e6bc96ebe49604df3ec2c6389cc3876cabe475e6bfc84ced1bf4e630662cb35"}, + {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:971e8990c5cc4ddcff26e149398fc7b0f6a042306e82500f5e8db3b10ce69f84"}, + {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8416c23161abd94cc7da80c734ad7c9f5dbebdadfdaa77dad78244457448223"}, + {file = "pyzmq-26.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:082a2988364b60bb5de809373098361cf1dbb239623e39e46cb18bc035ed9c0c"}, + {file = "pyzmq-26.0.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d57dfbf9737763b3a60d26e6800e02e04284926329aee8fb01049635e957fe81"}, + {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:77a85dca4c2430ac04dc2a2185c2deb3858a34fe7f403d0a946fa56970cf60a1"}, + {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4c82a6d952a1d555bf4be42b6532927d2a5686dd3c3e280e5f63225ab47ac1f5"}, + {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4496b1282c70c442809fc1b151977c3d967bfb33e4e17cedbf226d97de18f709"}, + {file = "pyzmq-26.0.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:e4946d6bdb7ba972dfda282f9127e5756d4f299028b1566d1245fa0d438847e6"}, + {file = "pyzmq-26.0.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:03c0ae165e700364b266876d712acb1ac02693acd920afa67da2ebb91a0b3c09"}, + {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3e3070e680f79887d60feeda051a58d0ac36622e1759f305a41059eff62c6da7"}, + {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6ca08b840fe95d1c2bd9ab92dac5685f949fc6f9ae820ec16193e5ddf603c3b2"}, + {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e76654e9dbfb835b3518f9938e565c7806976c07b37c33526b574cc1a1050480"}, + {file = "pyzmq-26.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:871587bdadd1075b112e697173e946a07d722459d20716ceb3d1bd6c64bd08ce"}, + {file = "pyzmq-26.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d0a2d1bd63a4ad79483049b26514e70fa618ce6115220da9efdff63688808b17"}, + {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0270b49b6847f0d106d64b5086e9ad5dc8a902413b5dbbb15d12b60f9c1747a4"}, + {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:703c60b9910488d3d0954ca585c34f541e506a091a41930e663a098d3b794c67"}, + {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74423631b6be371edfbf7eabb02ab995c2563fee60a80a30829176842e71722a"}, + {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4adfbb5451196842a88fda3612e2c0414134874bffb1c2ce83ab4242ec9e027d"}, + {file = "pyzmq-26.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3516119f4f9b8671083a70b6afaa0a070f5683e431ab3dc26e9215620d7ca1ad"}, + {file = "pyzmq-26.0.3.tar.gz", hash = "sha256:dba7d9f2e047dfa2bca3b01f4f84aa5246725203d6284e3790f2ca15fba6b40a"}, ] -recommonmark = [ + +[package.dependencies] +cffi = {version = "*", markers = "implementation_name == \"pypy\""} + +[[package]] +name = "recommonmark" +version = "0.7.1" +description = "A docutils-compatibility bridge to CommonMark, enabling you to write CommonMark inside of Docutils & Sphinx projects." +optional = true +python-versions = "*" +files = [ {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"}, {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"}, ] -reportlab = [ - {file = "reportlab-3.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6dfcf7bd6db5d80711cbbd0996b6e7a79cc414ca81457960367df11d2860f92a"}, - {file = "reportlab-3.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a0bc7a1d64fe754b62e175ba0cf47a630b529c0488ec9ac4e4c7655e295ea4d"}, - {file = "reportlab-3.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:adf78ccb2defad5b6ecb2e2e9f2a672719b0a8e2278592a7d77f6c220a042388"}, - {file = "reportlab-3.6.12-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c84afd5bef6e407c80ba9f99b6abbe3ea78e8243b0f19897a871a7bcad1f749d"}, - {file = "reportlab-3.6.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4fa3cdf490f3828b055381e8c7dc7819b3e5f7a442d7af7a8f90e9806a7fff51"}, - {file = "reportlab-3.6.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07fdd968df7941c2bfb67b9bb4532f424992dfafc71b72a4e4b291ff707e6b0e"}, - {file = "reportlab-3.6.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce85a204f46c871c8af6fa64b9bbed165456935c1d0bfb2f570a3194f6723ddb"}, - {file = "reportlab-3.6.12-cp310-cp310-win32.whl", hash = "sha256:090ea99ff829d918f7b6140594373b1340a34e1e6876eddae5aa06662ec10d64"}, - {file = "reportlab-3.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:4c599645af9b5b2241a23e977a82c965a59c24cd94b2600b8d34373c66cad763"}, - {file = "reportlab-3.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:236a6483210049205f6180d7a7595d0ca2e4ce343d83cc94ca719a4145809c6f"}, - {file = "reportlab-3.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:69f41295d696c822224334f0994f1f107df7efed72211d45a1118696f1427c84"}, - {file = "reportlab-3.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f51dcb39e910a853749250c0f82aced80bca3f7315e9c4ee14349eb7cab6a3f8"}, - {file = "reportlab-3.6.12-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8dddc52e0e486291be0ad39184da0607fae9cc665fdba1881211de9cfc0b332"}, - {file = "reportlab-3.6.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4863c49602722237e35cbce5aa91af4539cc63a671f59504d2b3f3767d898cf"}, - {file = "reportlab-3.6.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b1215facead57cc5325aef4229ef886e85d270b2ba02080fb5809ce9d2b81b4"}, - {file = "reportlab-3.6.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12049314497d872f6788f811e2b331654db207937f8a2fb34ff3e3cd9897faa"}, - {file = "reportlab-3.6.12-cp311-cp311-win32.whl", hash = "sha256:759495c2b8c15cb0d6b539c246896029e4cde42a896c3956f77e311c5f6b0807"}, - {file = "reportlab-3.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:666bdba4958b348460a765c48b8c0640e7085540846ed9494f47d8651604b33c"}, - {file = "reportlab-3.6.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a7c3369fa618eca79f9554ce06c618a5e738e592d61d96aa09b2457ca3ea410"}, - {file = "reportlab-3.6.12-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9b0861d8f40d7a24b094b8834f6a489b9e8c70bceaa7fa98237eed229671ce"}, - {file = "reportlab-3.6.12-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26c25ea4afa8b92a2c14f4edc41c8fc30505745ce84cae86538e80cacadd7ae2"}, - {file = "reportlab-3.6.12-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55a070206580e161b6bbe1a96abf816c18d4c2c225d49916654714c93d842835"}, - {file = "reportlab-3.6.12-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c40e108072379ff83dd7442159ebc249d12eb8eec15b70614953fecd2c403792"}, - {file = "reportlab-3.6.12-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39e92fa4ab2a8f0f2cc051d9c1e3acb881340c07ef59c0c8b627861343d653c0"}, - {file = "reportlab-3.6.12-cp37-cp37m-win32.whl", hash = "sha256:3fd1ffdd5204301eb4c290a5752ac62f44d2d0b262e02e35a1e5234c13e14662"}, - {file = "reportlab-3.6.12-cp37-cp37m-win_amd64.whl", hash = "sha256:d4cecfb48a6cfbfe2caf0fc280cecea999699e63bc98cb02254bd87b39eff677"}, - {file = "reportlab-3.6.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b6a1b685da0b9a8000bb980e02d9d5be202d0cc539af113b661c76c051fca6f1"}, - {file = "reportlab-3.6.12-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f5808e1dac6b66c109d6205ce2aebf84bb89e1a1493b7e6df38932df5ebfb9cf"}, - {file = "reportlab-3.6.12-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb83df8f7840321d34cb5b24c972c617a8c1716c8a36e5050fff56adf5891b8c"}, - {file = "reportlab-3.6.12-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72ec333f089b4fce5a6d740ed0a1963a3994146be195722da0d8e14d4a7e1600"}, - {file = "reportlab-3.6.12-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71cf73f9907c444ef663ea653dbac24af07c307079572c3ff8f20ad1463af3b7"}, - {file = "reportlab-3.6.12-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cee3b6ebef5e4a8654ec5f0effeb1a2bb157ad87b0ac856871d25a805c0f2f90"}, - {file = "reportlab-3.6.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db62bed0774778fdf82c609cb9efd0062f2fdcd285be527d01f6be9fd9755888"}, - {file = "reportlab-3.6.12-cp38-cp38-win32.whl", hash = "sha256:b777ddc57b2d3366cbc540616034cdc1089ca0a31fefc907028e1dd62a6bf16c"}, - {file = "reportlab-3.6.12-cp38-cp38-win_amd64.whl", hash = "sha256:c07ec796a2a5d44bf787f2b623b6e668a389b0cafb78af34cf74554ff3bc532b"}, - {file = "reportlab-3.6.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cdd206883e999278d2af656f988dfcc89eb0c175ce6d75e87b713cf1e792c0c4"}, - {file = "reportlab-3.6.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3a62e51a4a47616896bd0f1e9cc3fbfb174b713794a5031a34b84f69dbe01775"}, - {file = "reportlab-3.6.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1dd0307b2b13b0482ac8314fd793fbbce263a428b189371addf0466784e1d597"}, - {file = "reportlab-3.6.12-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c56d701f7dc662e1d3d7fe364e66fa1339eafce54a488c2d16ec0ea49dc213c2"}, - {file = "reportlab-3.6.12-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:109009b02fc225882ea766a5ed8be0ef473fa1356e252a3f651a6aa89b4a195f"}, - {file = "reportlab-3.6.12-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b3648f3c340b6b6aabf9352341478c708cee6f00c5cd5c902311fcf4ce870f3c"}, - {file = "reportlab-3.6.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:907f7cd4832bb295d0c1573de15cc5aab5988282caf2ee7a2b1276fb6cdf502b"}, - {file = "reportlab-3.6.12-cp39-cp39-win32.whl", hash = "sha256:93e229519d046491b798f2c12dbbf2f3e237e89589aa5cbb5e1d8c1a978816db"}, - {file = "reportlab-3.6.12-cp39-cp39-win_amd64.whl", hash = "sha256:498b4ec7e73426de64c6bf6ec03c5b3f10dedf5db8a9e13fdf195f95a3d065aa"}, - {file = "reportlab-3.6.12.tar.gz", hash = "sha256:b13cebf4e397bba14542bcd023338b6ff2c151a3a12aabca89eecbf972cb361a"}, + +[package.dependencies] +commonmark = ">=0.8.1" +docutils = ">=0.11" +sphinx = ">=1.3.1" + +[[package]] +name = "red-black-tree-mod" +version = "1.20" +description = "Flexible python implementation of red black trees" +optional = true +python-versions = "*" +files = [ + {file = "red-black-tree-mod-1.20.tar.gz", hash = "sha256:2448e6fc9cbf1be204c753f352c6ee49aa8156dbf1faa57dfc26bd7705077e0a"}, ] -requests = [ - {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, - {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, + +[[package]] +name = "referencing" +version = "0.35.1" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, + {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, ] -requests-mock = [ - {file = "requests-mock-1.10.0.tar.gz", hash = "sha256:59c9c32419a9fb1ae83ec242d98e889c45bd7d7a65d48375cc243ec08441658b"}, - {file = "requests_mock-1.10.0-py2.py3-none-any.whl", hash = "sha256:2fdbb637ad17ee15c06f33d31169e71bf9fe2bdb7bc9da26185be0dd8d842699"}, + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" + +[[package]] +name = "reportlab" +version = "4.2.2" +description = "The Reportlab Toolkit" +optional = true +python-versions = "<4,>=3.7" +files = [ + {file = "reportlab-4.2.2-py3-none-any.whl", hash = "sha256:927616931637e2f13e2ee3b3b6316d7a07803170e258621cff7d138bde17fbb5"}, + {file = "reportlab-4.2.2.tar.gz", hash = "sha256:765eecbdd68491c56947e29c38b8b69b834ee5dbbdd2fb7409f08ebdebf04428"}, ] -rtfde = [ - {file = "RTFDE-0.0.2-py3-none-any.whl", hash = "sha256:18386e4f060cee12a2a8035b0acf0cc99689f5dff1bf347bab7e92351860a21d"}, - {file = "RTFDE-0.0.2.tar.gz", hash = "sha256:b86b5d734950fe8745a5b89133f50554252dbd67c6d1b9265e23ee140e7ea8a2"}, + +[package.dependencies] +chardet = "*" +pillow = ">=9.0.0" + +[package.extras] +accel = ["rl-accel (>=0.9.0,<1.1)"] +pycairo = ["freetype-py (>=2.3.0,<2.4)", "rlPyCairo (>=0.2.0,<1)"] +renderpm = ["rl-renderPM (>=4.0.3,<4.1)"] + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] -send2trash = [ - {file = "Send2Trash-1.8.0-py3-none-any.whl", hash = "sha256:f20eaadfdb517eaca5ce077640cb261c7d2698385a6a0f072a4a5447fd49fa08"}, - {file = "Send2Trash-1.8.0.tar.gz", hash = "sha256:d2c24762fd3759860a0aff155e45871447ea58d2be6bdd39b5c8f966a0c99c2d"}, + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "requests-mock" +version = "1.12.1" +description = "Mock out responses from the requests package" +optional = false +python-versions = ">=3.5" +files = [ + {file = "requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401"}, + {file = "requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563"}, ] -setuptools = [ - {file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"}, - {file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"}, + +[package.dependencies] +requests = ">=2.22,<3" + +[package.extras] +fixture = ["fixtures"] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +description = "A pure python RFC3339 validator" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, + {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"}, ] -six = [ + +[package.dependencies] +six = "*" + +[[package]] +name = "rfc3986-validator" +version = "0.1.1" +description = "Pure python rfc3986 validator" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9"}, + {file = "rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055"}, +] + +[[package]] +name = "rpds-py" +version = "0.18.1" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "rpds_py-0.18.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:d31dea506d718693b6b2cffc0648a8929bdc51c70a311b2770f09611caa10d53"}, + {file = "rpds_py-0.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:732672fbc449bab754e0b15356c077cc31566df874964d4801ab14f71951ea80"}, + {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a98a1f0552b5f227a3d6422dbd61bc6f30db170939bd87ed14f3c339aa6c7c9"}, + {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f1944ce16401aad1e3f7d312247b3d5de7981f634dc9dfe90da72b87d37887d"}, + {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38e14fb4e370885c4ecd734f093a2225ee52dc384b86fa55fe3f74638b2cfb09"}, + {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08d74b184f9ab6289b87b19fe6a6d1a97fbfea84b8a3e745e87a5de3029bf944"}, + {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d70129cef4a8d979caa37e7fe957202e7eee8ea02c5e16455bc9808a59c6b2f0"}, + {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce0bb20e3a11bd04461324a6a798af34d503f8d6f1aa3d2aa8901ceaf039176d"}, + {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81c5196a790032e0fc2464c0b4ab95f8610f96f1f2fa3d4deacce6a79852da60"}, + {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f3027be483868c99b4985fda802a57a67fdf30c5d9a50338d9db646d590198da"}, + {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d44607f98caa2961bab4fa3c4309724b185b464cdc3ba6f3d7340bac3ec97cc1"}, + {file = "rpds_py-0.18.1-cp310-none-win32.whl", hash = "sha256:c273e795e7a0f1fddd46e1e3cb8be15634c29ae8ff31c196debb620e1edb9333"}, + {file = "rpds_py-0.18.1-cp310-none-win_amd64.whl", hash = "sha256:8352f48d511de5f973e4f2f9412736d7dea76c69faa6d36bcf885b50c758ab9a"}, + {file = "rpds_py-0.18.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6b5ff7e1d63a8281654b5e2896d7f08799378e594f09cf3674e832ecaf396ce8"}, + {file = "rpds_py-0.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8927638a4d4137a289e41d0fd631551e89fa346d6dbcfc31ad627557d03ceb6d"}, + {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:154bf5c93d79558b44e5b50cc354aa0459e518e83677791e6adb0b039b7aa6a7"}, + {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07f2139741e5deb2c5154a7b9629bc5aa48c766b643c1a6750d16f865a82c5fc"}, + {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c7672e9fba7425f79019db9945b16e308ed8bc89348c23d955c8c0540da0a07"}, + {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:489bdfe1abd0406eba6b3bb4fdc87c7fa40f1031de073d0cfb744634cc8fa261"}, + {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c20f05e8e3d4fc76875fc9cb8cf24b90a63f5a1b4c5b9273f0e8225e169b100"}, + {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:967342e045564cef76dfcf1edb700b1e20838d83b1aa02ab313e6a497cf923b8"}, + {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2cc7c1a47f3a63282ab0f422d90ddac4aa3034e39fc66a559ab93041e6505da7"}, + {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f7afbfee1157e0f9376c00bb232e80a60e59ed716e3211a80cb8506550671e6e"}, + {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9e6934d70dc50f9f8ea47081ceafdec09245fd9f6032669c3b45705dea096b88"}, + {file = "rpds_py-0.18.1-cp311-none-win32.whl", hash = "sha256:c69882964516dc143083d3795cb508e806b09fc3800fd0d4cddc1df6c36e76bb"}, + {file = "rpds_py-0.18.1-cp311-none-win_amd64.whl", hash = "sha256:70a838f7754483bcdc830444952fd89645569e7452e3226de4a613a4c1793fb2"}, + {file = "rpds_py-0.18.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3dd3cd86e1db5aadd334e011eba4e29d37a104b403e8ca24dcd6703c68ca55b3"}, + {file = "rpds_py-0.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05f3d615099bd9b13ecf2fc9cf2d839ad3f20239c678f461c753e93755d629ee"}, + {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35b2b771b13eee8729a5049c976197ff58a27a3829c018a04341bcf1ae409b2b"}, + {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee17cd26b97d537af8f33635ef38be873073d516fd425e80559f4585a7b90c43"}, + {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b646bf655b135ccf4522ed43d6902af37d3f5dbcf0da66c769a2b3938b9d8184"}, + {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19ba472b9606c36716062c023afa2484d1e4220548751bda14f725a7de17b4f6"}, + {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e30ac5e329098903262dc5bdd7e2086e0256aa762cc8b744f9e7bf2a427d3f8"}, + {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d58ad6317d188c43750cb76e9deacf6051d0f884d87dc6518e0280438648a9ac"}, + {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e1735502458621921cee039c47318cb90b51d532c2766593be6207eec53e5c4c"}, + {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f5bab211605d91db0e2995a17b5c6ee5edec1270e46223e513eaa20da20076ac"}, + {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2fc24a329a717f9e2448f8cd1f960f9dac4e45b6224d60734edeb67499bab03a"}, + {file = "rpds_py-0.18.1-cp312-none-win32.whl", hash = "sha256:1805d5901779662d599d0e2e4159d8a82c0b05faa86ef9222bf974572286b2b6"}, + {file = "rpds_py-0.18.1-cp312-none-win_amd64.whl", hash = "sha256:720edcb916df872d80f80a1cc5ea9058300b97721efda8651efcd938a9c70a72"}, + {file = "rpds_py-0.18.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:c827576e2fa017a081346dce87d532a5310241648eb3700af9a571a6e9fc7e74"}, + {file = "rpds_py-0.18.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:aa3679e751408d75a0b4d8d26d6647b6d9326f5e35c00a7ccd82b78ef64f65f8"}, + {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0abeee75434e2ee2d142d650d1e54ac1f8b01e6e6abdde8ffd6eeac6e9c38e20"}, + {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed402d6153c5d519a0faf1bb69898e97fb31613b49da27a84a13935ea9164dfc"}, + {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:338dee44b0cef8b70fd2ef54b4e09bb1b97fc6c3a58fea5db6cc083fd9fc2724"}, + {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7750569d9526199c5b97e5a9f8d96a13300950d910cf04a861d96f4273d5b104"}, + {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:607345bd5912aacc0c5a63d45a1f73fef29e697884f7e861094e443187c02be5"}, + {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:207c82978115baa1fd8d706d720b4a4d2b0913df1c78c85ba73fe6c5804505f0"}, + {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6d1e42d2735d437e7e80bab4d78eb2e459af48c0a46e686ea35f690b93db792d"}, + {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5463c47c08630007dc0fe99fb480ea4f34a89712410592380425a9b4e1611d8e"}, + {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:06d218939e1bf2ca50e6b0ec700ffe755e5216a8230ab3e87c059ebb4ea06afc"}, + {file = "rpds_py-0.18.1-cp38-none-win32.whl", hash = "sha256:312fe69b4fe1ffbe76520a7676b1e5ac06ddf7826d764cc10265c3b53f96dbe9"}, + {file = "rpds_py-0.18.1-cp38-none-win_amd64.whl", hash = "sha256:9437ca26784120a279f3137ee080b0e717012c42921eb07861b412340f85bae2"}, + {file = "rpds_py-0.18.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:19e515b78c3fc1039dd7da0a33c28c3154458f947f4dc198d3c72db2b6b5dc93"}, + {file = "rpds_py-0.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7b28c5b066bca9a4eb4e2f2663012debe680f097979d880657f00e1c30875a0"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:673fdbbf668dd958eff750e500495ef3f611e2ecc209464f661bc82e9838991e"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d960de62227635d2e61068f42a6cb6aae91a7fe00fca0e3aeed17667c8a34611"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352a88dc7892f1da66b6027af06a2e7e5d53fe05924cc2cfc56495b586a10b72"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e0ee01ad8260184db21468a6e1c37afa0529acc12c3a697ee498d3c2c4dcaf3"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4c39ad2f512b4041343ea3c7894339e4ca7839ac38ca83d68a832fc8b3748ab"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aaa71ee43a703c321906813bb252f69524f02aa05bf4eec85f0c41d5d62d0f4c"}, + {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6cd8098517c64a85e790657e7b1e509b9fe07487fd358e19431cb120f7d96338"}, + {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4adec039b8e2928983f885c53b7cc4cda8965b62b6596501a0308d2703f8af1b"}, + {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:32b7daaa3e9389db3695964ce8e566e3413b0c43e3394c05e4b243a4cd7bef26"}, + {file = "rpds_py-0.18.1-cp39-none-win32.whl", hash = "sha256:2625f03b105328729f9450c8badda34d5243231eef6535f80064d57035738360"}, + {file = "rpds_py-0.18.1-cp39-none-win_amd64.whl", hash = "sha256:bf18932d0003c8c4d51a39f244231986ab23ee057d235a12b2684ea26a353590"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cbfbea39ba64f5e53ae2915de36f130588bba71245b418060ec3330ebf85678e"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a3d456ff2a6a4d2adcdf3c1c960a36f4fd2fec6e3b4902a42a384d17cf4e7a65"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7700936ef9d006b7ef605dc53aa364da2de5a3aa65516a1f3ce73bf82ecfc7ae"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:51584acc5916212e1bf45edd17f3a6b05fe0cbb40482d25e619f824dccb679de"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:942695a206a58d2575033ff1e42b12b2aece98d6003c6bc739fbf33d1773b12f"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b906b5f58892813e5ba5c6056d6a5ad08f358ba49f046d910ad992196ea61397"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6f8e3fecca256fefc91bb6765a693d96692459d7d4c644660a9fff32e517843"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7732770412bab81c5a9f6d20aeb60ae943a9b36dcd990d876a773526468e7163"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:bd1105b50ede37461c1d51b9698c4f4be6e13e69a908ab7751e3807985fc0346"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:618916f5535784960f3ecf8111581f4ad31d347c3de66d02e728de460a46303c"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:17c6d2155e2423f7e79e3bb18151c686d40db42d8645e7977442170c360194d4"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6c4c4c3f878df21faf5fac86eda32671c27889e13570645a9eea0a1abdd50922"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:fab6ce90574645a0d6c58890e9bcaac8d94dff54fb51c69e5522a7358b80ab64"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:531796fb842b53f2695e94dc338929e9f9dbf473b64710c28af5a160b2a8927d"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:740884bc62a5e2bbb31e584f5d23b32320fd75d79f916f15a788d527a5e83644"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:998125738de0158f088aef3cb264a34251908dd2e5d9966774fdab7402edfab7"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2be6e9dd4111d5b31ba3b74d17da54a8319d8168890fbaea4b9e5c3de630ae5"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0cee71bc618cd93716f3c1bf56653740d2d13ddbd47673efa8bf41435a60daa"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c3caec4ec5cd1d18e5dd6ae5194d24ed12785212a90b37f5f7f06b8bedd7139"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:27bba383e8c5231cd559affe169ca0b96ec78d39909ffd817f28b166d7ddd4d8"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:a888e8bdb45916234b99da2d859566f1e8a1d2275a801bb8e4a9644e3c7e7909"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6031b25fb1b06327b43d841f33842b383beba399884f8228a6bb3df3088485ff"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48c2faaa8adfacefcbfdb5f2e2e7bdad081e5ace8d182e5f4ade971f128e6bb3"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:d85164315bd68c0806768dc6bb0429c6f95c354f87485ee3593c4f6b14def2bd"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6afd80f6c79893cfc0574956f78a0add8c76e3696f2d6a15bca2c66c415cf2d4"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa242ac1ff583e4ec7771141606aafc92b361cd90a05c30d93e343a0c2d82a89"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21be4770ff4e08698e1e8e0bce06edb6ea0626e7c8f560bc08222880aca6a6f"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c45a639e93a0c5d4b788b2613bd637468edd62f8f95ebc6fcc303d58ab3f0a8"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910e71711d1055b2768181efa0a17537b2622afeb0424116619817007f8a2b10"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b9bb1f182a97880f6078283b3505a707057c42bf55d8fca604f70dedfdc0772a"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1d54f74f40b1f7aaa595a02ff42ef38ca654b1469bef7d52867da474243cc633"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:8d2e182c9ee01135e11e9676e9a62dfad791a7a467738f06726872374a83db49"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:636a15acc588f70fda1661234761f9ed9ad79ebed3f2125d44be0862708b666e"}, + {file = "rpds_py-0.18.1.tar.gz", hash = "sha256:dc48b479d540770c811fbd1eb9ba2bb66951863e448efec2e2c102625328e92f"}, +] + +[[package]] +name = "rtfde" +version = "0.1.2" +description = "A library for extracting HTML content from RTF encapsulated HTML as commonly found in the exchange MSG email format." +optional = true +python-versions = "~=3.8" +files = [ + {file = "RTFDE-0.1.2-py3-none-any.whl", hash = "sha256:f6d1450c99b04e930da130e8b419aa33b1f953623e1b94ad5c0f67f0362eb737"}, +] + +[package.dependencies] +lark = ">=1.1.8,<1.2.0" +oletools = ">=0.56" + +[package.extras] +dev = ["coverage (>=7.2.2,<7.3.0)", "lxml (>=4.6,<5.0)", "mypy (>=1.1,<2.0)", "pdoc3 (>=0.10.0,<0.11.0)"] +msg-parse = ["extract-msg (>=0.27,<1.0)"] + +[[package]] +name = "send2trash" +version = "1.8.3" +description = "Send file to trash natively under Mac OS X, Windows and Linux" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "Send2Trash-1.8.3-py3-none-any.whl", hash = "sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9"}, + {file = "Send2Trash-1.8.3.tar.gz", hash = "sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf"}, +] + +[package.extras] +nativelib = ["pyobjc-framework-Cocoa", "pywin32"] +objc = ["pyobjc-framework-Cocoa"] +win32 = ["pywin32"] + +[[package]] +name = "setuptools" +version = "70.2.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-70.2.0-py3-none-any.whl", hash = "sha256:b8b8060bb426838fbe942479c90296ce976249451118ef566a5a0b7d8b78fb05"}, + {file = "setuptools-70.2.0.tar.gz", hash = "sha256:bd63e505105011b25c3c11f753f7e3b8465ea739efddaccef8f0efac2137bac1"}, +] + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -sniffio = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] -snowballstemmer = [ + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +optional = true +python-versions = "*" +files = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] -soupsieve = [ - {file = "soupsieve-2.3.2.post1-py3-none-any.whl", hash = "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759"}, - {file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"}, + +[[package]] +name = "soupsieve" +version = "2.5" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, ] -sphinx = [ - {file = "Sphinx-5.3.0.tar.gz", hash = "sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5"}, - {file = "sphinx-5.3.0-py3-none-any.whl", hash = "sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d"}, + +[[package]] +name = "sphinx" +version = "7.3.7" +description = "Python documentation generator" +optional = true +python-versions = ">=3.9" +files = [ + {file = "sphinx-7.3.7-py3-none-any.whl", hash = "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3"}, + {file = "sphinx-7.3.7.tar.gz", hash = "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc"}, ] -sphinx-autodoc-typehints = [ - {file = "sphinx_autodoc_typehints-1.19.5-py3-none-any.whl", hash = "sha256:ea55b3cc3f485e3a53668bcdd08de78121ab759f9724392fdb5bf3483d786328"}, - {file = "sphinx_autodoc_typehints-1.19.5.tar.gz", hash = "sha256:38a227378e2bc15c84e29af8cb1d7581182da1107111fd1c88b19b5eb7076205"}, + +[package.dependencies] +alabaster = ">=0.7.14,<0.8.0" +babel = ">=2.9" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +docutils = ">=0.18.1,<0.22" +imagesize = ">=1.3" +importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} +Jinja2 = ">=3.0" +packaging = ">=21.0" +Pygments = ">=2.14" +requests = ">=2.25.0" +snowballstemmer = ">=2.0" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = ">=1.1.9" +tomli = {version = ">=2", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=3.5.0)", "importlib_metadata", "mypy (==1.9.0)", "pytest (>=6.0)", "ruff (==0.3.7)", "sphinx-lint", "tomli", "types-docutils", "types-requests"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=6.0)", "setuptools (>=67.0)"] + +[[package]] +name = "sphinx-autodoc-typehints" +version = "2.2.2" +description = "Type hints (PEP 484) support for the Sphinx autodoc extension" +optional = true +python-versions = ">=3.9" +files = [ + {file = "sphinx_autodoc_typehints-2.2.2-py3-none-any.whl", hash = "sha256:b98337a8530c95b73ba0c65465847a8ab0a13403bdc81294d5ef396bbd1f783e"}, + {file = "sphinx_autodoc_typehints-2.2.2.tar.gz", hash = "sha256:128e600eeef63b722f3d8dac6403594592c8cade3ba66fd11dcb997465ee259d"}, ] -sphinxcontrib-applehelp = [ - {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, - {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, + +[package.dependencies] +sphinx = ">=7.3.5" + +[package.extras] +docs = ["furo (>=2024.1.29)"] +numpy = ["nptyping (>=2.5)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.4.4)", "defusedxml (>=0.7.1)", "diff-cover (>=9)", "pytest (>=8.1.1)", "pytest-cov (>=5)", "sphobjinv (>=2.3.1)", "typing-extensions (>=4.11)"] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.8" +description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" +optional = true +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_applehelp-1.0.8-py3-none-any.whl", hash = "sha256:cb61eb0ec1b61f349e5cc36b2028e9e7ca765be05e49641c97241274753067b4"}, + {file = "sphinxcontrib_applehelp-1.0.8.tar.gz", hash = "sha256:c40a4f96f3776c4393d933412053962fac2b84f4c99a7982ba42e09576a70619"}, ] -sphinxcontrib-devhelp = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.6" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" +optional = true +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_devhelp-1.0.6-py3-none-any.whl", hash = "sha256:6485d09629944511c893fa11355bda18b742b83a2b181f9a009f7e500595c90f"}, + {file = "sphinxcontrib_devhelp-1.0.6.tar.gz", hash = "sha256:9893fd3f90506bc4b97bdb977ceb8fbd823989f4316b28c3841ec128544372d3"}, ] -sphinxcontrib-htmlhelp = [ - {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, - {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.0.5" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +optional = true +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_htmlhelp-2.0.5-py3-none-any.whl", hash = "sha256:393f04f112b4d2f53d93448d4bce35842f62b307ccdc549ec1585e950bc35e04"}, + {file = "sphinxcontrib_htmlhelp-2.0.5.tar.gz", hash = "sha256:0dc87637d5de53dd5eec3a6a01753b1ccf99494bd756aafecd74b4fa9e729015"}, ] -sphinxcontrib-jsmath = [ + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +standalone = ["Sphinx (>=5)"] +test = ["html5lib", "pytest"] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +optional = true +python-versions = ">=3.5" +files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, ] -sphinxcontrib-qthelp = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, + +[package.extras] +test = ["flake8", "mypy", "pytest"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.7" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" +optional = true +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_qthelp-1.0.7-py3-none-any.whl", hash = "sha256:e2ae3b5c492d58fcbd73281fbd27e34b8393ec34a073c792642cd8e529288182"}, + {file = "sphinxcontrib_qthelp-1.0.7.tar.gz", hash = "sha256:053dedc38823a80a7209a80860b16b722e9e0209e32fea98c90e4e6624588ed6"}, ] -sphinxcontrib-serializinghtml = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.10" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" +optional = true +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_serializinghtml-1.1.10-py3-none-any.whl", hash = "sha256:326369b8df80a7d2d8d7f99aa5ac577f51ea51556ed974e7716cfd4fca3f6cb7"}, + {file = "sphinxcontrib_serializinghtml-1.1.10.tar.gz", hash = "sha256:93f3f5dc458b91b192fe10c397e324f262cf163d79f3282c158e8436a2c4511f"}, ] -terminado = [ - {file = "terminado-0.17.0-py3-none-any.whl", hash = "sha256:bf6fe52accd06d0661d7611cc73202121ec6ee51e46d8185d489ac074ca457c2"}, - {file = "terminado-0.17.0.tar.gz", hash = "sha256:520feaa3aeab8ad64a69ca779be54be9234edb2d0d6567e76c93c2c9a4e6e43f"}, + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "stack-data" +version = "0.6.3" +description = "Extract data from python stack frames and tracebacks for informative displays" +optional = false +python-versions = "*" +files = [ + {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, + {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, ] -tinycss2 = [ - {file = "tinycss2-1.2.1-py3-none-any.whl", hash = "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847"}, - {file = "tinycss2-1.2.1.tar.gz", hash = "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627"}, + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + +[[package]] +name = "terminado" +version = "0.18.1" +description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0"}, + {file = "terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e"}, ] -tomli = [ + +[package.dependencies] +ptyprocess = {version = "*", markers = "os_name != \"nt\""} +pywinpty = {version = ">=1.1.0", markers = "os_name == \"nt\""} +tornado = ">=6.1.0" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["pre-commit", "pytest (>=7.0)", "pytest-timeout"] +typing = ["mypy (>=1.6,<2.0)", "traitlets (>=5.11.1)"] + +[[package]] +name = "tinycss2" +version = "1.3.0" +description = "A tiny CSS parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tinycss2-1.3.0-py3-none-any.whl", hash = "sha256:54a8dbdffb334d536851be0226030e9505965bb2f30f21a4a82c55fb2a80fae7"}, + {file = "tinycss2-1.3.0.tar.gz", hash = "sha256:152f9acabd296a8375fbca5b84c961ff95971fcfc32e79550c8df8e29118c54d"}, +] + +[package.dependencies] +webencodings = ">=0.4" + +[package.extras] +doc = ["sphinx", "sphinx_rtd_theme"] +test = ["pytest", "ruff"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -tornado = [ - {file = "tornado-6.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72"}, - {file = "tornado-6.2-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9"}, - {file = "tornado-6.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba09ef14ca9893954244fd872798b4ccb2367c165946ce2dd7376aebdde8e3ac"}, - {file = "tornado-6.2-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8150f721c101abdef99073bf66d3903e292d851bee51910839831caba341a75"}, - {file = "tornado-6.2-cp37-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3a2f5999215a3a06a4fc218026cd84c61b8b2b40ac5296a6db1f1451ef04c1e"}, - {file = "tornado-6.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5f8c52d219d4995388119af7ccaa0bcec289535747620116a58d830e7c25d8a8"}, - {file = "tornado-6.2-cp37-abi3-musllinux_1_1_i686.whl", hash = "sha256:6fdfabffd8dfcb6cf887428849d30cf19a3ea34c2c248461e1f7d718ad30b66b"}, - {file = "tornado-6.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:1d54d13ab8414ed44de07efecb97d4ef7c39f7438cf5e976ccd356bebb1b5fca"}, - {file = "tornado-6.2-cp37-abi3-win32.whl", hash = "sha256:5c87076709343557ef8032934ce5f637dbb552efa7b21d08e89ae7619ed0eb23"}, - {file = "tornado-6.2-cp37-abi3-win_amd64.whl", hash = "sha256:e5f923aa6a47e133d1cf87d60700889d7eae68988704e20c75fb2d65677a8e4b"}, - {file = "tornado-6.2.tar.gz", hash = "sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13"}, + +[[package]] +name = "tornado" +version = "6.4.1" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = false +python-versions = ">=3.8" +files = [ + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"}, + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"}, + {file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"}, + {file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"}, + {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"}, ] -traitlets = [ - {file = "traitlets-5.5.0-py3-none-any.whl", hash = "sha256:1201b2c9f76097195989cdf7f65db9897593b0dfd69e4ac96016661bb6f0d30f"}, - {file = "traitlets-5.5.0.tar.gz", hash = "sha256:b122f9ff2f2f6c1709dab289a05555be011c87828e911c0cf4074b85cb780a79"}, + +[[package]] +name = "traitlets" +version = "5.14.3" +description = "Traitlets Python configuration system" +optional = false +python-versions = ">=3.8" +files = [ + {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, + {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, ] -typed-ast = [ - {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, - {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, - {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, - {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, - {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, - {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, - {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, - {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, - {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, - {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] + +[[package]] +name = "types-cffi" +version = "1.16.0.20240331" +description = "Typing stubs for cffi" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-cffi-1.16.0.20240331.tar.gz", hash = "sha256:b8b20d23a2b89cfed5f8c5bc53b0cb8677c3aac6d970dbc771e28b9c698f5dee"}, + {file = "types_cffi-1.16.0.20240331-py3-none-any.whl", hash = "sha256:a363e5ea54a4eb6a4a105d800685fde596bc318089b025b27dee09849fe41ff0"}, ] -types-click = [ + +[package.dependencies] +types-setuptools = "*" + +[[package]] +name = "types-click" +version = "7.1.8" +description = "Typing stubs for click" +optional = false +python-versions = "*" +files = [ {file = "types-click-7.1.8.tar.gz", hash = "sha256:b6604968be6401dc516311ca50708a0a28baa7a0cb840efd7412f0dbbff4e092"}, {file = "types_click-7.1.8-py3-none-any.whl", hash = "sha256:8cb030a669e2e927461be9827375f83c16b8178c365852c060a34e24871e7e81"}, ] -types-flask = [ + +[[package]] +name = "types-flask" +version = "1.1.6" +description = "Typing stubs for Flask" +optional = false +python-versions = "*" +files = [ {file = "types-Flask-1.1.6.tar.gz", hash = "sha256:aac777b3abfff9436e6b01f6d08171cf23ea6e5be71cbf773aaabb1c5763e9cf"}, {file = "types_Flask-1.1.6-py3-none-any.whl", hash = "sha256:6ab8a9a5e258b76539d652f6341408867298550b19b81f0e41e916825fc39087"}, ] -types-jinja2 = [ + +[package.dependencies] +types-click = "*" +types-Jinja2 = "*" +types-Werkzeug = "*" + +[[package]] +name = "types-jinja2" +version = "2.11.9" +description = "Typing stubs for Jinja2" +optional = false +python-versions = "*" +files = [ {file = "types-Jinja2-2.11.9.tar.gz", hash = "sha256:dbdc74a40aba7aed520b7e4d89e8f0fe4286518494208b35123bcf084d4b8c81"}, {file = "types_Jinja2-2.11.9-py3-none-any.whl", hash = "sha256:60a1e21e8296979db32f9374d8a239af4cb541ff66447bb915d8ad398f9c63b2"}, ] -types-markupsafe = [ + +[package.dependencies] +types-MarkupSafe = "*" + +[[package]] +name = "types-markupsafe" +version = "1.1.10" +description = "Typing stubs for MarkupSafe" +optional = false +python-versions = "*" +files = [ {file = "types-MarkupSafe-1.1.10.tar.gz", hash = "sha256:85b3a872683d02aea3a5ac2a8ef590193c344092032f58457287fbf8e06711b1"}, {file = "types_MarkupSafe-1.1.10-py3-none-any.whl", hash = "sha256:ca2bee0f4faafc45250602567ef38d533e877d2ddca13003b319c551ff5b3cc5"}, ] -types-python-dateutil = [ - {file = "types-python-dateutil-2.8.19.4.tar.gz", hash = "sha256:351a8ca9afd4aea662f87c1724d2e1ae59f9f5f99691be3b3b11d2393cd3aaa1"}, - {file = "types_python_dateutil-2.8.19.4-py3-none-any.whl", hash = "sha256:722a55be8e2eeff749c3e166e7895b0e2f4d29ab4921c0cff27aa6b997d7ee2e"}, + +[[package]] +name = "types-pyopenssl" +version = "24.1.0.20240425" +description = "Typing stubs for pyOpenSSL" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-pyOpenSSL-24.1.0.20240425.tar.gz", hash = "sha256:0a7e82626c1983dc8dc59292bf20654a51c3c3881bcbb9b337c1da6e32f0204e"}, + {file = "types_pyOpenSSL-24.1.0.20240425-py3-none-any.whl", hash = "sha256:f51a156835555dd2a1f025621e8c4fbe7493470331afeef96884d1d29bf3a473"}, ] -types-redis = [ - {file = "types-redis-4.3.21.6.tar.gz", hash = "sha256:f7969f73a0f79e9e7895f053a06d8b429fb7b5d4fe1269b8ee40463388f653ad"}, - {file = "types_redis-4.3.21.6-py3-none-any.whl", hash = "sha256:615e5a9142993789ffc22ee54435769b600da3e528bb51cf38430e5cd82af306"}, + +[package.dependencies] +cryptography = ">=35.0.0" +types-cffi = "*" + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20240316" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-python-dateutil-2.9.0.20240316.tar.gz", hash = "sha256:5d2f2e240b86905e40944dd787db6da9263f0deabef1076ddaed797351ec0202"}, + {file = "types_python_dateutil-2.9.0.20240316-py3-none-any.whl", hash = "sha256:6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b"}, ] -types-requests = [ - {file = "types-requests-2.28.11.5.tar.gz", hash = "sha256:a7df37cc6fb6187a84097da951f8e21d335448aa2501a6b0a39cbd1d7ca9ee2a"}, - {file = "types_requests-2.28.11.5-py3-none-any.whl", hash = "sha256:091d4a5a33c1b4f20d8b1b952aa8fa27a6e767c44c3cf65e56580df0b05fd8a9"}, + +[[package]] +name = "types-redis" +version = "4.6.0.20240425" +description = "Typing stubs for redis" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-redis-4.6.0.20240425.tar.gz", hash = "sha256:9402a10ee931d241fdfcc04592ebf7a661d7bb92a8dea631279f0d8acbcf3a22"}, + {file = "types_redis-4.6.0.20240425-py3-none-any.whl", hash = "sha256:ac5bc19e8f5997b9e76ad5d9cf15d0392d9f28cf5fc7746ea4a64b989c45c6a8"}, ] -types-urllib3 = [ - {file = "types-urllib3-1.26.25.4.tar.gz", hash = "sha256:eec5556428eec862b1ac578fb69aab3877995a99ffec9e5a12cf7fbd0cc9daee"}, - {file = "types_urllib3-1.26.25.4-py3-none-any.whl", hash = "sha256:ed6b9e8a8be488796f72306889a06a3fc3cb1aa99af02ab8afb50144d7317e49"}, + +[package.dependencies] +cryptography = ">=35.0.0" +types-pyOpenSSL = "*" + +[[package]] +name = "types-requests" +version = "2.32.0.20240622" +description = "Typing stubs for requests" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-requests-2.32.0.20240622.tar.gz", hash = "sha256:ed5e8a412fcc39159d6319385c009d642845f250c63902718f605cd90faade31"}, + {file = "types_requests-2.32.0.20240622-py3-none-any.whl", hash = "sha256:97bac6b54b5bd4cf91d407e62f0932a74821bc2211f22116d9ee1dd643826caf"}, ] -types-werkzeug = [ + +[package.dependencies] +urllib3 = ">=2" + +[[package]] +name = "types-setuptools" +version = "70.2.0.20240704" +description = "Typing stubs for setuptools" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-setuptools-70.2.0.20240704.tar.gz", hash = "sha256:2f8d28d16ca1607080f9fdf19595bd49c942884b2bbd6529c9b8a9a8fc8db911"}, + {file = "types_setuptools-70.2.0.20240704-py3-none-any.whl", hash = "sha256:6b892d5441c2ed58dd255724516e3df1db54892fb20597599aea66d04c3e4d7f"}, +] + +[[package]] +name = "types-werkzeug" +version = "1.0.9" +description = "Typing stubs for Werkzeug" +optional = false +python-versions = "*" +files = [ {file = "types-Werkzeug-1.0.9.tar.gz", hash = "sha256:5cc269604c400133d452a40cee6397655f878fc460e03fde291b9e3a5eaa518c"}, {file = "types_Werkzeug-1.0.9-py3-none-any.whl", hash = "sha256:194bd5715a13c598f05c63e8a739328657590943bce941e8a3619a6b5d4a54ec"}, ] -typing-extensions = [ - {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, - {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] -tzdata = [ - {file = "tzdata-2022.6-py2.py3-none-any.whl", hash = "sha256:04a680bdc5b15750c39c12a448885a51134a27ec9af83667663f0b3a1bf3f342"}, - {file = "tzdata-2022.6.tar.gz", hash = "sha256:91f11db4503385928c15598c98573e3af07e7229181bee5375bd30f1695ddcae"}, + +[[package]] +name = "tzdata" +version = "2024.1" +description = "Provider of IANA time zone data" +optional = true +python-versions = ">=2" +files = [ + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, ] -tzlocal = [ - {file = "tzlocal-4.2-py3-none-any.whl", hash = "sha256:89885494684c929d9191c57aa27502afc87a579be5cdd3225c77c463ea043745"}, - {file = "tzlocal-4.2.tar.gz", hash = "sha256:ee5842fa3a795f023514ac2d801c4a81d1743bbe642e3940143326b3a00addd7"}, + +[[package]] +name = "tzlocal" +version = "5.2" +description = "tzinfo object for the local timezone" +optional = true +python-versions = ">=3.8" +files = [ + {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"}, + {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"}, ] -urllib3 = [ - {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"}, - {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"}, + +[package.dependencies] +"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} +tzdata = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] + +[[package]] +name = "uri-template" +version = "1.3.0" +description = "RFC 6570 URI Template Processor" +optional = false +python-versions = ">=3.7" +files = [ + {file = "uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7"}, + {file = "uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363"}, ] -validators = [ - {file = "validators-0.20.0.tar.gz", hash = "sha256:24148ce4e64100a2d5e267233e23e7afeb55316b47d30faae7eb6e7292bc226a"}, + +[package.extras] +dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake8-commas", "flake8-comprehensions", "flake8-continuation", "flake8-datetimez", "flake8-docstrings", "flake8-import-order", "flake8-literal", "flake8-modern-annotations", "flake8-noqa", "flake8-pyproject", "flake8-requirements", "flake8-typechecking-import", "flake8-use-fstring", "mypy", "pep8-naming", "types-PyYAML"] + +[[package]] +name = "urllib3" +version = "2.2.2" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] -wcwidth = [ - {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, - {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, + +[package.dependencies] +brotli = {version = ">=1.0.9", optional = true, markers = "platform_python_implementation == \"CPython\" and extra == \"brotli\""} +brotlicffi = {version = ">=0.8.0", optional = true, markers = "platform_python_implementation != \"CPython\" and extra == \"brotli\""} + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "validators" +version = "0.30.0" +description = "Python Data Validation for Humans™" +optional = true +python-versions = ">=3.8" +files = [ + {file = "validators-0.30.0-py3-none-any.whl", hash = "sha256:0f2387a9fe76d26c151ab716de18e34467413800abced256fd3a506f4f51cbdc"}, + {file = "validators-0.30.0.tar.gz", hash = "sha256:c2dc5ffef052040bc11b62677429a904f9e04abaf35e0196ac509237cd3c9961"}, ] -webencodings = [ + +[package.extras] +crypto-eth-addresses = ["eth-hash[pycryptodome] (>=0.7.0)"] + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[[package]] +name = "webcolors" +version = "24.6.0" +description = "A library for working with the color formats defined by HTML and CSS." +optional = false +python-versions = ">=3.8" +files = [ + {file = "webcolors-24.6.0-py3-none-any.whl", hash = "sha256:8cf5bc7e28defd1d48b9e83d5fc30741328305a8195c29a8e668fa45586568a1"}, + {file = "webcolors-24.6.0.tar.gz", hash = "sha256:1d160d1de46b3e81e58d0a280d0c78b467dc80f47294b91b1ad8029d2cedb55b"}, +] + +[package.extras] +docs = ["furo", "sphinx", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-notfound-page", "sphinxext-opengraph"] +tests = ["coverage[toml]"] + +[[package]] +name = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +optional = false +python-versions = "*" +files = [ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, ] -websocket-client = [ - {file = "websocket-client-1.4.2.tar.gz", hash = "sha256:d6e8f90ca8e2dd4e8027c4561adeb9456b54044312dba655e7cae652ceb9ae59"}, - {file = "websocket_client-1.4.2-py3-none-any.whl", hash = "sha256:d6b06432f184438d99ac1f456eaf22fe1ade524c3dd16e661142dc54e9cba574"}, + +[[package]] +name = "websocket-client" +version = "1.8.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, + {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, ] -win-unicode-console = [ + +[package.extras] +docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + +[[package]] +name = "win-unicode-console" +version = "0.5" +description = "Enable Unicode input and display when running Python from Windows console." +optional = true +python-versions = "*" +files = [ {file = "win_unicode_console-0.5.zip", hash = "sha256:d4142d4d56d46f449d6f00536a73625a871cba040f0bc1a2e305a04578f07d1e"}, ] -wrapt = [ - {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, - {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, - {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, - {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, - {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, - {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, - {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, - {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, - {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, - {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, - {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, - {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, - {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, - {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, - {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, - {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, + +[[package]] +name = "wrapt" +version = "1.16.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.6" +files = [ + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, + {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, + {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, + {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, + {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, + {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, ] -zipp = [ - {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"}, - {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"}, + +[[package]] +name = "zipp" +version = "3.19.2" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, + {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, ] + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[extras] +brotli = ["urllib3"] +docs = ["Sphinx", "docutils", "recommonmark", "sphinx-autodoc-typehints"] +email = ["RTFDE", "extract_msg", "oletools"] +fileobjects = ["lief", "pydeep2", "python-magic"] +openioc = ["beautifulsoup4"] +pdfexport = ["reportlab"] +url = ["pyfaup"] +virustotal = ["validators"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.8" +content-hash = "09667f5fd27a84e620de8c25381fc28a4f24aad10579222501ac4a5046a40177" diff --git a/pymisp/__init__.py b/pymisp/__init__.py index 188c523..ca0868d 100644 --- a/pymisp/__init__.py +++ b/pymisp/__init__.py @@ -1,16 +1,21 @@ -__version__ = '2.4.166' +from __future__ import annotations + import logging import sys import warnings +import importlib.metadata + logger = logging.getLogger(__name__) +__version__ = importlib.metadata.version("pymisp") -def warning_2022(): - if sys.version_info < (3, 8): + +def warning_2024() -> None: + if sys.version_info < (3, 10): warnings.warn(""" -As our baseline system is the latest Ubuntu LTS, and Ubuntu LTS 20.04 has Python 3.8 available, -we will officially deprecate python versions below 3.8 on January 1st 2022. +As our baseline system is the latest Ubuntu LTS, and Ubuntu LTS 22.04 has Python 3.10 available, +we will officially deprecate python versions below 3.10 on January 1st 2024. **Please update your codebase.**""", DeprecationWarning, stacklevel=3) @@ -25,26 +30,24 @@ Response (if any): try: - warning_2022() - from .exceptions import PyMISPError, NewEventError, NewAttributeError, MissingDependency, NoURL, NoKey, InvalidMISPObject, UnknownMISPObjectTemplate, PyMISPInvalidFormat, MISPServerError, PyMISPNotImplementedYet, PyMISPUnexpectedResponse, PyMISPEmptyResponse # noqa - from .abstract import AbstractMISP, MISPEncode, pymisp_json_default, MISPTag, Distribution, ThreatLevel, Analysis # noqa + warning_2024() + from .exceptions import (PyMISPError, NewEventError, NewAttributeError, MissingDependency, NoURL, NoKey, # noqa + InvalidMISPObject, UnknownMISPObjectTemplate, PyMISPInvalidFormat, MISPServerError, PyMISPNotImplementedYet, PyMISPUnexpectedResponse, PyMISPEmptyResponse) + from .abstract import AbstractMISP, MISPEncode, pymisp_json_default, MISPTag, Distribution, ThreatLevel, Analysis # noqa from .mispevent import (MISPEvent, MISPAttribute, MISPObjectReference, MISPObjectAttribute, MISPObject, MISPUser, # noqa MISPOrganisation, MISPSighting, MISPLog, MISPShadowAttribute, MISPWarninglist, MISPTaxonomy, MISPNoticelist, MISPObjectTemplate, MISPSharingGroup, MISPRole, MISPServer, MISPFeed, MISPEventDelegation, MISPUserSetting, MISPInbox, MISPEventBlocklist, MISPOrganisationBlocklist, - MISPEventReport, MISPGalaxyCluster, MISPGalaxyClusterElement, MISPGalaxyClusterRelation, - MISPCorrelationExclusion, MISPGalaxy, MISPDecayingModel) + MISPEventReport, MISPCorrelationExclusion, MISPDecayingModel, MISPGalaxy, MISPGalaxyCluster, + MISPGalaxyClusterElement, MISPGalaxyClusterRelation, MISPNote, MISPOpinion, MISPRelationship) + from .api import PyMISP, register_user # noqa + # NOTE: the direct imports to .tools are kept for backward compatibility but should be removed in the future from .tools import AbstractMISPObjectGenerator # noqa - from .tools import Neo4j # noqa - from .tools import stix # noqa from .tools import openioc # noqa from .tools import ext_lookups # noqa from .tools import update_objects # noqa - - from .api import PyMISP, register_user # noqa - from .api import PyMISP as ExpandedPyMISP # noqa from .tools import load_warninglists # noqa - # Let's not bother with old python + try: from .tools import reportlab_generator # noqa except ImportError: @@ -55,4 +58,26 @@ try: pass logger.debug('pymisp loaded properly') except ImportError as e: - logger.warning('Unable to load pymisp properly: {}'.format(e)) + logger.warning(f'Unable to load pymisp properly: {e}') + + +class ExpandedPyMISP(PyMISP): + + def __init__(self, *args, **kwargs): + warnings.warn('This class is deprecated, use PyMISP instead', FutureWarning) + super().__init__(*args, **kwargs) + + +__all__ = ['PyMISP', 'register_user', 'AbstractMISP', 'MISPTag', + 'MISPEvent', 'MISPAttribute', 'MISPObjectReference', 'MISPObjectAttribute', + 'MISPObject', 'MISPUser', 'MISPOrganisation', 'MISPSighting', 'MISPLog', + 'MISPShadowAttribute', 'MISPWarninglist', 'MISPTaxonomy', 'MISPNoticelist', + 'MISPObjectTemplate', 'MISPSharingGroup', 'MISPRole', 'MISPServer', 'MISPFeed', + 'MISPEventDelegation', 'MISPUserSetting', 'MISPInbox', 'MISPEventBlocklist', + 'MISPOrganisationBlocklist', 'MISPEventReport', 'MISPCorrelationExclusion', + 'MISPDecayingModel', 'MISPGalaxy', 'MISPGalaxyCluster', 'MISPGalaxyClusterElement', + 'MISPGalaxyClusterRelation', 'MISPNote', 'MISPOpinion', 'MISPRelationship', + 'PyMISPError', 'NewEventError', 'NewAttributeError', + 'NoURL', 'NoKey', 'InvalidMISPObject', 'UnknownMISPObjectTemplate', 'PyMISPInvalidFormat', + 'Distribution', 'ThreatLevel', 'Analysis', 'ExpandedPyMISP' + ] diff --git a/pymisp/abstract.py b/pymisp/abstract.py index 20dd2f6..c6d5a38 100644 --- a/pymisp/abstract.py +++ b/pymisp/abstract.py @@ -1,53 +1,48 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- +from __future__ import annotations + +import logging from datetime import date, datetime - from deprecated import deprecated # type: ignore from json import JSONEncoder from uuid import UUID from abc import ABCMeta - -try: - from rapidjson import load # type: ignore - from rapidjson import loads # type: ignore - from rapidjson import dumps # type: ignore - HAS_RAPIDJSON = True -except ImportError: - from json import load - from json import loads - from json import dumps - HAS_RAPIDJSON = False - -import logging from enum import Enum -from typing import Union, Optional, Any, Dict, List, Set, Mapping - -from .exceptions import PyMISPInvalidFormat, PyMISPError - - +from typing import Any, Mapping from collections.abc import MutableMapping from functools import lru_cache from pathlib import Path +try: + import orjson # type: ignore + from orjson import loads, dumps + HAS_ORJSON = True +except ImportError: + from json import loads, dumps + HAS_ORJSON = False + +from .exceptions import PyMISPInvalidFormat, PyMISPError + logger = logging.getLogger('pymisp') + resources_path = Path(__file__).parent / 'data' misp_objects_path = resources_path / 'misp-objects' / 'objects' -with (resources_path / 'describeTypes.json').open('r') as f: - describe_types = load(f)['result'] +with (resources_path / 'describeTypes.json').open('rb') as f: + describe_types: dict[str, Any] = loads(f.read())['result'] -class MISPFileCache(object): +class MISPFileCache: # cache up to 150 JSON structures in class attribute @staticmethod @lru_cache(maxsize=150) - def _load_json(path: Path) -> Union[dict, None]: + def _load_json(path: Path) -> dict[str, Any] | None: if not path.exists(): return None - with path.open('r', encoding='utf-8') as f: - data = load(f) + with path.open('rb') as f: + data = loads(f.read()) return data @@ -73,7 +68,7 @@ class Analysis(Enum): completed = 2 -def _int_to_str(d: Dict[str, Any]) -> Dict[str, Any]: +def _int_to_str(d: dict[str, Any]) -> dict[str, Any]: # transform all integer back to string for k, v in d.items(): if isinstance(v, dict): @@ -85,7 +80,7 @@ def _int_to_str(d: Dict[str, Any]) -> Dict[str, Any]: @deprecated(reason=" Use method default=pymisp_json_default instead of cls=MISPEncode", version='2.4.117', action='default') class MISPEncode(JSONEncoder): - def default(self, obj): + def default(self, obj: Any) -> dict[str, Any] | str: if isinstance(obj, AbstractMISP): return obj.jsonable() elif isinstance(obj, (datetime, date)): @@ -97,12 +92,12 @@ class MISPEncode(JSONEncoder): return JSONEncoder.default(self, obj) -class AbstractMISP(MutableMapping, MISPFileCache, metaclass=ABCMeta): +class AbstractMISP(MutableMapping, MISPFileCache, metaclass=ABCMeta): # type: ignore[type-arg] __resources_path = resources_path __misp_objects_path = misp_objects_path __describe_types = describe_types - def __init__(self, **kwargs: Dict): + def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] """Abstract class for all the MISP objects. NOTE: Every method in every classes inheriting this one are doing changes in memory and do not modify data on a remote MISP instance. @@ -111,9 +106,9 @@ class AbstractMISP(MutableMapping, MISPFileCache, metaclass=ABCMeta): """ super().__init__() self.__edited: bool = True # As we create a new object, we assume it is edited - self.__not_jsonable: List[str] = [] - self._fields_for_feed: Set - self.__self_defined_describe_types: Optional[Dict] = None + self.__not_jsonable: list[str] = [] + self._fields_for_feed: set[str] + self.__self_defined_describe_types: dict[str, Any] | None = None self.uuid: str if kwargs.get('force_timestamps') is not None: @@ -123,13 +118,13 @@ class AbstractMISP(MutableMapping, MISPFileCache, metaclass=ABCMeta): self.__force_timestamps = False @property - def describe_types(self) -> Dict: + def describe_types(self) -> dict[str, Any]: if self.__self_defined_describe_types: return self.__self_defined_describe_types return self.__describe_types @describe_types.setter - def describe_types(self, describe_types: Dict): + def describe_types(self, describe_types: dict[str, Any]) -> None: self.__self_defined_describe_types = describe_types @property @@ -141,12 +136,12 @@ class AbstractMISP(MutableMapping, MISPFileCache, metaclass=ABCMeta): return self.__misp_objects_path @misp_objects_path.setter - def misp_objects_path(self, misp_objects_path: Union[str, Path]): + def misp_objects_path(self, misp_objects_path: str | Path) -> None: if isinstance(misp_objects_path, str): misp_objects_path = Path(misp_objects_path) self.__misp_objects_path = misp_objects_path - def from_dict(self, **kwargs) -> None: + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] """Loading all the parameters as class properties, if they aren't `None`. This method aims to be called when all the properties requiring a special treatment are processed. @@ -159,15 +154,15 @@ class AbstractMISP(MutableMapping, MISPFileCache, metaclass=ABCMeta): # We load an existing dictionary, marking it an not-edited self.__edited = False - def update_not_jsonable(self, *args) -> None: + def update_not_jsonable(self, *args) -> None: # type: ignore[no-untyped-def] """Add entries to the __not_jsonable list""" self.__not_jsonable += args - def set_not_jsonable(self, args: List[str]) -> None: + def set_not_jsonable(self, args: list[str]) -> None: """Set __not_jsonable to a new list""" self.__not_jsonable = args - def _remove_from_not_jsonable(self, *args) -> None: + def _remove_from_not_jsonable(self, *args) -> None: # type: ignore[no-untyped-def] """Remove the entries that are in the __not_jsonable list""" for entry in args: try: @@ -179,7 +174,7 @@ class AbstractMISP(MutableMapping, MISPFileCache, metaclass=ABCMeta): """Load a JSON string""" self.from_dict(**loads(json_string)) - def to_dict(self, json_format: bool = False) -> Dict: + def to_dict(self, json_format: bool = False) -> dict[str, Any]: """Dump the class to a dictionary. This method automatically removes the timestamp recursively in every object that has been edited is order to let MISP update the event accordingly.""" @@ -221,15 +216,15 @@ class AbstractMISP(MutableMapping, MISPFileCache, metaclass=ABCMeta): to_return = _int_to_str(to_return) return to_return - def jsonable(self) -> Dict: + def jsonable(self) -> dict[str, Any]: """This method is used by the JSON encoder""" return self.to_dict() - def _to_feed(self) -> Dict: + def _to_feed(self) -> dict[str, Any]: if not hasattr(self, '_fields_for_feed') or not self._fields_for_feed: raise PyMISPError('Unable to export in the feed format, _fields_for_feed is missing.') - if hasattr(self, '_set_default') and callable(self._set_default): # type: ignore - self._set_default() # type: ignore + if hasattr(self, '_set_default') and callable(self._set_default): + self._set_default() to_return = {} for field in sorted(self._fields_for_feed): if getattr(self, field, None) is not None: @@ -243,15 +238,25 @@ class AbstractMISP(MutableMapping, MISPFileCache, metaclass=ABCMeta): if field in ['data', 'first_seen', 'last_seen', 'deleted']: # special fields continue - raise PyMISPError('The field {} is required in {} when generating a feed.'.format(field, self.__class__.__name__)) + raise PyMISPError(f'The field {field} is required in {self.__class__.__name__} when generating a feed.') to_return = _int_to_str(to_return) return to_return - def to_json(self, sort_keys: bool = False, indent: Optional[int] = None) -> str: + def to_json(self, sort_keys: bool = False, indent: int | None = None) -> str: """Dump recursively any class of type MISPAbstract to a json string""" + if HAS_ORJSON: + option = 0 + if sort_keys: + option |= orjson.OPT_SORT_KEYS + if indent: + option |= orjson.OPT_INDENT_2 + # orjson dumps method returns bytes instead of bytes, to keep compatibility with json + # we have to convert output to str + return dumps(self, default=pymisp_json_default, option=option).decode() + return dumps(self, default=pymisp_json_default, sort_keys=sort_keys, indent=indent) - def __getitem__(self, key): + def __getitem__(self, key: str) -> Any: try: if key[0] != '_' and key not in self.__not_jsonable: return self.__dict__[key] @@ -260,13 +265,13 @@ class AbstractMISP(MutableMapping, MISPFileCache, metaclass=ABCMeta): # Expected by pop and other dict-related methods raise KeyError - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: Any) -> None: setattr(self, key, value) - def __delitem__(self, key): + def __delitem__(self, key: str) -> None: delattr(self, key) - def __iter__(self): + def __iter__(self) -> Any: '''When we call **self, skip keys: * starting with _ * in __not_jsonable @@ -285,7 +290,7 @@ class AbstractMISP(MutableMapping, MISPFileCache, metaclass=ABCMeta): return self.__force_timestamps @force_timestamp.setter - def force_timestamp(self, force: bool): + def force_timestamp(self, force: bool) -> None: self.__force_timestamps = force @property @@ -305,28 +310,28 @@ class AbstractMISP(MutableMapping, MISPFileCache, metaclass=ABCMeta): return self.__edited @edited.setter - def edited(self, val: bool): + def edited(self, val: bool) -> None: """Set the edit flag""" if isinstance(val, bool): self.__edited = val else: raise PyMISPError('edited can only be True or False') - def __setattr__(self, name: str, value: Any): + def __setattr__(self, name: str, value: Any) -> None: if name[0] != '_' and not self.__edited and name in self: # The private members don't matter # If we already have a key with that name, we're modifying it. self.__edited = True super().__setattr__(name, value) - def _datetime_to_timestamp(self, d: Union[int, float, str, datetime]) -> int: + def _datetime_to_timestamp(self, d: int | float | str | datetime) -> int: """Convert a datetime object to a timestamp (int)""" if isinstance(d, (int, float, str)): # Assume we already have a timestamp return int(d) return int(d.timestamp()) - def _add_tag(self, tag: Optional[Union[str, 'MISPTag', Mapping]] = None, **kwargs): + def _add_tag(self, tag: str | MISPTag | Mapping[str, Any] | None = None, **kwargs): # type: ignore[no-untyped-def] """Add a tag to the attribute (by name or a MISPTag object)""" if isinstance(tag, str): misp_tag = MISPTag() @@ -346,14 +351,14 @@ class AbstractMISP(MutableMapping, MISPFileCache, metaclass=ABCMeta): self.edited = True return misp_tag - def _set_tags(self, tags: List['MISPTag']): + def _set_tags(self, tags: list[MISPTag]) -> None: """Set a list of prepared MISPTag.""" if all(isinstance(x, MISPTag) for x in tags): self.Tag = tags else: raise PyMISPInvalidFormat('All the attributes have to be of type MISPTag.') - def __eq__(self, other) -> bool: + def __eq__(self, other: object) -> bool: if isinstance(other, AbstractMISP): return self.to_dict() == other.to_dict() elif isinstance(other, dict): @@ -362,62 +367,57 @@ class AbstractMISP(MutableMapping, MISPFileCache, metaclass=ABCMeta): return False def __repr__(self) -> str: - return '<{self.__class__.__name__} - please define me>'.format(self=self) + return f'<{self.__class__.__name__} - please define me>' class MISPTag(AbstractMISP): - _fields_for_feed: set = {'name', 'colour'} + _fields_for_feed: set[str] = {'name', 'colour', 'relationship_type', 'local'} - def __init__(self, **kwargs: Dict): + def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(**kwargs) self.name: str self.exportable: bool self.local: bool + self.relationship_type: str | None - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if kwargs.get('Tag'): - kwargs = kwargs.get('Tag') + kwargs = kwargs.get('Tag') # type: ignore[assignment] super().from_dict(**kwargs) - def _set_default(self): + def _set_default(self) -> None: + if not hasattr(self, 'relationship_type'): + self.relationship_type = '' if not hasattr(self, 'colour'): self.colour = '#ffffff' + if not hasattr(self, 'local'): + self.local = False - def _to_feed(self, with_local: bool = True) -> Dict: + def _to_feed(self, with_local: bool = True) -> dict[str, Any]: if hasattr(self, 'exportable') and not self.exportable: return {} if with_local is False and hasattr(self, 'local') and self.local: return {} return super()._to_feed() - def delete(self): + def delete(self) -> None: self.deleted = True self.edited = True def __repr__(self) -> str: if hasattr(self, 'name'): return '<{self.__class__.__name__}(name={self.name})>'.format(self=self) - return '<{self.__class__.__name__}(NotInitialized)>'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)>' -if HAS_RAPIDJSON: - def pymisp_json_default(obj: Union[AbstractMISP, datetime, date, Enum, UUID]) -> Union[Dict, str]: - if isinstance(obj, AbstractMISP): - return obj.jsonable() - elif isinstance(obj, (datetime, date)): - return obj.isoformat() - elif isinstance(obj, Enum): - return obj.value - elif isinstance(obj, UUID): - return str(obj) -else: - def pymisp_json_default(obj: Union[AbstractMISP, datetime, date, Enum, UUID]) -> Union[Dict, str]: - if isinstance(obj, AbstractMISP): - return obj.jsonable() - elif isinstance(obj, (datetime, date)): - return obj.isoformat() - elif isinstance(obj, Enum): - return obj.value - elif isinstance(obj, UUID): - return str(obj) +# UUID, datetime, date and Enum is serialized by ORJSON by default +def pymisp_json_default(obj: AbstractMISP | datetime | date | Enum | UUID) -> dict[str, Any] | str: + if isinstance(obj, AbstractMISP): + return obj.jsonable() + elif isinstance(obj, (datetime, date)): + return obj.isoformat() + elif isinstance(obj, Enum): + return obj.value + elif isinstance(obj, UUID): + return str(obj) diff --git a/pymisp/api.py b/pymisp/api.py index cd2a74a..5d7d00a 100644 --- a/pymisp/api.py +++ b/pymisp/api.py @@ -1,13 +1,11 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- +from __future__ import annotations -from typing import TypeVar, Optional, Tuple, List, Dict, Union, Any, Mapping, Iterable, MutableMapping +from typing import TypeVar, Any, Mapping, Iterable, MutableMapping, Union, List, Dict from datetime import date, datetime import csv from pathlib import Path import logging from urllib.parse import urljoin -import json import requests from requests.auth import AuthBase import re @@ -15,8 +13,16 @@ from uuid import UUID import warnings import sys import copy -import urllib3 # type: ignore from io import BytesIO, StringIO +from importlib.metadata import version + +try: + # orjson is optional dependency that speedups parsing and encoding JSON + from orjson import loads, dumps # type: ignore + HAS_ORJSON = True +except ImportError: + from json import loads, dumps + HAS_ORJSON = False from . import __version__, everything_broken from .exceptions import MISPServerError, PyMISPUnexpectedResponse, PyMISPError, NoURL, NoKey @@ -25,7 +31,8 @@ from .mispevent import MISPEvent, MISPAttribute, MISPSighting, MISPLog, MISPObje MISPGalaxy, MISPNoticelist, MISPObjectReference, MISPObjectTemplate, MISPSharingGroup, \ MISPRole, MISPServer, MISPFeed, MISPEventDelegation, MISPCommunity, MISPUserSetting, \ MISPInbox, MISPEventBlocklist, MISPOrganisationBlocklist, MISPEventReport, \ - MISPGalaxyCluster, MISPGalaxyClusterRelation, MISPCorrelationExclusion, MISPDecayingModel + MISPGalaxyCluster, MISPGalaxyClusterRelation, MISPCorrelationExclusion, MISPDecayingModel, \ + MISPNote, MISPOpinion, MISPRelationship, AnalystDataBehaviorMixin from .abstract import pymisp_json_default, MISPTag, AbstractMISP, describe_types @@ -33,7 +40,7 @@ if sys.platform == 'linux': # Enable TCP keepalive by default on every requests import socket from urllib3.connection import HTTPConnection - HTTPConnection.default_socket_options = HTTPConnection.default_socket_options + [ + HTTPConnection.default_socket_options = HTTPConnection.default_socket_options + [ # type: ignore (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), # enable keepalive (socket.SOL_TCP, socket.TCP_KEEPIDLE, 30), # Start pinging after 30s of idle time (socket.SOL_TCP, socket.TCP_KEEPINTVL, 10), # ping every 10s @@ -42,15 +49,16 @@ if sys.platform == 'linux': try: # cached_property exists since Python 3.8 - from functools import cached_property # type: ignore + from functools import cached_property except ImportError: from functools import lru_cache def cached_property(func): # type: ignore - return property(lru_cache()(func)) + return property(lru_cache(func)) SearchType = TypeVar('SearchType', str, int) # str: string to search / list: values to search (OR) / dict: {'OR': [list], 'NOT': [list], 'AND': [list]} +# NOTE: we cannot use new typing here until we drop Python 3.8 and 3.9 support SearchParameterTypes = TypeVar('SearchParameterTypes', str, List[Union[str, int]], Dict[str, Union[str, int]]) ToIDSType = TypeVar('ToIDSType', str, int, bool) @@ -58,14 +66,14 @@ ToIDSType = TypeVar('ToIDSType', str, int, bool) logger = logging.getLogger('pymisp') -def get_uuid_or_id_from_abstract_misp(obj: Union[AbstractMISP, int, str, UUID, dict]) -> Union[str, int]: +def get_uuid_or_id_from_abstract_misp(obj: AbstractMISP | int | str | UUID | dict[str, Any]) -> str | int: """Extract the relevant ID accordingly to the given type passed as parameter""" if isinstance(obj, UUID): return str(obj) if isinstance(obj, (int, str)): return obj - if isinstance(obj, dict) and len(obj.keys()) == 1: + if isinstance(obj, dict) and len(obj) == 1: # We have an object in that format: {'Event': {'id': 2, ...}} # We need to get the content of that dictionary obj = obj[list(obj.keys())[0]] @@ -90,11 +98,11 @@ def get_uuid_or_id_from_abstract_misp(obj: Union[AbstractMISP, int, str, UUID, d def register_user(misp_url: str, email: str, - organisation: Optional[Union[MISPOrganisation, int, str, UUID]] = None, - org_id: Optional[str] = None, org_name: Optional[str] = None, - message: Optional[str] = None, custom_perms: Optional[str] = None, + organisation: MISPOrganisation | int | str | UUID | None = None, + org_id: str | None = None, org_name: str | None = None, + message: str | None = None, custom_perms: str | None = None, perm_sync: bool = False, perm_publish: bool = False, perm_admin: bool = False, - verify: bool = True) -> Dict: + verify: bool = True) -> dict[str, Any] | list[dict[str, Any]]: """Ask for the creation of an account for the user with the given email address""" data = copy.deepcopy(locals()) if organisation: @@ -119,13 +127,13 @@ def brotli_supported() -> bool: patch: int # urllib >= 1.25.1 includes brotli support - version_splitted = urllib3.__version__.split('.') # noqa: F811 + version_splitted = version('urllib3').split('.') # noqa: F811 if len(version_splitted) == 2: major, minor = version_splitted # type: ignore patch = 0 else: major, minor, patch = version_splitted # type: ignore - major, minor, patch = int(major), int(minor), int(patch) # type: ignore + major, minor, patch = int(major), int(minor), int(patch) urllib3_with_brotli = (major == 1 and ((minor == 25 and patch >= 1) or (minor >= 26))) or major >= 2 if not urllib3_with_brotli: @@ -151,13 +159,17 @@ class PyMISP: :param auth: The auth parameter is passed directly to requests, as described here: http://docs.python-requests.org/en/master/user/authentication/ :param tool: The software using PyMISP (string), used to set a unique user-agent :param http_headers: Arbitrary headers to pass to all the requests. + :param https_adapter: Arbitrary HTTPS adapter for the requests session. + :param http_auth_header_name: The name of the HTTP header to use for the API key. Can be either "Authorization" or "X-MISP-AUTH". :param timeout: Timeout, as described here: https://requests.readthedocs.io/en/master/user/advanced/#timeouts """ - def __init__(self, url: str, key: str, ssl: bool = True, debug: bool = False, proxies: Optional[MutableMapping[str, str]] = None, - cert: Optional[Union[str, Tuple[str, str]]] = None, auth: Optional[AuthBase] = None, tool: str = '', - timeout: Optional[Union[float, Tuple[float, float]]] = None, - http_headers: Optional[Dict[str, str]]=None + def __init__(self, url: str, key: str, ssl: bool | str = True, debug: bool = False, proxies: MutableMapping[str, str] | None = None, + cert: str | tuple[str, str] | None = None, auth: AuthBase | None = None, tool: str = '', + timeout: float | tuple[float, float] | None = None, + http_headers: dict[str, str] | None = None, + https_adapter: requests.adapters.BaseAdapter | None = None, + http_auth_header_name: str = 'Authorization' ): if not url: @@ -166,16 +178,28 @@ class PyMISP: raise NoKey('Please provide your authorization key.') self.root_url: str = url - self.key: str = key - self.ssl: bool = ssl - self.proxies: Optional[MutableMapping[str, str]] = proxies - self.cert: Optional[Union[str, Tuple[str, str]]] = cert - self.auth: Optional[AuthBase] = auth - self.tool: str = tool - self.timeout: Optional[Union[float, Tuple[float, float]]] = timeout + self.key: str = key.strip() + self.ssl: bool | str = ssl + self.proxies: MutableMapping[str, str] | None = proxies + self.cert: str | tuple[str, str] | None = cert + self.auth: AuthBase | None = auth + self.timeout: float | tuple[float, float] | None = timeout self.__session = requests.Session() # use one session to keep connection between requests + if https_adapter is not None: + self.__session.mount('https://', https_adapter) if brotli_supported(): self.__session.headers['Accept-Encoding'] = ', '.join(('br', 'gzip', 'deflate')) + + if http_auth_header_name in ['Authorization', 'X-MISP-AUTH']: + self.__session.headers[http_auth_header_name] = self.key + else: + raise PyMISPError('http_auth_header_name should be either "Authorization" or "X-MISP-AUTH"') + + user_agent = f'PyMISP {__version__} - Python {".".join(str(x) for x in sys.version_info[:2])}' + if tool: + user_agent = f'{user_agent} - {tool}' + self.__session.headers['User-Agent'] = user_agent + if http_headers: self.__session.headers.update(http_headers) @@ -206,8 +230,19 @@ class PyMISP: # Get the user information self._current_user: MISPUser self._current_role: MISPRole - self._current_user_settings: List[MISPUserSetting] - self._current_user, self._current_role, self._current_user_settings = self.get_user(pythonify=True, expanded=True) + self._current_user_settings: list[MISPUserSetting] + user_infos = self.get_user(pythonify=True, expanded=True) + if isinstance(user_infos, dict): + # There was an error during the get_user call + if e := user_infos.get('errors'): + raise PyMISPError(f'Unable to get the user settings: {e}') + raise PyMISPError(f'Unexpected error when initializing the connection: {user_infos}') + elif isinstance(user_infos, tuple) and len(user_infos) == 3: + self._current_user, self._current_role, self._current_user_settings = user_infos + else: + raise PyMISPError(f'Unexpected error when initializing the connection: {user_infos}') + except PyMISPError as e: + raise e except Exception as e: raise PyMISPError(f'Unable to connect to MISP ({self.root_url}). Please make sure the API key and the URL are correct (http/https is required): {e}') @@ -221,7 +256,7 @@ class PyMISP: self.category_type_mapping = self.describe_types['category_type_mappings'] self.sane_default = self.describe_types['sane_defaults'] - def remote_acl(self, debug_type: str = 'findMissingFunctionNames') -> Dict: + def remote_acl(self, debug_type: str = 'findMissingFunctionNames') -> dict[str, Any] | list[dict[str, Any]]: """This should return an empty list, unless the ACL is outdated. :param debug_type: printAllFunctionNames, findMissingFunctionNames, or printRoleAccess @@ -230,19 +265,19 @@ class PyMISP: return self._check_json_response(response) @property - def describe_types_local(self) -> Dict: + def describe_types_local(self) -> dict[str, Any] | list[dict[str, Any]]: '''Returns the content of describe types from the package''' return describe_types @property - def describe_types_remote(self) -> Dict: + def describe_types_remote(self) -> dict[str, Any] | list[dict[str, Any]]: '''Returns the content of describe types from the remote instance''' response = self._prepare_request('GET', 'attributes/describeTypes.json') remote_describe_types = self._check_json_response(response) return remote_describe_types['result'] @property - def recommended_pymisp_version(self) -> Dict: + def recommended_pymisp_version(self) -> dict[str, Any] | list[dict[str, Any]]: """Returns the recommended API version from the server""" # Sine MISP 2.4.146 is recommended PyMISP version included in getVersion call misp_version = self.misp_instance_version @@ -253,45 +288,45 @@ class PyMISP: return self._check_json_response(response) @property - def version(self) -> Dict: + def version(self) -> dict[str, Any] | list[dict[str, Any]]: """Returns the version of PyMISP you're currently using""" return {'version': __version__} @property - def pymisp_version_master(self) -> Dict: + def pymisp_version_master(self) -> dict[str, Any] | list[dict[str, Any]]: """PyMISP version as defined in the main repository""" return self.pymisp_version_main @property - def pymisp_version_main(self) -> Dict: + def pymisp_version_main(self) -> dict[str, Any] | list[dict[str, Any]]: """Get the most recent version of PyMISP from github""" - r = requests.get('https://raw.githubusercontent.com/MISP/PyMISP/main/pymisp/__init__.py') + r = requests.get('https://raw.githubusercontent.com/MISP/PyMISP/main/pyproject.toml') if r.status_code == 200: - version = re.findall("__version__ = '(.*)'", r.text) + version = re.findall('version = "(.*)"', r.text) return {'version': version[0]} return {'error': 'Impossible to retrieve the version of the main branch.'} @cached_property - def misp_instance_version(self) -> Dict: + def misp_instance_version(self) -> dict[str, Any] | list[dict[str, Any]]: """Returns the version of the instance.""" response = self._prepare_request('GET', 'servers/getVersion') return self._check_json_response(response) @property - def misp_instance_version_master(self) -> Dict: + def misp_instance_version_master(self) -> dict[str, Any] | list[dict[str, Any]]: """Get the most recent version from github""" r = requests.get('https://raw.githubusercontent.com/MISP/MISP/2.4/VERSION.json') if r.status_code == 200: - master_version = json.loads(r.text) + master_version = loads(r.content) return {'version': '{}.{}.{}'.format(master_version['major'], master_version['minor'], master_version['hotfix'])} return {'error': 'Impossible to retrieve the version of the master branch.'} - def update_misp(self) -> Dict: + def update_misp(self) -> dict[str, Any] | list[dict[str, Any]]: """Trigger a server update""" response = self._prepare_request('POST', 'servers/update') return self._check_json_response(response) - def set_server_setting(self, setting: str, value: Union[str, int, bool], force: bool = False) -> Dict: + def set_server_setting(self, setting: str, value: str | int | bool, force: bool = False) -> dict[str, Any] | list[dict[str, Any]]: """Set a setting on the MISP instance :param setting: server setting name @@ -302,7 +337,7 @@ class PyMISP: response = self._prepare_request('POST', f'servers/serverSettingsEdit/{setting}', data=data) return self._check_json_response(response) - def get_server_setting(self, setting: str) -> Dict: + def get_server_setting(self, setting: str) -> dict[str, Any] | list[dict[str, Any]]: """Get a setting from the MISP instance :param setting: server setting name @@ -310,17 +345,17 @@ class PyMISP: response = self._prepare_request('GET', f'servers/getSetting/{setting}') return self._check_json_response(response) - def server_settings(self) -> Dict: + def server_settings(self) -> dict[str, Any] | list[dict[str, Any]]: """Get all the settings from the server""" response = self._prepare_request('GET', 'servers/serverSettings') return self._check_json_response(response) - def restart_workers(self) -> Dict: + def restart_workers(self) -> dict[str, Any] | list[dict[str, Any]]: """Restart all the workers""" response = self._prepare_request('POST', 'servers/restartWorkers') return self._check_json_response(response) - def db_schema_diagnostic(self) -> Dict: + def db_schema_diagnostic(self) -> dict[str, Any] | list[dict[str, Any]]: """Get the schema diagnostic""" response = self._prepare_request('GET', 'servers/dbSchemaDiagnostic') return self._check_json_response(response) @@ -331,14 +366,14 @@ class PyMISP: # ## BEGIN Event ## - def events(self, pythonify: bool = False) -> Union[Dict, List[MISPEvent]]: + def events(self, pythonify: bool = False) -> dict[str, Any] | list[MISPEvent] | list[dict[str, Any]]: """Get all the events from the MISP instance: https://www.misp-project.org/openapi/#tag/Events/operation/getEvents :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM """ r = self._prepare_request('GET', 'events/index') events_r = self._check_json_response(r) - if not (self.global_pythonify or pythonify) or 'errors' in events_r: + if not (self.global_pythonify or pythonify) or isinstance(events_r, dict): return events_r to_return = [] for event in events_r: @@ -347,10 +382,10 @@ class PyMISP: to_return.append(e) return to_return - def get_event(self, event: Union[MISPEvent, int, str, UUID], - deleted: Union[bool, int, list] = False, - extended: Union[bool, int] = False, - pythonify: bool = False) -> Union[Dict, MISPEvent]: + def get_event(self, event: MISPEvent | int | str | UUID, + deleted: bool | int | list[int] = False, + extended: bool | int = False, + pythonify: bool = False) -> dict[str, Any] | MISPEvent: """Get an event from a MISP instance. Includes collections like Attribute, EventReport, Feed, Galaxy, Object, Tag, etc. so the response size may be large : https://www.misp-project.org/openapi/#tag/Events/operation/getEventById @@ -377,7 +412,7 @@ class PyMISP: e.load(event_r) return e - def event_exists(self, event: Union[MISPEvent, int, str, UUID]) -> bool: + def event_exists(self, event: MISPEvent | int | str | UUID) -> bool: """Fast check if event exists. :param event: Event to check @@ -386,7 +421,7 @@ class PyMISP: r = self._prepare_request('HEAD', f'events/view/{event_id}') return self._check_head_response(r) - def add_event(self, event: MISPEvent, pythonify: bool = False, metadata: bool = False) -> Union[Dict, MISPEvent]: + def add_event(self, event: MISPEvent, pythonify: bool = False, metadata: bool = False) -> dict[str, Any] | MISPEvent: """Add a new event on a MISP instance: https://www.misp-project.org/openapi/#tag/Events/operation/addEvent :param event: event to add @@ -401,8 +436,8 @@ class PyMISP: e.load(new_event) return e - def update_event(self, event: MISPEvent, event_id: Optional[int] = None, pythonify: bool = False, - metadata: bool = False) -> Union[Dict, MISPEvent]: + def update_event(self, event: MISPEvent, event_id: int | None = None, pythonify: bool = False, + metadata: bool = False) -> dict[str, Any] | MISPEvent: """Update an event on a MISP instance: https://www.misp-project.org/openapi/#tag/Events/operation/editEvent :param event: event to update @@ -422,7 +457,7 @@ class PyMISP: e.load(updated_event) return e - def delete_event(self, event: Union[MISPEvent, int, str, UUID]) -> Dict: + def delete_event(self, event: MISPEvent | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Delete an event from a MISP instance: https://www.misp-project.org/openapi/#tag/Events/operation/deleteEvent :param event: event to delete @@ -431,7 +466,7 @@ class PyMISP: response = self._prepare_request('POST', f'events/delete/{event_id}') return self._check_json_response(response) - def publish(self, event: Union[MISPEvent, int, str, UUID], alert: bool = False) -> Dict: + def publish(self, event: MISPEvent | int | str | UUID, alert: bool = False) -> dict[str, Any] | list[dict[str, Any]]: """Publish the event with one single HTTP POST: https://www.misp-project.org/openapi/#tag/Events/operation/publishEvent :param event: event to publish @@ -444,16 +479,16 @@ class PyMISP: response = self._prepare_request('POST', f'events/publish/{event_id}') return self._check_json_response(response) - def unpublish(self, event: Union[MISPEvent, int, str, UUID]) -> Dict: + def unpublish(self, event: MISPEvent | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Unpublish the event with one single HTTP POST: https://www.misp-project.org/openapi/#tag/Events/operation/unpublishEvent :param event: event to unpublish """ event_id = get_uuid_or_id_from_abstract_misp(event) - response = self._prepare_request('POST', f'events/publish/{event_id}') + response = self._prepare_request('POST', f'events/unpublish/{event_id}') return self._check_json_response(response) - def contact_event_reporter(self, event: Union[MISPEvent, int, str, UUID], message: str) -> Dict: + def contact_event_reporter(self, event: MISPEvent | int | str | UUID, message: str) -> dict[str, Any] | list[dict[str, Any]]: """Send a message to the reporter of an event :param event: event with reporter to contact @@ -468,8 +503,8 @@ class PyMISP: # ## BEGIN Event Report ### - def get_event_report(self, event_report: Union[MISPEventReport, int, str, UUID], - pythonify: bool = False) -> Union[Dict, MISPEventReport]: + def get_event_report(self, event_report: MISPEventReport | int | str | UUID, + pythonify: bool = False) -> dict[str, Any] | MISPEventReport: """Get an event report from a MISP instance :param event_report: event report to get @@ -484,8 +519,8 @@ class PyMISP: er.from_dict(**event_report_r) return er - def get_event_reports(self, event_id: Union[int, str], - pythonify: bool = False) -> Union[Dict, List[MISPEventReport]]: + def get_event_reports(self, event_id: int | str, + pythonify: bool = False) -> dict[str, Any] | list[MISPEventReport] | list[dict[str, Any]]: """Get event report from a MISP instance that are attached to an event ID :param event_id: event id to get the event reports for @@ -493,7 +528,7 @@ class PyMISP: """ r = self._prepare_request('GET', f'eventReports/index/event_id:{event_id}') event_reports = self._check_json_response(r) - if not (self.global_pythonify or pythonify) or 'errors' in event_reports: + if not (self.global_pythonify or pythonify) or isinstance(event_reports, dict): return event_reports to_return = [] for event_report in event_reports: @@ -502,7 +537,7 @@ class PyMISP: to_return.append(er) return to_return - def add_event_report(self, event: Union[MISPEvent, int, str, UUID], event_report: MISPEventReport, pythonify: bool = False) -> Union[Dict, MISPEventReport]: + def add_event_report(self, event: MISPEvent | int | str | UUID, event_report: MISPEventReport, pythonify: bool = False) -> dict[str, Any] | MISPEventReport: """Add an event report to an existing MISP event :param event: event to extend @@ -518,7 +553,7 @@ class PyMISP: er.from_dict(**new_event_report) return er - def update_event_report(self, event_report: MISPEventReport, event_report_id: Optional[int] = None, pythonify: bool = False) -> Union[Dict, MISPEventReport]: + def update_event_report(self, event_report: MISPEventReport, event_report_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPEventReport: """Update an event report on a MISP instance :param event_report: event report to update @@ -537,7 +572,7 @@ class PyMISP: er.from_dict(**updated_event_report) return er - def delete_event_report(self, event_report: Union[MISPEventReport, int, str, UUID], hard: bool = False) -> Dict: + def delete_event_report(self, event_report: MISPEventReport | int | str | UUID, hard: bool = False) -> dict[str, Any] | list[dict[str, Any]]: """Delete an event report from a MISP instance :param event_report: event report to delete @@ -550,12 +585,228 @@ class PyMISP: data['hard'] = 1 r = self._prepare_request('POST', request_url, data=data) return self._check_json_response(r) - # ## END Event Report ### + # ## BEGIN Galaxy Cluster ### + def attach_galaxy_cluster(self, misp_entity: MISPEvent | MISPAttribute, galaxy_cluster: MISPGalaxyCluster | int | str, local: bool = False, pythonify: bool = False) -> dict[str, Any] | list[dict[str, Any]]: + """Attach a galaxy cluster to an event or an attribute + + :param misp_entity: a MISP Event or a MISP Attribute + :param galaxy_cluster: Galaxy cluster to attach + :param local: whether the object should be attached locally or not to the target + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + if isinstance(misp_entity, MISPEvent): + attach_target_type = 'event' + elif isinstance(misp_entity, MISPAttribute): + attach_target_type = 'attribute' + else: + raise PyMISPError('The misp_entity must be MISPEvent or MISPAttribute') + + attach_target_id = misp_entity.id + local = 1 if local else 0 + + if isinstance(galaxy_cluster, MISPGalaxyCluster): + cluster_id = galaxy_cluster.id + elif isinstance(galaxy_cluster, (int, str)): + cluster_id = galaxy_cluster + else: + raise PyMISPError('The galaxy_cluster must be MISPGalaxyCluster or the id associated with the cluster (int or str)') + + to_post = {'Galaxy': {'target_id': cluster_id}} + url = f'galaxies/attachCluster/{attach_target_id}/{attach_target_type}/local:{local}' + + r = self._prepare_request('POST', url, data=to_post) + return self._check_json_response(r) + # ## END Galaxy Cluster ### + + # ## BEGIN Analyst Data ###a + def get_analyst_data(self, analyst_data: AnalystDataBehaviorMixin | int | str | UUID, + pythonify: bool = False) -> dict[str, Any] | MISPNote | MISPOpinion | MISPRelationship: + """Get an analyst data from a MISP instance + + :param analyst_data: analyst data to get + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + if isinstance(analyst_data, AnalystDataBehaviorMixin): + analyst_data_type = analyst_data.analyst_data_object_type + else: + analyst_data_type = 'all' + analyst_data_id = get_uuid_or_id_from_abstract_misp(analyst_data) + r = self._prepare_request('GET', f'analyst_data/view/{analyst_data_type}/{analyst_data_id}') + analyst_data_r = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in analyst_data_r or analyst_data_type == 'all': + return analyst_data_r + er = type(analyst_data)() + er.from_dict(**analyst_data_r) + return er + + def add_analyst_data(self, analyst_data: MISPNote | MISPOpinion | MISPRelationship, + pythonify: bool = False) -> dict[str, Any] | MISPNote | MISPOpinion | MISPRelationship: + """Add an analyst data to an existing MISP element + + :param analyst_data: analyst_data to add + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + object_uuid = analyst_data.object_uuid + object_type = analyst_data.object_type + r = self._prepare_request('POST', f'analyst_data/add/{analyst_data.analyst_data_object_type}/{object_uuid}/{object_type}', data=analyst_data) + new_analyst_data = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in new_analyst_data: + return new_analyst_data + er = type(analyst_data)() + er.from_dict(**new_analyst_data) + return er + + def update_analyst_data(self, analyst_data: MISPNote | MISPOpinion | MISPRelationship, analyst_data_id: int | None = None, + pythonify: bool = False) -> dict[str, Any] | MISPNote | MISPOpinion | MISPRelationship: + """Update an analyst data on a MISP instance + + :param analyst_data: analyst data to update + :param analyst_data_id: analyst data ID to update + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + if isinstance(analyst_data, AnalystDataBehaviorMixin): + analyst_data_type = analyst_data.analyst_data_object_type + else: + analyst_data_type = 'all' + if analyst_data_id is None: + analyst_data_id = get_uuid_or_id_from_abstract_misp(analyst_data) + r = self._prepare_request('POST', f'analyst_data/edit/{analyst_data_type}/{analyst_data_id}', data=analyst_data) + updated_analyst_data = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in updated_analyst_data or analyst_data_type == 'all': + return updated_analyst_data + er = type(analyst_data)() + er.from_dict(**updated_analyst_data) + return er + + def delete_analyst_data(self, analyst_data: MISPNote | MISPOpinion | MISPRelationship | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Delete an analyst data from a MISP instance + + :param analyst_data: analyst data to delete + """ + if isinstance(analyst_data, AnalystDataBehaviorMixin): + analyst_data_type = analyst_data.analyst_data_object_type + else: + analyst_data_type = 'all' + analyst_data_id = get_uuid_or_id_from_abstract_misp(analyst_data) + request_url = f'analyst_data/delete/{analyst_data_type}/{analyst_data_id}' + r = self._prepare_request('POST', request_url) + return self._check_json_response(r) + + # ## END Analyst Data ### + + # ## BEGIN Analyst Note ### + + def get_note(self, note: MISPNote, pythonify: bool = False) -> dict[str, Any] | MISPNote: + """Get a note from a MISP instance + + :param note: note to get + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + return self.get_analyst_data(note, pythonify) + + def add_note(self, note: MISPNote, pythonify: bool = False) -> dict[str, Any] | MISPNote: + """Add a note to an existing MISP element + + :param note: note to add + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + return self.add_analyst_data(note, pythonify) + + def update_note(self, note: MISPNote, note_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPNote: + """Update a note on a MISP instance + + :param note: note to update + :param note_id: note ID to update + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + return self.update_analyst_data(note, note_id, pythonify) + + def delete_note(self, note: MISPNote | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Delete a note from a MISP instance + + :param note: note delete + """ + return self.delete_analyst_data(note) + + # ## END Analyst Note ### + + # ## BEGIN Analyst Opinion ### + + def get_opinion(self, opinion: MISPOpinion, pythonify: bool = False) -> dict[str, Any] | MISPOpinion: + """Get an opinion from a MISP instance + + :param opinion: opinion to get + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + return self.get_analyst_data(opinion, pythonify) + + def add_opinion(self, opinion: MISPOpinion, pythonify: bool = False) -> dict[str, Any] | MISPOpinion: + """Add an opinion to an existing MISP element + + :param opinion: opinion to add + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + return self.add_analyst_data(opinion, pythonify) + + def update_opinion(self, opinion: MISPOpinion, opinion_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPOpinion: + """Update an opinion on a MISP instance + + :param opinion: opinion to update + :param opinion_id: opinion ID to update + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + return self.update_analyst_data(opinion, opinion_id, pythonify) + + def delete_opinion(self, opinion: MISPOpinion | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Delete an opinion from a MISP instance + + :param opinion: opinion to delete + """ + return self.delete_analyst_data(opinion) + + # ## END Analyst Opinion ### + + # ## BEGIN Analyst Relationship ### + + def get_relationship(self, relationship: MISPRelationship, pythonify: bool = False) -> dict[str, Any] | MISPRelationship: + """Get a relationship from a MISP instance + + :param relationship: relationship to get + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + return self.get_analyst_data(relationship, pythonify) + + def add_relationship(self, relationship: MISPRelationship, pythonify: bool = False) -> dict[str, Any] | MISPRelationship: + """Add a relationship to an existing MISP element + + :param relationship: relationship to add + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + return self.add_analyst_data(relationship, pythonify) + + def update_relationship(self, relationship: MISPRelationship, relationship_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPRelationship: + """Update a relationship on a MISP instance + + :param relationship: relationship to update + :param relationship_id: relationship ID to update + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + return self.update_analyst_data(relationship, relationship_id, pythonify) + + def delete_relationship(self, relationship: MISPRelationship | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Delete a relationship from a MISP instance + + :param relationship: relationship to delete + """ + return self.delete_analyst_data(relationship) + + # ## END Analyst Relationship ### + # ## BEGIN Object ### - def get_object(self, misp_object: Union[MISPObject, int, str, UUID], pythonify: bool = False) -> Union[Dict, MISPObject]: + def get_object(self, misp_object: MISPObject | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPObject: """Get an object from the remote MISP instance: https://www.misp-project.org/openapi/#tag/Objects/operation/getObjectById :param misp_object: object to get @@ -570,7 +821,7 @@ class PyMISP: o.from_dict(**misp_object_r) return o - def object_exists(self, misp_object: Union[MISPObject, int, str, UUID]) -> bool: + def object_exists(self, misp_object: MISPObject | int | str | UUID) -> bool: """Fast check if object exists. :param misp_object: Attribute to check @@ -579,7 +830,7 @@ class PyMISP: r = self._prepare_request('HEAD', f'objects/view/{object_id}') return self._check_head_response(r) - def add_object(self, event: Union[MISPEvent, int, str, UUID], misp_object: MISPObject, pythonify: bool = False, break_on_duplicate: bool = False) -> Union[Dict, MISPObject]: + def add_object(self, event: MISPEvent | int | str | UUID, misp_object: MISPObject, pythonify: bool = False, break_on_duplicate: bool = False) -> dict[str, Any] | MISPObject: """Add a MISP Object to an existing MISP event: https://www.misp-project.org/openapi/#tag/Objects/operation/addObject :param event: event to extend @@ -588,7 +839,7 @@ class PyMISP: :param break_on_duplicate: if True, check and reject if this object's attributes match an existing object's attributes; may require much time """ event_id = get_uuid_or_id_from_abstract_misp(event) - params = {'breakOnDuplicate': True} if break_on_duplicate else {} + params = {'breakOnDuplicate': 1} if break_on_duplicate else {} r = self._prepare_request('POST', f'objects/add/{event_id}', data=misp_object, kw_params=params) new_object = self._check_json_response(r) if not (self.global_pythonify or pythonify) or 'errors' in new_object: @@ -597,7 +848,7 @@ class PyMISP: o.from_dict(**new_object) return o - def update_object(self, misp_object: MISPObject, object_id: Optional[int] = None, pythonify: bool = False) -> Union[Dict, MISPObject]: + def update_object(self, misp_object: MISPObject, object_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPObject: """Update an object on a MISP instance :param misp_object: object to update @@ -616,7 +867,7 @@ class PyMISP: o.from_dict(**updated_object) return o - def delete_object(self, misp_object: Union[MISPObject, int, str, UUID], hard: bool = False) -> Dict: + def delete_object(self, misp_object: MISPObject | int | str | UUID, hard: bool = False) -> dict[str, Any] | list[dict[str, Any]]: """Delete an object from a MISP instance: https://www.misp-project.org/openapi/#tag/Objects/operation/deleteObject :param misp_object: object to delete @@ -629,7 +880,7 @@ class PyMISP: r = self._prepare_request('POST', f'objects/delete/{object_id}', data=data) return self._check_json_response(r) - def add_object_reference(self, misp_object_reference: MISPObjectReference, pythonify: bool = False) -> Union[Dict, MISPObjectReference]: + def add_object_reference(self, misp_object_reference: MISPObjectReference, pythonify: bool = False) -> dict[str, Any] | MISPObjectReference: """Add a reference to an object :param misp_object_reference: object reference @@ -643,25 +894,29 @@ class PyMISP: ref.from_dict(**object_reference) return ref - def delete_object_reference(self, object_reference: Union[MISPObjectReference, int, str, UUID]) -> Dict: - """Delete a reference to an object - - :param object_reference: object reference - """ + def delete_object_reference( + self, + object_reference: MISPObjectReference | int | str | UUID, + hard: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """Delete a reference to an object.""" object_reference_id = get_uuid_or_id_from_abstract_misp(object_reference) - response = self._prepare_request('POST', f'objectReferences/delete/{object_reference_id}') + query_url = f"objectReferences/delete/{object_reference_id}" + if hard: + query_url += "/true" + response = self._prepare_request("POST", query_url) return self._check_json_response(response) # Object templates - def object_templates(self, pythonify: bool = False) -> Union[Dict, List[MISPObjectTemplate]]: + def object_templates(self, pythonify: bool = False) -> dict[str, Any] | list[MISPObjectTemplate] | list[dict[str, Any]]: """Get all the object templates :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM """ r = self._prepare_request('GET', 'objectTemplates/index') templates = self._check_json_response(r) - if not (self.global_pythonify or pythonify) or 'errors' in templates: + if not (self.global_pythonify or pythonify) or isinstance(templates, dict): return templates to_return = [] for object_template in templates: @@ -670,7 +925,7 @@ class PyMISP: to_return.append(o) return to_return - def get_object_template(self, object_template: Union[MISPObjectTemplate, int, str, UUID], pythonify: bool = False) -> Union[Dict, MISPObjectTemplate]: + def get_object_template(self, object_template: MISPObjectTemplate | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPObjectTemplate: """Gets the full object template :param object_template: template or ID to get @@ -685,14 +940,14 @@ class PyMISP: t.from_dict(**object_template_r) return t - def get_raw_object_template(self, uuid_or_name: str) -> Dict: + def get_raw_object_template(self, uuid_or_name: str) -> dict[str, Any] | list[dict[str, Any]]: """Get a row template. It needs to be present on disk on the MISP instance you're connected to. The response of this method can be passed to MISPObject(, misp_objects_template_custom=) """ r = self._prepare_request('GET', f'objectTemplates/getRaw/{uuid_or_name}') return self._check_json_response(r) - def update_object_templates(self) -> Dict: + def update_object_templates(self) -> dict[str, Any] | list[dict[str, Any]]: """Trigger an update of the object templates""" response = self._prepare_request('POST', 'objectTemplates/update') return self._check_json_response(response) @@ -701,14 +956,14 @@ class PyMISP: # ## BEGIN Attribute ### - def attributes(self, pythonify: bool = False) -> Union[Dict, List[MISPAttribute]]: + def attributes(self, pythonify: bool = False) -> dict[str, Any] | list[MISPAttribute] | list[dict[str, Any]]: """Get all the attributes from the MISP instance: https://www.misp-project.org/openapi/#tag/Attributes/operation/getAttributes :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM """ r = self._prepare_request('GET', 'attributes/index') attributes_r = self._check_json_response(r) - if not (self.global_pythonify or pythonify) or 'errors' in attributes_r: + if not (self.global_pythonify or pythonify) or isinstance(attributes_r, dict): return attributes_r to_return = [] for attribute in attributes_r: @@ -717,7 +972,7 @@ class PyMISP: to_return.append(a) return to_return - def get_attribute(self, attribute: Union[MISPAttribute, int, str, UUID], pythonify: bool = False) -> Union[Dict, MISPAttribute]: + def get_attribute(self, attribute: MISPAttribute | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPAttribute: """Get an attribute from a MISP instance: https://www.misp-project.org/openapi/#tag/Attributes/operation/getAttributeById :param attribute: attribute to get @@ -732,7 +987,7 @@ class PyMISP: a.from_dict(**attribute_r) return a - def attribute_exists(self, attribute: Union[MISPAttribute, int, str, UUID]) -> bool: + def attribute_exists(self, attribute: MISPAttribute | int | str | UUID) -> bool: """Fast check if attribute exists. :param attribute: Attribute to check @@ -741,7 +996,7 @@ class PyMISP: r = self._prepare_request('HEAD', f'attributes/view/{attribute_id}') return self._check_head_response(r) - def add_attribute(self, event: Union[MISPEvent, int, str, UUID], attribute: Union[MISPAttribute, Iterable], pythonify: bool = False) -> Union[Dict, MISPAttribute, MISPShadowAttribute]: + def add_attribute(self, event: MISPEvent | int | str | UUID, attribute: MISPAttribute | Iterable[str], pythonify: bool = False, break_on_duplicate: bool = True) -> dict[str, Any] | MISPAttribute | MISPShadowAttribute: """Add an attribute to an existing MISP event: https://www.misp-project.org/openapi/#tag/Attributes/operation/addAttribute :param event: event to extend @@ -749,15 +1004,17 @@ class PyMISP: If a list is passed, the pythonified response is a dict with the following structure: {'attributes': [MISPAttribute], 'errors': {errors by attributes}} :param pythonify: Returns a PyMISP Object instead of the plain json output + :param break_on_duplicate: if False, do not fail if the attribute already exists, updates existing attribute instead (timestamp will be always updated) """ + params = {'breakOnDuplicate': 0} if break_on_duplicate is not True else {} event_id = get_uuid_or_id_from_abstract_misp(event) - r = self._prepare_request('POST', f'attributes/add/{event_id}', data=attribute) + r = self._prepare_request('POST', f'attributes/add/{event_id}', data=attribute, kw_params=params) new_attribute = self._check_json_response(r) if isinstance(attribute, list): # Multiple attributes were passed at once, the handling is totally different if not (self.global_pythonify or pythonify): return new_attribute - to_return: Dict[str, List[MISPAttribute]] = {'attributes': []} + to_return: dict[str, list[MISPAttribute]] = {'attributes': []} if 'errors' in new_attribute: to_return['errors'] = new_attribute['errors'] @@ -786,7 +1043,7 @@ class PyMISP: a.from_dict(**new_attribute) return a - def update_attribute(self, attribute: MISPAttribute, attribute_id: Optional[int] = None, pythonify: bool = False) -> Union[Dict, MISPAttribute, MISPShadowAttribute]: + def update_attribute(self, attribute: MISPAttribute, attribute_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPAttribute | MISPShadowAttribute: """Update an attribute on a MISP instance: https://www.misp-project.org/openapi/#tag/Attributes/operation/editAttribute :param attribute: attribute to update @@ -811,7 +1068,7 @@ class PyMISP: a.from_dict(**updated_attribute) return a - def delete_attribute(self, attribute: Union[MISPAttribute, int, str, UUID], hard: bool = False) -> Dict: + def delete_attribute(self, attribute: MISPAttribute | int | str | UUID, hard: bool = False) -> dict[str, Any] | list[dict[str, Any]]: """Delete an attribute from a MISP instance: https://www.misp-project.org/openapi/#tag/Attributes/operation/deleteAttribute :param attribute: attribute to delete @@ -831,7 +1088,7 @@ class PyMISP: return self.delete_attribute_proposal(attribute_id) return response - def restore_attribute(self, attribute: Union[MISPAttribute, int, str, UUID], pythonify: bool = False) -> Union[Dict, MISPAttribute]: + def restore_attribute(self, attribute: MISPAttribute | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPAttribute: """Restore a soft deleted attribute from a MISP instance: https://www.misp-project.org/openapi/#tag/Attributes/operation/restoreAttribute :param attribute: attribute to restore @@ -849,7 +1106,7 @@ class PyMISP: # ## BEGIN Attribute Proposal ### - def attribute_proposals(self, event: Optional[Union[MISPEvent, int, str, UUID]] = None, pythonify: bool = False) -> Union[Dict, List[MISPShadowAttribute]]: + def attribute_proposals(self, event: MISPEvent | int | str | UUID | None = None, pythonify: bool = False) -> dict[str, Any] | list[MISPShadowAttribute] | list[dict[str, Any]]: """Get all the attribute proposals :param event: event @@ -861,7 +1118,7 @@ class PyMISP: else: r = self._prepare_request('GET', 'shadowAttributes/index') attribute_proposals = self._check_json_response(r) - if not (self.global_pythonify or pythonify) or 'errors' in attribute_proposals: + if not (self.global_pythonify or pythonify) or isinstance(attribute_proposals, dict): return attribute_proposals to_return = [] for attribute_proposal in attribute_proposals: @@ -870,7 +1127,7 @@ class PyMISP: to_return.append(a) return to_return - def get_attribute_proposal(self, proposal: Union[MISPShadowAttribute, int, str, UUID], pythonify: bool = False) -> Union[Dict, MISPShadowAttribute]: + def get_attribute_proposal(self, proposal: MISPShadowAttribute | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPShadowAttribute: """Get an attribute proposal :param proposal: proposal to get @@ -887,7 +1144,7 @@ class PyMISP: # NOTE: the tree following method have a very specific meaning, look at the comments - def add_attribute_proposal(self, event: Union[MISPEvent, int, str, UUID], attribute: MISPAttribute, pythonify: bool = False) -> Union[Dict, MISPShadowAttribute]: + def add_attribute_proposal(self, event: MISPEvent | int | str | UUID, attribute: MISPAttribute, pythonify: bool = False) -> dict[str, Any] | MISPShadowAttribute: """Propose a new attribute in an event :param event: event to receive new attribute @@ -903,7 +1160,7 @@ class PyMISP: a.from_dict(**new_attribute_proposal) return a - def update_attribute_proposal(self, initial_attribute: Union[MISPAttribute, int, str, UUID], attribute: MISPAttribute, pythonify: bool = False) -> Union[Dict, MISPShadowAttribute]: + def update_attribute_proposal(self, initial_attribute: MISPAttribute | int | str | UUID, attribute: MISPAttribute, pythonify: bool = False) -> dict[str, Any] | MISPShadowAttribute: """Propose a change for an attribute :param initial_attribute: attribute to change @@ -919,7 +1176,7 @@ class PyMISP: a.from_dict(**update_attribute_proposal) return a - def delete_attribute_proposal(self, attribute: Union[MISPAttribute, int, str, UUID]) -> Dict: + def delete_attribute_proposal(self, attribute: MISPAttribute | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Propose the deletion of an attribute :param attribute: attribute to delete @@ -928,7 +1185,7 @@ class PyMISP: response = self._prepare_request('POST', f'shadowAttributes/delete/{attribute_id}') return self._check_json_response(response) - def accept_attribute_proposal(self, proposal: Union[MISPShadowAttribute, int, str, UUID]) -> Dict: + def accept_attribute_proposal(self, proposal: MISPShadowAttribute | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Accept a proposal. You cannot modify an existing proposal, only accept/discard :param proposal: attribute proposal to accept @@ -937,7 +1194,7 @@ class PyMISP: response = self._prepare_request('POST', f'shadowAttributes/accept/{proposal_id}') return self._check_json_response(response) - def discard_attribute_proposal(self, proposal: Union[MISPShadowAttribute, int, str, UUID]) -> Dict: + def discard_attribute_proposal(self, proposal: MISPShadowAttribute | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Discard a proposal. You cannot modify an existing proposal, only accept/discard :param proposal: attribute proposal to discard @@ -950,9 +1207,9 @@ class PyMISP: # ## BEGIN Sighting ### - def sightings(self, misp_entity: Optional[AbstractMISP] = None, - org: Optional[Union[MISPOrganisation, int, str, UUID]] = None, - pythonify: bool = False) -> Union[Dict, List[MISPSighting]]: + def sightings(self, misp_entity: AbstractMISP | None = None, + org: MISPOrganisation | int | str | UUID | None = None, + pythonify: bool = False) -> dict[str, Any] | list[MISPSighting] | list[dict[str, Any]]: """Get the list of sightings related to a MISPEvent or a MISPAttribute (depending on type of misp_entity): https://www.misp-project.org/openapi/#tag/Sightings/operation/getSightingsByEventId :param misp_entity: MISP entity @@ -975,7 +1232,7 @@ class PyMISP: r = self._prepare_request('POST', url, data=to_post) sightings = self._check_json_response(r) - if not (self.global_pythonify or pythonify) or 'errors' in sightings: + if not (self.global_pythonify or pythonify) or isinstance(sightings, dict): return sightings to_return = [] for sighting in sightings: @@ -984,9 +1241,9 @@ class PyMISP: to_return.append(s) return to_return - def add_sighting(self, sighting: MISPSighting, - attribute: Optional[Union[MISPAttribute, int, str, UUID]] = None, - pythonify: bool = False) -> Union[Dict, MISPSighting]: + def add_sighting(self, sighting: MISPSighting | dict[str, Any], + attribute: MISPAttribute | int | str | UUID | None = None, + pythonify: bool = False) -> dict[str, Any] | MISPSighting: """Add a new sighting (globally, or to a specific attribute): https://www.misp-project.org/openapi/#tag/Sightings/operation/addSighting and https://www.misp-project.org/openapi/#tag/Sightings/operation/getSightingsByEventId :param sighting: sighting to add @@ -1006,7 +1263,7 @@ class PyMISP: s.from_dict(**new_sighting) return s - def delete_sighting(self, sighting: Union[MISPSighting, int, str, UUID]) -> Dict: + def delete_sighting(self, sighting: MISPSighting | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Delete a sighting from a MISP instance: https://www.misp-project.org/openapi/#tag/Sightings/operation/deleteSighting :param sighting: sighting to delete @@ -1019,7 +1276,7 @@ class PyMISP: # ## BEGIN Tags ### - def tags(self, pythonify: bool = False, **kw_params) -> Union[Dict, List[MISPTag]]: + def tags(self, pythonify: bool = False, **kw_params) -> dict[str, Any] | list[MISPTag]: # type: ignore[no-untyped-def] """Get the list of existing tags: https://www.misp-project.org/openapi/#tag/Tags/operation/getTags :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM @@ -1035,7 +1292,7 @@ class PyMISP: to_return.append(t) return to_return - def get_tag(self, tag: Union[MISPTag, int, str, UUID], pythonify: bool = False) -> Union[Dict, MISPTag]: + def get_tag(self, tag: MISPTag | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPTag: """Get a tag by id: https://www.misp-project.org/openapi/#tag/Tags/operation/getTagById :param tag: tag to get @@ -1050,7 +1307,7 @@ class PyMISP: t.from_dict(**tag_r) return t - def add_tag(self, tag: MISPTag, pythonify: bool = False) -> Union[Dict, MISPTag]: + def add_tag(self, tag: MISPTag, pythonify: bool = False) -> dict[str, Any] | MISPTag: """Add a new tag on a MISP instance: https://www.misp-project.org/openapi/#tag/Tags/operation/addTag The user calling this method needs the Tag Editor permission. It doesn't add a tag to an event, simply creates it on the MISP instance. @@ -1066,7 +1323,7 @@ class PyMISP: t.from_dict(**new_tag) return t - def enable_tag(self, tag: MISPTag, pythonify: bool = False) -> Union[Dict, MISPTag]: + def enable_tag(self, tag: MISPTag, pythonify: bool = False) -> dict[str, Any] | MISPTag: """Enable a tag :param tag: tag to enable @@ -1075,7 +1332,7 @@ class PyMISP: tag.hide_tag = False return self.update_tag(tag, pythonify=pythonify) - def disable_tag(self, tag: MISPTag, pythonify: bool = False) -> Union[Dict, MISPTag]: + def disable_tag(self, tag: MISPTag, pythonify: bool = False) -> dict[str, Any] | MISPTag: """Disable a tag :param tag: tag to disable @@ -1084,7 +1341,7 @@ class PyMISP: tag.hide_tag = True return self.update_tag(tag, pythonify=pythonify) - def update_tag(self, tag: MISPTag, tag_id: Optional[int] = None, pythonify: bool = False) -> Union[Dict, MISPTag]: + def update_tag(self, tag: MISPTag, tag_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPTag: """Edit only the provided parameters of a tag: https://www.misp-project.org/openapi/#tag/Tags/operation/editTag :param tag: tag to update @@ -1103,7 +1360,7 @@ class PyMISP: t.from_dict(**updated_tag) return t - def delete_tag(self, tag: Union[MISPTag, int, str, UUID]) -> Dict: + def delete_tag(self, tag: MISPTag | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Delete a tag from a MISP instance: https://www.misp-project.org/openapi/#tag/Tags/operation/deleteTag :param tag: tag to delete @@ -1112,7 +1369,7 @@ class PyMISP: response = self._prepare_request('POST', f'tags/delete/{tag_id}') return self._check_json_response(response) - def search_tags(self, tagname: str, strict_tagname: bool = False, pythonify: bool = False) -> Union[Dict, List[MISPTag]]: + def search_tags(self, tagname: str, strict_tagname: bool = False, pythonify: bool = False) -> dict[str, Any] | list[MISPTag] | list[dict[str, Any]]: """Search for tags by name: https://www.misp-project.org/openapi/#tag/Tags/operation/searchTag :param tag_name: Name to search, use % for substrings matches. @@ -1121,9 +1378,9 @@ class PyMISP: query = {'tagname': tagname, 'strict_tagname': strict_tagname} response = self._prepare_request('POST', 'tags/search', data=query) normalized_response = self._check_json_response(response) - if not (self.global_pythonify or pythonify) or 'errors' in normalized_response: + if not (self.global_pythonify or pythonify) or isinstance(normalized_response, dict): return normalized_response - to_return: List[MISPTag] = [] + to_return: list[MISPTag] = [] for tag in normalized_response: t = MISPTag() t.from_dict(**tag) @@ -1134,14 +1391,14 @@ class PyMISP: # ## BEGIN Taxonomies ### - def taxonomies(self, pythonify: bool = False) -> Union[Dict, List[MISPTaxonomy]]: + def taxonomies(self, pythonify: bool = False) -> dict[str, Any] | list[MISPTaxonomy] | list[dict[str, Any]]: """Get all the taxonomies: https://www.misp-project.org/openapi/#tag/Taxonomies/operation/getTaxonomies :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM """ r = self._prepare_request('GET', 'taxonomies/index') taxonomies = self._check_json_response(r) - if not (self.global_pythonify or pythonify) or 'errors' in taxonomies: + if not (self.global_pythonify or pythonify) or isinstance(taxonomies, dict): return taxonomies to_return = [] for taxonomy in taxonomies: @@ -1150,7 +1407,7 @@ class PyMISP: to_return.append(t) return to_return - def get_taxonomy(self, taxonomy: Union[MISPTaxonomy, int, str, UUID], pythonify: bool = False) -> Union[Dict, MISPTaxonomy]: + def get_taxonomy(self, taxonomy: MISPTaxonomy | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPTaxonomy: """Get a taxonomy by id or namespace from a MISP instance: https://www.misp-project.org/openapi/#tag/Taxonomies/operation/getTaxonomyById :param taxonomy: taxonomy to get @@ -1165,7 +1422,7 @@ class PyMISP: t.from_dict(**taxonomy_r) return t - def enable_taxonomy(self, taxonomy: Union[MISPTaxonomy, int, str, UUID]) -> Dict: + def enable_taxonomy(self, taxonomy: MISPTaxonomy | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Enable a taxonomy: https://www.misp-project.org/openapi/#tag/Taxonomies/operation/enableTaxonomy :param taxonomy: taxonomy to enable @@ -1174,7 +1431,7 @@ class PyMISP: response = self._prepare_request('POST', f'taxonomies/enable/{taxonomy_id}') return self._check_json_response(response) - def disable_taxonomy(self, taxonomy: Union[MISPTaxonomy, int, str, UUID]) -> Dict: + def disable_taxonomy(self, taxonomy: MISPTaxonomy | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Disable a taxonomy: https://www.misp-project.org/openapi/#tag/Taxonomies/operation/disableTaxonomy :param taxonomy: taxonomy to disable @@ -1182,9 +1439,12 @@ class PyMISP: taxonomy_id = get_uuid_or_id_from_abstract_misp(taxonomy) self.disable_taxonomy_tags(taxonomy_id) response = self._prepare_request('POST', f'taxonomies/disable/{taxonomy_id}') - return self._check_json_response(response) + try: + return self._check_json_response(response) + except PyMISPError: + return self._check_json_response(response) - def disable_taxonomy_tags(self, taxonomy: Union[MISPTaxonomy, int, str, UUID]) -> Dict: + def disable_taxonomy_tags(self, taxonomy: MISPTaxonomy | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Disable all the tags of a taxonomy :param taxonomy: taxonomy with tags to disable @@ -1193,30 +1453,31 @@ class PyMISP: response = self._prepare_request('POST', f'taxonomies/disableTag/{taxonomy_id}') return self._check_json_response(response) - def enable_taxonomy_tags(self, taxonomy: Union[MISPTaxonomy, int, str, UUID]) -> Dict: + def enable_taxonomy_tags(self, taxonomy: MISPTaxonomy | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Enable all the tags of a taxonomy. NOTE: this is automatically done when you call enable_taxonomy :param taxonomy: taxonomy with tags to enable """ taxonomy_id = get_uuid_or_id_from_abstract_misp(taxonomy) t = self.get_taxonomy(taxonomy_id) - if isinstance(t, MISPTaxonomy) and not t.enabled: - # Can happen if global pythonify is enabled. - raise PyMISPError(f"The taxonomy {t.namespace} is not enabled.") + if isinstance(t, MISPTaxonomy): + if not t.enabled: + # Can happen if global pythonify is enabled. + raise PyMISPError(f"The taxonomy {t.namespace} is not enabled.") elif not t['Taxonomy']['enabled']: raise PyMISPError(f"The taxonomy {t['Taxonomy']['namespace']} is not enabled.") - url = urljoin(self.root_url, 'taxonomies/addTag/{}'.format(taxonomy_id)) + url = urljoin(self.root_url, f'taxonomies/addTag/{taxonomy_id}') response = self._prepare_request('POST', url) return self._check_json_response(response) - def update_taxonomies(self) -> Dict: + def update_taxonomies(self) -> dict[str, Any] | list[dict[str, Any]]: """Update all the taxonomies: https://www.misp-project.org/openapi/#tag/Taxonomies/operation/updateTaxonomies""" response = self._prepare_request('POST', 'taxonomies/update') return self._check_json_response(response) - def set_taxonomy_required(self, taxonomy: Union[MISPTaxonomy, int, str], required: bool = False) -> Dict: + def set_taxonomy_required(self, taxonomy: MISPTaxonomy | int | str, required: bool = False) -> dict[str, Any] | list[dict[str, Any]]: taxonomy_id = get_uuid_or_id_from_abstract_misp(taxonomy) - url = urljoin(self.root_url, 'taxonomies/toggleRequired/{}'.format(taxonomy_id)) + url = urljoin(self.root_url, f'taxonomies/toggleRequired/{taxonomy_id}') payload = { "Taxonomy": { "required": required @@ -1229,7 +1490,7 @@ class PyMISP: # ## BEGIN Warninglists ### - def warninglists(self, pythonify: bool = False) -> Union[Dict, List[MISPWarninglist]]: + def warninglists(self, pythonify: bool = False) -> dict[str, Any] | list[MISPWarninglist]: """Get all the warninglists: https://www.misp-project.org/openapi/#tag/Warninglists/operation/getWarninglists :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM @@ -1245,7 +1506,7 @@ class PyMISP: to_return.append(w) return to_return - def get_warninglist(self, warninglist: Union[MISPWarninglist, int, str, UUID], pythonify: bool = False) -> Union[Dict, MISPWarninglist]: + def get_warninglist(self, warninglist: MISPWarninglist | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPWarninglist: """Get a warninglist by id: https://www.misp-project.org/openapi/#tag/Warninglists/operation/getWarninglistById :param warninglist: warninglist to get @@ -1260,16 +1521,16 @@ class PyMISP: w.from_dict(**wl) return w - def toggle_warninglist(self, warninglist_id: Optional[Union[str, int, List[int]]] = None, warninglist_name: Optional[Union[str, List[str]]] = None, force_enable: bool = False) -> Dict: + def toggle_warninglist(self, warninglist_id: str | int | list[int] | None = None, warninglist_name: str | list[str] | None = None, force_enable: bool | None = None) -> dict[str, Any] | list[dict[str, Any]]: '''Toggle (enable/disable) the status of a warninglist by id: https://www.misp-project.org/openapi/#tag/Warninglists/operation/toggleEnableWarninglist :param warninglist_id: ID of the WarningList :param warninglist_name: name of the WarningList - :param force_enable: Force the warning list in the enabled state (does nothing if already enabled) + :param force_enable: Force the warning list in the enabled state (does nothing if already enabled) - None means toggle. ''' if warninglist_id is None and warninglist_name is None: raise PyMISPError('Either warninglist_id or warninglist_name is required.') - query: Dict[str, Union[List[str], List[int], bool]] = {} + query: dict[str, list[str] | list[int] | bool] = {} if warninglist_id is not None: if isinstance(warninglist_id, list): query['id'] = warninglist_id @@ -1280,12 +1541,12 @@ class PyMISP: query['name'] = warninglist_name else: query['name'] = [warninglist_name] - if force_enable: + if force_enable is not None: query['enabled'] = force_enable response = self._prepare_request('POST', 'warninglists/toggleEnable', data=query) return self._check_json_response(response) - def enable_warninglist(self, warninglist: Union[MISPWarninglist, int, str, UUID]) -> Dict: + def enable_warninglist(self, warninglist: MISPWarninglist | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Enable a warninglist :param warninglist: warninglist to enable @@ -1293,7 +1554,7 @@ class PyMISP: warninglist_id = get_uuid_or_id_from_abstract_misp(warninglist) return self.toggle_warninglist(warninglist_id=warninglist_id, force_enable=True) - def disable_warninglist(self, warninglist: Union[MISPWarninglist, int, str, UUID]) -> Dict: + def disable_warninglist(self, warninglist: MISPWarninglist | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Disable a warninglist :param warninglist: warninglist to disable @@ -1301,15 +1562,18 @@ class PyMISP: warninglist_id = get_uuid_or_id_from_abstract_misp(warninglist) return self.toggle_warninglist(warninglist_id=warninglist_id, force_enable=False) - def values_in_warninglist(self, value: Iterable) -> Dict: + def values_in_warninglist(self, value: Iterable[str]) -> dict[str, Any] | list[dict[str, Any]]: """Check if IOC values are in warninglist :param value: iterator with values to check """ response = self._prepare_request('POST', 'warninglists/checkValue', data=value) - return self._check_json_response(response) + try: + return self._check_json_response(response) + except PyMISPError: + return self._check_json_response(response) - def update_warninglists(self) -> Dict: + def update_warninglists(self) -> dict[str, Any] | list[dict[str, Any]]: """Update all the warninglists: https://www.misp-project.org/openapi/#tag/Warninglists/operation/updateWarninglists""" response = self._prepare_request('POST', 'warninglists/update') return self._check_json_response(response) @@ -1318,14 +1582,14 @@ class PyMISP: # ## BEGIN Noticelist ### - def noticelists(self, pythonify: bool = False) -> Union[Dict, List[MISPNoticelist]]: + def noticelists(self, pythonify: bool = False) -> dict[str, Any] | list[MISPNoticelist] | list[dict[str, Any]]: """Get all the noticelists: https://www.misp-project.org/openapi/#tag/Noticelists/operation/getNoticelists :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM """ r = self._prepare_request('GET', 'noticelists/index') noticelists = self._check_json_response(r) - if not (self.global_pythonify or pythonify) or 'errors' in noticelists: + if not (self.global_pythonify or pythonify) or isinstance(noticelists, dict): return noticelists to_return = [] for noticelist in noticelists: @@ -1334,7 +1598,7 @@ class PyMISP: to_return.append(n) return to_return - def get_noticelist(self, noticelist: Union[MISPNoticelist, int, str, UUID], pythonify: bool = False) -> Union[Dict, MISPNoticelist]: + def get_noticelist(self, noticelist: MISPNoticelist | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPNoticelist: """Get a noticelist by id: https://www.misp-project.org/openapi/#tag/Noticelists/operation/getNoticelistById :param notistlist: Noticelist to get @@ -1349,7 +1613,7 @@ class PyMISP: n.from_dict(**noticelist_j) return n - def enable_noticelist(self, noticelist: Union[MISPNoticelist, int, str, UUID]) -> Dict: + def enable_noticelist(self, noticelist: MISPNoticelist | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Enable a noticelist by id: https://www.misp-project.org/openapi/#tag/Noticelists/operation/toggleEnableNoticelist :param noticelist: Noticelist to enable @@ -1360,7 +1624,7 @@ class PyMISP: response = self._prepare_request('POST', f'noticelists/enableNoticelist/{noticelist_id}/true') return self._check_json_response(response) - def disable_noticelist(self, noticelist: Union[MISPNoticelist, int, str, UUID]) -> Dict: + def disable_noticelist(self, noticelist: MISPNoticelist | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Disable a noticelist by id :param noticelist: Noticelist to disable @@ -1371,7 +1635,7 @@ class PyMISP: response = self._prepare_request('POST', f'noticelists/enableNoticelist/{noticelist_id}') return self._check_json_response(response) - def update_noticelists(self) -> Dict: + def update_noticelists(self) -> dict[str, Any] | list[dict[str, Any]]: """Update all the noticelists: https://www.misp-project.org/openapi/#tag/Noticelists/operation/updateNoticelists""" response = self._prepare_request('POST', 'noticelists/update') return self._check_json_response(response) @@ -1380,14 +1644,14 @@ class PyMISP: # ## BEGIN Correlation Exclusions ### - def correlation_exclusions(self, pythonify: bool = False) -> Union[Dict, List[MISPCorrelationExclusion]]: + def correlation_exclusions(self, pythonify: bool = False) -> dict[str, Any] | list[MISPCorrelationExclusion] | list[dict[str, Any]]: """Get all the correlation exclusions :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM """ r = self._prepare_request('GET', 'correlation_exclusions') correlation_exclusions = self._check_json_response(r) - if not (self.global_pythonify or pythonify) or 'errors' in correlation_exclusions: + if not (self.global_pythonify or pythonify) or isinstance(correlation_exclusions, dict): return correlation_exclusions to_return = [] for correlation_exclusion in correlation_exclusions: @@ -1396,7 +1660,7 @@ class PyMISP: to_return.append(c) return to_return - def get_correlation_exclusion(self, correlation_exclusion: Union[MISPCorrelationExclusion, int, str, UUID], pythonify: bool = False) -> Union[Dict, MISPCorrelationExclusion]: + def get_correlation_exclusion(self, correlation_exclusion: MISPCorrelationExclusion | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPCorrelationExclusion: """Get a correlation exclusion by ID :param correlation_exclusion: Correlation exclusion to get @@ -1411,7 +1675,7 @@ class PyMISP: c.from_dict(**correlation_exclusion_j) return c - def add_correlation_exclusion(self, correlation_exclusion: MISPCorrelationExclusion, pythonify: bool = False) -> Union[Dict, MISPCorrelationExclusion]: + def add_correlation_exclusion(self, correlation_exclusion: MISPCorrelationExclusion, pythonify: bool = False) -> dict[str, Any] | MISPCorrelationExclusion: """Add a new correlation exclusion :param correlation_exclusion: correlation exclusion to add @@ -1425,7 +1689,7 @@ class PyMISP: c.from_dict(**new_correlation_exclusion) return c - def delete_correlation_exclusion(self, correlation_exclusion: Union[MISPCorrelationExclusion, int, str, UUID]) -> Dict: + def delete_correlation_exclusion(self, correlation_exclusion: MISPCorrelationExclusion | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Delete a correlation exclusion :param correlation_exclusion: The MISPCorrelationExclusion you wish to delete from MISP @@ -1434,7 +1698,7 @@ class PyMISP: r = self._prepare_request('POST', f'correlation_exclusions/delete/{exclusion_id}') return self._check_json_response(r) - def clean_correlation_exclusions(self): + def clean_correlation_exclusions(self) -> dict[str, Any] | list[dict[str, Any]]: """Initiate correlation exclusions cleanup""" r = self._prepare_request('POST', 'correlation_exclusions/clean') return self._check_json_response(r) @@ -1443,23 +1707,45 @@ class PyMISP: # ## BEGIN Galaxy ### - def galaxies(self, pythonify: bool = False) -> Union[Dict, List[MISPGalaxy]]: + def galaxies( + self, + withCluster: bool = False, + pythonify: bool = False, + ) -> dict[str, Any] | list[MISPGalaxy] | list[dict[str, Any]]: """Get all the galaxies: https://www.misp-project.org/openapi/#tag/Galaxies/operation/getGalaxies :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM """ r = self._prepare_request('GET', 'galaxies/index') galaxies = self._check_json_response(r) - if not (self.global_pythonify or pythonify) or 'errors' in galaxies: + if not (self.global_pythonify or pythonify) or isinstance(galaxies, dict): return galaxies to_return = [] for galaxy in galaxies: g = MISPGalaxy() - g.from_dict(**galaxy) + g.from_dict(**galaxy, withCluster=withCluster) to_return.append(g) return to_return - def get_galaxy(self, galaxy: Union[MISPGalaxy, int, str, UUID], withCluster: bool = False, pythonify: bool = False) -> Union[Dict, MISPGalaxy]: + def search_galaxy( + self, + value: str, + withCluster: bool = False, + pythonify: bool = False, + ) -> dict[str, Any] | list[MISPGalaxy] | list[dict[str, Any]]: + """Text search to find a matching galaxy name, namespace, description, or uuid.""" + r = self._prepare_request("POST", "galaxies", data={"value": value}) + galaxies = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(galaxies, dict): + return galaxies + to_return = [] + for galaxy in galaxies: + g = MISPGalaxy() + g.from_dict(**galaxy, withCluster=withCluster) + to_return.append(g) + return to_return + + def get_galaxy(self, galaxy: MISPGalaxy | int | str | UUID, withCluster: bool = False, pythonify: bool = False) -> dict[str, Any] | MISPGalaxy: """Get a galaxy by id: https://www.misp-project.org/openapi/#tag/Galaxies/operation/getGalaxyById :param galaxy: galaxy to get @@ -1475,7 +1761,7 @@ class PyMISP: g.from_dict(**galaxy_j, withCluster=withCluster) return g - def search_galaxy_clusters(self, galaxy: Union[MISPGalaxy, int, str, UUID], context: str = "all", searchall: Optional[str] = None, pythonify: bool = False) -> Union[Dict, List[MISPGalaxyCluster]]: + def search_galaxy_clusters(self, galaxy: MISPGalaxy | int | str | UUID, context: str = "all", searchall: str | None = None, pythonify: bool = False) -> dict[str, Any] | list[MISPGalaxyCluster] | list[dict[str, Any]]: """Searches the galaxy clusters within a specific galaxy: https://www.misp-project.org/openapi/#tag/Galaxy-Clusters/operation/getGalaxyClusters and https://www.misp-project.org/openapi/#tag/Galaxy-Clusters/operation/getGalaxyClusterById :param galaxy: The MISPGalaxy you wish to search in @@ -1485,15 +1771,15 @@ class PyMISP: """ galaxy_id = get_uuid_or_id_from_abstract_misp(galaxy) - allowed_context_types: List[str] = ["all", "default", "custom", "org", "deleted"] + allowed_context_types: list[str] = ["all", "default", "custom", "org", "orgc", "deleted"] if context not in allowed_context_types: raise PyMISPError(f"The context must be one of {', '.join(allowed_context_types)}") kw_params = {"context": context} if searchall: kw_params["searchall"] = searchall - r = self._prepare_request('GET', f"galaxy_clusters/index/{galaxy_id}", kw_params=kw_params) + r = self._prepare_request('POST', f"galaxy_clusters/index/{galaxy_id}", data=kw_params) clusters_j = self._check_json_response(r) - if not (self.global_pythonify or pythonify) or 'errors' in clusters_j: + if not (self.global_pythonify or pythonify) or isinstance(clusters_j, dict): return clusters_j response = [] for cluster in clusters_j: @@ -1502,12 +1788,12 @@ class PyMISP: response.append(c) return response - def update_galaxies(self) -> Dict: + def update_galaxies(self) -> dict[str, Any] | list[dict[str, Any]]: """Update all the galaxies: https://www.misp-project.org/openapi/#tag/Galaxies/operation/updateGalaxies""" response = self._prepare_request('POST', 'galaxies/update') return self._check_json_response(response) - def get_galaxy_cluster(self, galaxy_cluster: Union[MISPGalaxyCluster, int, str, UUID], pythonify: bool = False) -> Union[Dict, MISPGalaxyCluster]: + def get_galaxy_cluster(self, galaxy_cluster: MISPGalaxyCluster | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPGalaxyCluster: """Gets a specific galaxy cluster :param galaxy_cluster: The MISPGalaxyCluster you want to get @@ -1523,7 +1809,7 @@ class PyMISP: gc.from_dict(**cluster_j) return gc - def add_galaxy_cluster(self, galaxy: Union[MISPGalaxy, str, UUID], galaxy_cluster: MISPGalaxyCluster, pythonify: bool = False) -> Union[Dict, MISPGalaxyCluster]: + def add_galaxy_cluster(self, galaxy: MISPGalaxy | str | UUID, galaxy_cluster: MISPGalaxyCluster, pythonify: bool = False) -> dict[str, Any] | MISPGalaxyCluster: """Add a new galaxy cluster to a MISP Galaxy: https://www.misp-project.org/openapi/#tag/Galaxy-Clusters/operation/addGalaxyCluster :param galaxy: A MISPGalaxy (or UUID) where you wish to add the galaxy cluster @@ -1543,7 +1829,7 @@ class PyMISP: gc.from_dict(**cluster_j) return gc - def update_galaxy_cluster(self, galaxy_cluster: MISPGalaxyCluster, pythonify: bool = False) -> Union[Dict, MISPGalaxyCluster]: + def update_galaxy_cluster(self, galaxy_cluster: MISPGalaxyCluster, pythonify: bool = False) -> dict[str, Any] | MISPGalaxyCluster: """Update a custom galaxy cluster: https://www.misp-project.org/openapi/#tag/Galaxy-Clusters/operation/editGalaxyCluster ;param galaxy_cluster: The MISPGalaxyCluster you wish to update @@ -1562,7 +1848,7 @@ class PyMISP: gc.from_dict(**cluster_j) return gc - def publish_galaxy_cluster(self, galaxy_cluster: Union[MISPGalaxyCluster, int, str, UUID]) -> Dict: + def publish_galaxy_cluster(self, galaxy_cluster: MISPGalaxyCluster | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Publishes a galaxy cluster: https://www.misp-project.org/openapi/#tag/Galaxy-Clusters/operation/publishGalaxyCluster :param galaxy_cluster: The galaxy cluster you wish to publish @@ -1574,7 +1860,7 @@ class PyMISP: response = self._check_json_response(r) return response - def fork_galaxy_cluster(self, galaxy: Union[MISPGalaxy, int, str, UUID], galaxy_cluster: MISPGalaxyCluster, pythonify: bool = False) -> Union[Dict, MISPGalaxyCluster]: + def fork_galaxy_cluster(self, galaxy: MISPGalaxy | int | str | UUID, galaxy_cluster: MISPGalaxyCluster, pythonify: bool = False) -> dict[str, Any] | MISPGalaxyCluster: """Forks an existing galaxy cluster, creating a new one with matching attributes :param galaxy: The galaxy (or galaxy ID) where the cluster you want to fork resides @@ -1598,7 +1884,7 @@ class PyMISP: gc.from_dict(**cluster_j) return gc - def delete_galaxy_cluster(self, galaxy_cluster: Union[MISPGalaxyCluster, int, str, UUID], hard=False) -> Dict: + def delete_galaxy_cluster(self, galaxy_cluster: MISPGalaxyCluster | int | str | UUID, hard: bool=False) -> dict[str, Any] | list[dict[str, Any]]: """Deletes a galaxy cluster from MISP: https://www.misp-project.org/openapi/#tag/Galaxy-Clusters/operation/deleteGalaxyCluster :param galaxy_cluster: The MISPGalaxyCluster you wish to delete from MISP @@ -1614,7 +1900,7 @@ class PyMISP: r = self._prepare_request('POST', f'galaxy_clusters/delete/{cluster_id}', data=data) return self._check_json_response(r) - def add_galaxy_cluster_relation(self, galaxy_cluster_relation: MISPGalaxyClusterRelation) -> Dict: + def add_galaxy_cluster_relation(self, galaxy_cluster_relation: MISPGalaxyClusterRelation) -> dict[str, Any] | list[dict[str, Any]]: """Add a galaxy cluster relation, cluster relation must include cluster UUIDs in both directions @@ -1624,7 +1910,7 @@ class PyMISP: cluster_rel_j = self._check_json_response(r) return cluster_rel_j - def update_galaxy_cluster_relation(self, galaxy_cluster_relation: MISPGalaxyClusterRelation) -> Dict: + def update_galaxy_cluster_relation(self, galaxy_cluster_relation: MISPGalaxyClusterRelation) -> dict[str, Any] | list[dict[str, Any]]: """Update a galaxy cluster relation :param galaxy_cluster_relation: The MISPGalaxyClusterRelation to update @@ -1634,7 +1920,7 @@ class PyMISP: cluster_rel_j = self._check_json_response(r) return cluster_rel_j - def delete_galaxy_cluster_relation(self, galaxy_cluster_relation: Union[MISPGalaxyClusterRelation, int, str, UUID]) -> Dict: + def delete_galaxy_cluster_relation(self, galaxy_cluster_relation: MISPGalaxyClusterRelation | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Delete a galaxy cluster relation :param galaxy_cluster_relation: The MISPGalaxyClusterRelation to delete @@ -1648,14 +1934,14 @@ class PyMISP: # ## BEGIN Feed ### - def feeds(self, pythonify: bool = False) -> Union[Dict, List[MISPFeed]]: + def feeds(self, pythonify: bool = False) -> dict[str, Any] | list[MISPFeed] | list[dict[str, Any]]: """Get the list of existing feeds: https://www.misp-project.org/openapi/#tag/Feeds/operation/getFeeds :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM """ r = self._prepare_request('GET', 'feeds/index') feeds = self._check_json_response(r) - if not (self.global_pythonify or pythonify) or 'errors' in feeds: + if not (self.global_pythonify or pythonify) or isinstance(feeds, dict): return feeds to_return = [] for feed in feeds: @@ -1664,7 +1950,7 @@ class PyMISP: to_return.append(f) return to_return - def get_feed(self, feed: Union[MISPFeed, int, str, UUID], pythonify: bool = False) -> Union[Dict, MISPFeed]: + def get_feed(self, feed: MISPFeed | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPFeed: """Get a feed by id: https://www.misp-project.org/openapi/#tag/Feeds/operation/getFeedById :param feed: feed to get @@ -1679,7 +1965,7 @@ class PyMISP: f.from_dict(**feed_j) return f - def add_feed(self, feed: MISPFeed, pythonify: bool = False) -> Union[Dict, MISPFeed]: + def add_feed(self, feed: MISPFeed, pythonify: bool = False) -> dict[str, Any] | MISPFeed: """Add a new feed on a MISP instance: https://www.misp-project.org/openapi/#tag/Feeds/operation/addFeed :param feed: feed to add @@ -1694,7 +1980,7 @@ class PyMISP: f.from_dict(**new_feed) return f - def enable_feed(self, feed: Union[MISPFeed, int, str, UUID], pythonify: bool = False) -> Union[Dict, MISPFeed]: + def enable_feed(self, feed: MISPFeed | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPFeed: """Enable a feed; fetching it will create event(s): https://www.misp-project.org/openapi/#tag/Feeds/operation/enableFeed :param feed: feed to enable @@ -1709,7 +1995,7 @@ class PyMISP: f.enabled = True return self.update_feed(feed=f, pythonify=pythonify) - def disable_feed(self, feed: Union[MISPFeed, int, str, UUID], pythonify: bool = False) -> Union[Dict, MISPFeed]: + def disable_feed(self, feed: MISPFeed | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPFeed: """Disable a feed: https://www.misp-project.org/openapi/#tag/Feeds/operation/disableFeed :param feed: feed to disable @@ -1724,7 +2010,7 @@ class PyMISP: f.enabled = False return self.update_feed(feed=f, pythonify=pythonify) - def enable_feed_cache(self, feed: Union[MISPFeed, int, str, UUID], pythonify: bool = False) -> Union[Dict, MISPFeed]: + def enable_feed_cache(self, feed: MISPFeed | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPFeed: """Enable the caching of a feed :param feed: feed to enable caching @@ -1739,7 +2025,7 @@ class PyMISP: f.caching_enabled = True return self.update_feed(feed=f, pythonify=pythonify) - def disable_feed_cache(self, feed: Union[MISPFeed, int, str, UUID], pythonify: bool = False) -> Union[Dict, MISPFeed]: + def disable_feed_cache(self, feed: MISPFeed | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPFeed: """Disable the caching of a feed :param feed: feed to disable caching @@ -1754,7 +2040,7 @@ class PyMISP: f.caching_enabled = False return self.update_feed(feed=f, pythonify=pythonify) - def update_feed(self, feed: MISPFeed, feed_id: Optional[int] = None, pythonify: bool = False) -> Union[Dict, MISPFeed]: + def update_feed(self, feed: MISPFeed, feed_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPFeed: """Update a feed on a MISP instance :param feed: feed to update @@ -1774,7 +2060,7 @@ class PyMISP: f.from_dict(**updated_feed) return f - def delete_feed(self, feed: Union[MISPFeed, int, str, UUID]) -> Dict: + def delete_feed(self, feed: MISPFeed | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Delete a feed from a MISP instance :param feed: feed to delete @@ -1783,7 +2069,7 @@ class PyMISP: response = self._prepare_request('POST', f'feeds/delete/{feed_id}') return self._check_json_response(response) - def fetch_feed(self, feed: Union[MISPFeed, int, str, UUID]) -> Dict: + def fetch_feed(self, feed: MISPFeed | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Fetch one single feed by id: https://www.misp-project.org/openapi/#tag/Feeds/operation/fetchFromFeed :param feed: feed to fetch @@ -1792,12 +2078,12 @@ class PyMISP: response = self._prepare_request('GET', f'feeds/fetchFromFeed/{feed_id}') return self._check_json_response(response) - def cache_all_feeds(self) -> Dict: + def cache_all_feeds(self) -> dict[str, Any] | list[dict[str, Any]]: """ Cache all the feeds: https://www.misp-project.org/openapi/#tag/Feeds/operation/cacheFeeds""" response = self._prepare_request('GET', 'feeds/cacheFeeds/all') return self._check_json_response(response) - def cache_feed(self, feed: Union[MISPFeed, int, str, UUID]) -> Dict: + def cache_feed(self, feed: MISPFeed | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Cache a specific feed by id: https://www.misp-project.org/openapi/#tag/Feeds/operation/cacheFeeds :param feed: feed to cache @@ -1806,22 +2092,22 @@ class PyMISP: response = self._prepare_request('GET', f'feeds/cacheFeeds/{feed_id}') return self._check_json_response(response) - def cache_freetext_feeds(self) -> Dict: + def cache_freetext_feeds(self) -> dict[str, Any] | list[dict[str, Any]]: """Cache all the freetext feeds""" response = self._prepare_request('GET', 'feeds/cacheFeeds/freetext') return self._check_json_response(response) - def cache_misp_feeds(self) -> Dict: + def cache_misp_feeds(self) -> dict[str, Any] | list[dict[str, Any]]: """Cache all the MISP feeds""" response = self._prepare_request('GET', 'feeds/cacheFeeds/misp') return self._check_json_response(response) - def compare_feeds(self) -> Dict: + def compare_feeds(self) -> dict[str, Any] | list[dict[str, Any]]: """Generate the comparison matrix for all the MISP feeds""" response = self._prepare_request('GET', 'feeds/compareFeeds') return self._check_json_response(response) - def load_default_feeds(self) -> Dict: + def load_default_feeds(self) -> dict[str, Any] | list[dict[str, Any]]: """Load all the default feeds.""" response = self._prepare_request('POST', 'feeds/loadDefaultFeeds') return self._check_json_response(response) @@ -1830,14 +2116,14 @@ class PyMISP: # ## BEGIN Server ### - def servers(self, pythonify: bool = False) -> Union[Dict, List[MISPServer]]: + def servers(self, pythonify: bool = False) -> dict[str, Any] | list[MISPServer] | list[dict[str, Any]]: """Get the existing servers the MISP instance can synchronise with: https://www.misp-project.org/openapi/#tag/Servers/operation/getServers :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM """ r = self._prepare_request('GET', 'servers/index') servers = self._check_json_response(r) - if not (self.global_pythonify or pythonify) or 'errors' in servers: + if not (self.global_pythonify or pythonify) or isinstance(servers, dict): return servers to_return = [] for server in servers: @@ -1846,7 +2132,7 @@ class PyMISP: to_return.append(s) return to_return - def get_sync_config(self, pythonify: bool = False) -> Union[Dict, MISPServer]: + def get_sync_config(self, pythonify: bool = False) -> dict[str, Any] | MISPServer: """Get the sync server config. WARNING: This method only works if the user calling it is a sync user @@ -1860,7 +2146,7 @@ class PyMISP: s.from_dict(**server) return s - def import_server(self, server: MISPServer, pythonify: bool = False) -> Union[Dict, MISPServer]: + def import_server(self, server: MISPServer, pythonify: bool = False) -> dict[str, Any] | MISPServer: """Import a sync server config received from get_sync_config :param server: sync server config @@ -1874,7 +2160,7 @@ class PyMISP: s.from_dict(**server_j) return s - def add_server(self, server: MISPServer, pythonify: bool = False) -> Union[Dict, MISPServer]: + def add_server(self, server: MISPServer, pythonify: bool = False) -> dict[str, Any] | MISPServer: """Add a server to synchronise with: https://www.misp-project.org/openapi/#tag/Servers/operation/getServers Note: You probably want to use PyMISP.get_sync_config and PyMISP.import_server instead @@ -1889,7 +2175,7 @@ class PyMISP: s.from_dict(**server_j) return s - def update_server(self, server: MISPServer, server_id: Optional[int] = None, pythonify: bool = False) -> Union[Dict, MISPServer]: + def update_server(self, server: MISPServer, server_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPServer: """Update a server to synchronise with: https://www.misp-project.org/openapi/#tag/Servers/operation/getServers :param server: sync server config @@ -1907,7 +2193,7 @@ class PyMISP: s.from_dict(**updated_server) return s - def delete_server(self, server: Union[MISPServer, int, str, UUID]) -> Dict: + def delete_server(self, server: MISPServer | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Delete a sync server: https://www.misp-project.org/openapi/#tag/Servers/operation/getServers :param server: sync server config @@ -1916,7 +2202,7 @@ class PyMISP: response = self._prepare_request('POST', f'servers/delete/{server_id}') return self._check_json_response(response) - def server_pull(self, server: Union[MISPServer, int, str, UUID], event: Optional[Union[MISPEvent, int, str, UUID]] = None) -> Dict: + def server_pull(self, server: MISPServer | int | str | UUID, event: MISPEvent | int | str | UUID | None = None) -> dict[str, Any] | list[dict[str, Any]]: """Initialize a pull from a sync server, optionally limited to one event: https://www.misp-project.org/openapi/#tag/Servers/operation/pullServer :param server: sync server config @@ -1932,7 +2218,7 @@ class PyMISP: # FIXME: can we pythonify? return self._check_json_response(response) - def server_push(self, server: Union[MISPServer, int, str, UUID], event: Optional[Union[MISPEvent, int, str, UUID]] = None) -> Dict: + def server_push(self, server: MISPServer | int | str | UUID, event: MISPEvent | int | str | UUID | None = None) -> dict[str, Any] | list[dict[str, Any]]: """Initialize a push to a sync server, optionally limited to one event: https://www.misp-project.org/openapi/#tag/Servers/operation/pushServer :param server: sync server config @@ -1948,7 +2234,7 @@ class PyMISP: # FIXME: can we pythonify? return self._check_json_response(response) - def test_server(self, server: Union[MISPServer, int, str, UUID]) -> Dict: + def test_server(self, server: MISPServer | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Test if a sync link is working as expected :param server: sync server config @@ -1961,14 +2247,14 @@ class PyMISP: # ## BEGIN Sharing group ### - def sharing_groups(self, pythonify: bool = False) -> Union[Dict, List[MISPSharingGroup]]: + def sharing_groups(self, pythonify: bool = False) -> dict[str, Any] | list[MISPSharingGroup] | list[dict[str, Any]]: """Get the existing sharing groups: https://www.misp-project.org/openapi/#tag/Sharing-Groups/operation/getSharingGroup :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM """ r = self._prepare_request('GET', 'sharingGroups/index') sharing_groups = self._check_json_response(r) - if not (self.global_pythonify or pythonify) or 'errors' in sharing_groups: + if not (self.global_pythonify or pythonify) or isinstance(sharing_groups, dict): return sharing_groups to_return = [] for sharing_group in sharing_groups: @@ -1977,7 +2263,7 @@ class PyMISP: to_return.append(s) return to_return - def get_sharing_group(self, sharing_group: Union[MISPSharingGroup, int, str, UUID], pythonify: bool = False) -> Union[Dict, MISPSharingGroup]: + def get_sharing_group(self, sharing_group: MISPSharingGroup | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPSharingGroup: """Get a sharing group: https://www.misp-project.org/openapi/#tag/Sharing-Groups/operation/getSharingGroupById :param sharing_group: sharing group to find @@ -1992,7 +2278,7 @@ class PyMISP: s.from_dict(**sharing_group_resp) return s - def add_sharing_group(self, sharing_group: MISPSharingGroup, pythonify: bool = False) -> Union[Dict, MISPSharingGroup]: + def add_sharing_group(self, sharing_group: MISPSharingGroup, pythonify: bool = False) -> dict[str, Any] | MISPSharingGroup: """Add a new sharing group: https://www.misp-project.org/openapi/#tag/Sharing-Groups/operation/addSharingGroup :param sharing_group: sharing group to add @@ -2006,7 +2292,7 @@ class PyMISP: s.from_dict(**sharing_group_j) return s - def update_sharing_group(self, sharing_group: Union[MISPSharingGroup, dict], sharing_group_id: Optional[int] = None, pythonify: bool = False) -> Union[Dict, MISPSharingGroup]: + def update_sharing_group(self, sharing_group: MISPSharingGroup | dict[str, Any], sharing_group_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPSharingGroup: """Update sharing group parameters: https://www.misp-project.org/openapi/#tag/Sharing-Groups/operation/editSharingGroup :param sharing_group: MISP Sharing Group @@ -2017,6 +2303,7 @@ class PyMISP: sid = get_uuid_or_id_from_abstract_misp(sharing_group) else: sid = get_uuid_or_id_from_abstract_misp(sharing_group_id) + sharing_group.pop('modified', None) # Quick fix for https://github.com/MISP/PyMISP/issues/1049 - remove when fixed in MISP. r = self._prepare_request('POST', f'sharing_groups/edit/{sid}', data=sharing_group) updated_sharing_group = self._check_json_response(r) if not (self.global_pythonify or pythonify) or 'errors' in updated_sharing_group: @@ -2025,7 +2312,7 @@ class PyMISP: s.from_dict(**updated_sharing_group) return s - def sharing_group_exists(self, sharing_group: Union[MISPSharingGroup, int, str, UUID]) -> bool: + def sharing_group_exists(self, sharing_group: MISPSharingGroup | int | str | UUID) -> bool: """Fast check if sharing group exists. :param sharing_group: Sharing group to check @@ -2034,7 +2321,7 @@ class PyMISP: r = self._prepare_request('HEAD', f'sharing_groups/view/{sharing_group_id}') return self._check_head_response(r) - def delete_sharing_group(self, sharing_group: Union[MISPSharingGroup, int, str, UUID]) -> Dict: + def delete_sharing_group(self, sharing_group: MISPSharingGroup | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Delete a sharing group: https://www.misp-project.org/openapi/#tag/Sharing-Groups/operation/deleteSharingGroup :param sharing_group: sharing group to delete @@ -2043,8 +2330,8 @@ class PyMISP: response = self._prepare_request('POST', f'sharingGroups/delete/{sharing_group_id}') return self._check_json_response(response) - def add_org_to_sharing_group(self, sharing_group: Union[MISPSharingGroup, int, str, UUID], - organisation: Union[MISPOrganisation, int, str, UUID], extend: bool = False) -> Dict: + def add_org_to_sharing_group(self, sharing_group: MISPSharingGroup | int | str | UUID, + organisation: MISPOrganisation | int | str | UUID, extend: bool = False) -> dict[str, Any] | list[dict[str, Any]]: '''Add an organisation to a sharing group: https://www.misp-project.org/openapi/#tag/Sharing-Groups/operation/addOrganisationToSharingGroup :param sharing_group: Sharing group's local instance ID, or Sharing group's global UUID @@ -2057,8 +2344,8 @@ class PyMISP: response = self._prepare_request('POST', 'sharingGroups/addOrg', data=to_jsonify) return self._check_json_response(response) - def remove_org_from_sharing_group(self, sharing_group: Union[MISPSharingGroup, int, str, UUID], - organisation: Union[MISPOrganisation, int, str, UUID]) -> Dict: + def remove_org_from_sharing_group(self, sharing_group: MISPSharingGroup | int | str | UUID, + organisation: MISPOrganisation | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: '''Remove an organisation from a sharing group: https://www.misp-project.org/openapi/#tag/Sharing-Groups/operation/removeOrganisationFromSharingGroup :param sharing_group: Sharing group's local instance ID, or Sharing group's global UUID @@ -2070,8 +2357,8 @@ class PyMISP: response = self._prepare_request('POST', 'sharingGroups/removeOrg', data=to_jsonify) return self._check_json_response(response) - def add_server_to_sharing_group(self, sharing_group: Union[MISPSharingGroup, int, str, UUID], - server: Union[MISPServer, int, str, UUID], all_orgs: bool = False) -> Dict: + def add_server_to_sharing_group(self, sharing_group: MISPSharingGroup | int | str | UUID, + server: MISPServer | int | str | UUID, all_orgs: bool = False) -> dict[str, Any] | list[dict[str, Any]]: '''Add a server to a sharing group: https://www.misp-project.org/openapi/#tag/Sharing-Groups/operation/addServerToSharingGroup :param sharing_group: Sharing group's local instance ID, or Sharing group's global UUID @@ -2084,8 +2371,8 @@ class PyMISP: response = self._prepare_request('POST', 'sharingGroups/addServer', data=to_jsonify) return self._check_json_response(response) - def remove_server_from_sharing_group(self, sharing_group: Union[MISPSharingGroup, int, str, UUID], - server: Union[MISPServer, int, str, UUID]) -> Dict: + def remove_server_from_sharing_group(self, sharing_group: MISPSharingGroup | int | str | UUID, + server: MISPServer | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: '''Remove a server from a sharing group: https://www.misp-project.org/openapi/#tag/Sharing-Groups/operation/removeServerFromSharingGroup :param sharing_group: Sharing group's local instance ID, or Sharing group's global UUID @@ -2101,7 +2388,7 @@ class PyMISP: # ## BEGIN Organisation ### - def organisations(self, scope="local", search: Optional[str] = None, pythonify: bool = False) -> Union[Dict, List[MISPOrganisation]]: + def organisations(self, scope: str="local", search: str | None = None, pythonify: bool = False) -> dict[str, Any] | list[MISPOrganisation] | list[dict[str, Any]]: """Get all the organisations: https://www.misp-project.org/openapi/#tag/Organisations/operation/getOrganisations :param scope: scope of organizations to get @@ -2114,7 +2401,7 @@ class PyMISP: r = self._prepare_request('GET', url_path) organisations = self._check_json_response(r) - if not (self.global_pythonify or pythonify) or 'errors' in organisations: + if not (self.global_pythonify or pythonify) or isinstance(organisations, dict): return organisations to_return = [] for organisation in organisations: @@ -2123,7 +2410,7 @@ class PyMISP: to_return.append(o) return to_return - def get_organisation(self, organisation: Union[MISPOrganisation, int, str, UUID], pythonify: bool = False) -> Union[Dict, MISPOrganisation]: + def get_organisation(self, organisation: MISPOrganisation | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPOrganisation: """Get an organisation by id: https://www.misp-project.org/openapi/#tag/Organisations/operation/getOrganisationById :param organisation: organization to get @@ -2138,7 +2425,7 @@ class PyMISP: o.from_dict(**organisation_j) return o - def organisation_exists(self, organisation: Union[MISPOrganisation, int, str, UUID]) -> bool: + def organisation_exists(self, organisation: MISPOrganisation | int | str | UUID) -> bool: """Fast check if organisation exists. :param organisation: Organisation to check @@ -2147,7 +2434,7 @@ class PyMISP: r = self._prepare_request('HEAD', f'organisations/view/{organisation_id}') return self._check_head_response(r) - def add_organisation(self, organisation: MISPOrganisation, pythonify: bool = False) -> Union[Dict, MISPOrganisation]: + def add_organisation(self, organisation: MISPOrganisation, pythonify: bool = False) -> dict[str, Any] | MISPOrganisation: """Add an organisation: https://www.misp-project.org/openapi/#tag/Organisations/operation/addOrganisation :param organisation: organization to add @@ -2161,7 +2448,7 @@ class PyMISP: o.from_dict(**new_organisation) return o - def update_organisation(self, organisation: MISPOrganisation, organisation_id: Optional[int] = None, pythonify: bool = False) -> Union[Dict, MISPOrganisation]: + def update_organisation(self, organisation: MISPOrganisation, organisation_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPOrganisation: """Update an organisation: https://www.misp-project.org/openapi/#tag/Organisations/operation/editOrganisation :param organisation: organization to update @@ -2180,7 +2467,7 @@ class PyMISP: o.from_dict(**organisation) return o - def delete_organisation(self, organisation: Union[MISPOrganisation, int, str, UUID]) -> Dict: + def delete_organisation(self, organisation: MISPOrganisation | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Delete an organisation by id: https://www.misp-project.org/openapi/#tag/Organisations/operation/deleteOrganisation :param organisation: organization to delete @@ -2194,7 +2481,7 @@ class PyMISP: # ## BEGIN User ### - def users(self, search: Optional[str] = None, organisation: Optional[int] = None, pythonify: bool = False) -> Union[Dict, List[MISPUser]]: + def users(self, search: str | None = None, organisation: int | None = None, pythonify: bool = False) -> dict[str, Any] | list[MISPUser] | list[dict[str, Any]]: """Get all the users, or a filtered set of users: https://www.misp-project.org/openapi/#tag/Users/operation/getUsers :param search: The search to make against the list of users @@ -2209,7 +2496,7 @@ class PyMISP: urlpath += f"/searchorg:{organisation_id}" r = self._prepare_request('GET', urlpath) users = self._check_json_response(r) - if not (self.global_pythonify or pythonify) or 'errors' in users: + if not (self.global_pythonify or pythonify) or isinstance(users, dict): return users to_return = [] for user in users: @@ -2218,12 +2505,12 @@ class PyMISP: to_return.append(u) return to_return - def get_user(self, user: Union[MISPUser, int, str, UUID] = 'me', pythonify: bool = False, expanded: bool = False) -> Union[Dict, MISPUser, Tuple[MISPUser, MISPRole, List[MISPUserSetting]]]: + def get_user(self, user: MISPUser | int | str | UUID = 'me', pythonify: bool = False, expanded: bool = False) -> dict[str, Any] | MISPUser | tuple[MISPUser, MISPRole, list[MISPUserSetting]]: """Get a user by id: https://www.misp-project.org/openapi/#tag/Users/operation/getUsers :param user: user to get; `me` means the owner of the API key doing the query :param pythonify: Returns a PyMISP Object instead of the plain json output - :param expanded: Also returns a MISPRole and a MISPUserSetting + :param expanded: Also returns a MISPRole and a MISPUserSetting. Only taken in account if pythonify is True. """ user_id = get_uuid_or_id_from_abstract_misp(user) r = self._prepare_request('GET', f'users/view/{user_id}') @@ -2245,7 +2532,7 @@ class PyMISP: usersettings.append(us) return u, role, usersettings - def get_new_authkey(self, user: Union[MISPUser, int, str, UUID] = 'me') -> str: + def get_new_authkey(self, user: MISPUser | int | str | UUID = 'me') -> str: '''Get a new authorization key for a specific user, defaults to user doing the call: https://www.misp-project.org/openapi/#tag/AuthKeys/operation/addAuthKey :param user: The owner of the key @@ -2258,7 +2545,7 @@ class PyMISP: else: raise PyMISPUnexpectedResponse(f'Unable to get authkey: {authkey}') - def add_user(self, user: MISPUser, pythonify: bool = False) -> Union[Dict, MISPUser]: + def add_user(self, user: MISPUser, pythonify: bool = False) -> dict[str, Any] | MISPUser: """Add a new user: https://www.misp-project.org/openapi/#tag/Users/operation/addUser :param user: user to add @@ -2272,7 +2559,7 @@ class PyMISP: u.from_dict(**user_j) return u - def update_user(self, user: MISPUser, user_id: Optional[int] = None, pythonify: bool = False) -> Union[Dict, MISPUser]: + def update_user(self, user: MISPUser, user_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPUser: """Update a user on a MISP instance: https://www.misp-project.org/openapi/#tag/Users/operation/editUser :param user: user to update @@ -2294,7 +2581,7 @@ class PyMISP: e.from_dict(**updated_user) return e - def delete_user(self, user: Union[MISPUser, int, str, UUID]) -> Dict: + def delete_user(self, user: MISPUser | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Delete a user by id: https://www.misp-project.org/openapi/#tag/Users/operation/deleteUser :param user: user to delete @@ -2304,7 +2591,7 @@ class PyMISP: response = self._prepare_request('POST', f'admin/users/delete/{user_id}') return self._check_json_response(response) - def change_user_password(self, new_password: str) -> Dict: + def change_user_password(self, new_password: str) -> dict[str, Any] | list[dict[str, Any]]: """Change the password of the curent user: :param new_password: password to set @@ -2312,14 +2599,14 @@ class PyMISP: response = self._prepare_request('POST', 'users/change_pw', data={'password': new_password}) return self._check_json_response(response) - def user_registrations(self, pythonify: bool = False) -> Union[Dict, List[MISPInbox]]: + def user_registrations(self, pythonify: bool = False) -> dict[str, Any] | list[MISPInbox] | list[dict[str, Any]]: """Get all the user registrations :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM """ r = self._prepare_request('GET', 'users/registrations/index') registrations = self._check_json_response(r) - if not (self.global_pythonify or pythonify) or 'errors' in registrations: + if not (self.global_pythonify or pythonify) or isinstance(registrations, dict): return registrations to_return = [] for registration in registrations: @@ -2328,11 +2615,12 @@ class PyMISP: to_return.append(i) return to_return - def accept_user_registration(self, registration: Union[MISPInbox, int, str, UUID], - organisation: Optional[Union[MISPOrganisation, int, str, UUID]] = None, - role: Optional[Union[MISPRole, int, str]] = None, - perm_sync: bool = False, perm_publish: bool = False, perm_admin: bool = False, - unsafe_fallback: bool = False): + def accept_user_registration(self, registration: MISPInbox | int | str | UUID, + organisation: MISPOrganisation | int | str | UUID | None = None, + role: MISPRole | int | str | None = None, + perm_sync: bool = False, perm_publish: bool = False, + perm_admin: bool = False, + unsafe_fallback: bool = False) -> dict[str, Any] | list[dict[str, Any]]: """Accept a user registration :param registration: the registration to accept @@ -2347,11 +2635,11 @@ class PyMISP: if role: role_id = role_id = get_uuid_or_id_from_abstract_misp(role) else: - for role in self.roles(pythonify=True): - if not isinstance(role, MISPRole): + for _r in self.roles(pythonify=True): + if not isinstance(_r, MISPRole): continue - if role.default_role: # type: ignore - role_id = get_uuid_or_id_from_abstract_misp(role) + if _r.default_role: # type: ignore + role_id = get_uuid_or_id_from_abstract_misp(_r) break else: raise PyMISPError('Unable to find default role') @@ -2379,7 +2667,7 @@ class PyMISP: r = self._prepare_request('POST', f'users/acceptRegistrations/{registration_id}', data=to_post) return self._check_json_response(r) - def discard_user_registration(self, registration: Union[MISPInbox, int, str, UUID]): + def discard_user_registration(self, registration: MISPInbox | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Discard a user registration :param registration: the registration to discard @@ -2392,14 +2680,14 @@ class PyMISP: # ## BEGIN Role ### - def roles(self, pythonify: bool = False) -> Union[Dict, List[MISPRole]]: + def roles(self, pythonify: bool = False) -> dict[str, Any] | list[MISPRole] | list[dict[str, Any]]: """Get the existing roles :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM """ r = self._prepare_request('GET', 'roles/index') roles = self._check_json_response(r) - if not (self.global_pythonify or pythonify) or 'errors' in roles: + if not (self.global_pythonify or pythonify) or isinstance(roles, dict): return roles to_return = [] for role in roles: @@ -2408,7 +2696,7 @@ class PyMISP: to_return.append(nr) return to_return - def set_default_role(self, role: Union[MISPRole, int, str, UUID]) -> Dict: + def set_default_role(self, role: MISPRole | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Set a default role for the new user accounts :param role: the default role to set @@ -2422,19 +2710,19 @@ class PyMISP: # ## BEGIN Decaying Models ### - def update_decaying_models(self) -> Dict: + def update_decaying_models(self) -> dict[str, Any] | list[dict[str, Any]]: """Update all the Decaying models""" response = self._prepare_request('POST', 'decayingModel/update') return self._check_json_response(response) - def decaying_models(self, pythonify: bool = False) -> Union[Dict, List[MISPDecayingModel]]: + def decaying_models(self, pythonify: bool = False) -> dict[str, Any] | list[MISPDecayingModel] | list[dict[str, Any]]: """Get all the decaying models :param pythonify: Returns a list of PyMISP Objects instead of the plain json output """ r = self._prepare_request('GET', 'decayingModel/index') models = self._check_json_response(r) - if not (self.global_pythonify or pythonify) or 'errors' in models: + if not (self.global_pythonify or pythonify) or isinstance(models, dict): return models to_return = [] for model in models: @@ -2443,7 +2731,7 @@ class PyMISP: to_return.append(n) return to_return - def enable_decaying_model(self, decaying_model: Union[MISPDecayingModel, int, str]) -> Dict: + def enable_decaying_model(self, decaying_model: MISPDecayingModel | int | str) -> dict[str, Any] | list[dict[str, Any]]: """Enable a decaying Model""" if isinstance(decaying_model, MISPDecayingModel): decaying_model_id = decaying_model.id @@ -2452,7 +2740,7 @@ class PyMISP: response = self._prepare_request('POST', f'decayingModel/enable/{decaying_model_id}') return self._check_json_response(response) - def disable_decaying_model(self, decaying_model: Union[MISPDecayingModel, int, str]) -> Dict: + def disable_decaying_model(self, decaying_model: MISPDecayingModel | int | str) -> dict[str, Any] | list[dict[str, Any]]: """Disable a decaying Model""" if isinstance(decaying_model, MISPDecayingModel): decaying_model_id = decaying_model.id @@ -2465,53 +2753,54 @@ class PyMISP: # ## BEGIN Search methods ### - def search(self, controller: str = 'events', return_format: str = 'json', - limit: Optional[int] = None, page: Optional[int] = None, - value: Optional[SearchParameterTypes] = None, - type_attribute: Optional[SearchParameterTypes] = None, - category: Optional[SearchParameterTypes] = None, - org: Optional[SearchParameterTypes] = None, - tags: Optional[SearchParameterTypes] = None, - quick_filter: Optional[str] = None, quickFilter: Optional[str] = None, - date_from: Optional[Union[datetime, date, int, str, float, None]] = None, - date_to: Optional[Union[datetime, date, int, str, float, None]] = None, - eventid: Optional[SearchType] = None, - with_attachments: Optional[bool] = None, withAttachments: Optional[bool] = None, - metadata: Optional[bool] = None, - uuid: Optional[str] = None, - publish_timestamp: Optional[Union[Union[datetime, date, int, str, float, None], - Tuple[Union[datetime, date, int, str, float, None], - Union[datetime, date, int, str, float, None]] - ]] = None, - last: Optional[Union[Union[datetime, date, int, str, float, None], - Tuple[Union[datetime, date, int, str, float, None], - Union[datetime, date, int, str, float, None]] - ]] = None, - timestamp: Optional[Union[Union[datetime, date, int, str, float, None], - Tuple[Union[datetime, date, int, str, float, None], - Union[datetime, date, int, str, float, None]] - ]] = None, - published: Optional[bool] = None, - enforce_warninglist: Optional[bool] = None, enforceWarninglist: Optional[bool] = None, - to_ids: Optional[Union[ToIDSType, List[ToIDSType]]] = None, - deleted: Optional[str] = None, - include_event_uuid: Optional[bool] = None, includeEventUuid: Optional[bool] = None, - include_event_tags: Optional[bool] = None, includeEventTags: Optional[bool] = None, - event_timestamp: Optional[Union[datetime, date, int, str, float, None]] = None, - sg_reference_only: Optional[bool] = None, - eventinfo: Optional[str] = None, - searchall: Optional[bool] = None, - requested_attributes: Optional[str] = None, - include_context: Optional[bool] = None, includeContext: Optional[bool] = None, - headerless: Optional[bool] = None, - include_sightings: Optional[bool] = None, includeSightings: Optional[bool] = None, - include_correlations: Optional[bool] = None, includeCorrelations: Optional[bool] = None, - include_decay_score: Optional[bool] = None, includeDecayScore: Optional[bool] = None, - object_name: Optional[str] = None, - exclude_decayed: Optional[bool] = None, - sharinggroup: Optional[Union[int, List[int]]] = None, - pythonify: Optional[bool] = False, - **kwargs) -> Union[Dict, str, List[Union[MISPEvent, MISPAttribute, MISPObject]]]: + def search(self, controller: str = 'events', return_format: str = 'json', # type: ignore[no-untyped-def] + limit: int | None = None, page: int | None = None, + value: SearchParameterTypes | None = None, + type_attribute: SearchParameterTypes | None = None, + category: SearchParameterTypes | None = None, + org: SearchParameterTypes | None = None, + tags: SearchParameterTypes | None = None, + event_tags: SearchParameterTypes | None = None, + quick_filter: str | None = None, quickFilter: str | None = None, + date_from: datetime | date | int | str | float | None | None = None, + date_to: datetime | date | int | str | float | None | None = None, + eventid: SearchType | None = None, + with_attachments: bool | None = None, withAttachments: bool | None = None, + metadata: bool | None = None, + uuid: str | None = None, + publish_timestamp: None | (datetime | date | int | str | float | None + | tuple[datetime | date | int | str | float | None, + datetime | date | int | str | float | None] + ) = None, + last: None | (datetime | date | int | str | float | None + | tuple[datetime | date | int | str | float | None, + datetime | date | int | str | float | None] + ) = None, + timestamp: None | (datetime | date | int | str | float | None + | tuple[datetime | date | int | str | float | None, + datetime | date | int | str | float | None] + ) = None, + published: bool | None = None, + enforce_warninglist: bool | None = None, enforceWarninglist: bool | None = None, + to_ids: ToIDSType | list[ToIDSType] | None = None, + deleted: str | None = None, + include_event_uuid: bool | None = None, includeEventUuid: bool | None = None, + include_event_tags: bool | None = None, includeEventTags: bool | None = None, + event_timestamp: datetime | date | int | str | float | None | None = None, + sg_reference_only: bool | None = None, + eventinfo: str | None = None, + searchall: bool | None = None, + requested_attributes: str | None = None, + include_context: bool | None = None, includeContext: bool | None = None, + headerless: bool | None = None, + include_sightings: bool | None = None, includeSightings: bool | None = None, + include_correlations: bool | None = None, includeCorrelations: bool | None = None, + include_decay_score: bool | None = None, includeDecayScore: bool | None = None, + object_name: str | None = None, + exclude_decayed: bool | None = None, + sharinggroup: int | list[int] | None = None, + pythonify: bool | None = False, + **kwargs) -> dict[str, Any] | str | list[MISPEvent | MISPAttribute | MISPObject] | list[dict[str, Any]]: '''Search in the MISP instance :param controller: Controller to search on, it can be `events`, `objects`, `attributes`. The response will either be a list of events, objects, or attributes. @@ -2529,6 +2818,7 @@ class PyMISP: :param category: The attribute category, any valid MISP attribute category is accepted. :param org: Search by the creator organisation by supplying the organisation identifier. :param tags: Tags to search or to exclude. You can pass a list, or the output of `build_complex_query` + :param event_tags: Tags to search or to exclude at the event level. You can pass a list, or the output of `build_complex_query` :param quick_filter: The string passed to this field will ignore all of the other arguments. MISP will return an xml / json (depending on the header sent) of all events that have a sub-string match on value in the event info, event orgc, or any of the attribute value1 / value2 fields, or in the attribute comment. :param date_from: Events with the date set to a date after the one specified. This filter will use the date of the event. :param date_to: Events with the date set to a date before the one specified. This filter will use the date of the event. @@ -2571,9 +2861,10 @@ class PyMISP: ''' - return_formats = ['openioc', 'json', 'xml', 'suricata', 'snort', 'text', 'rpz', 'csv', 'cache', 'stix-xml', 'stix', 'stix2', 'yara', 'yara-json', 'attack', 'attack-sightings'] + return_formats = ('openioc', 'json', 'xml', 'suricata', 'snort', 'text', 'rpz', 'csv', 'cache', 'stix-xml', + 'stix', 'stix2', 'yara', 'yara-json', 'attack', 'attack-sightings', 'context', 'context-markdown') - if controller not in ['events', 'attributes', 'objects']: + if controller not in ('events', 'attributes', 'objects'): raise ValueError('controller has to be in {}'.format(', '.join(['events', 'attributes', 'objects']))) # Deprecated stuff / synonyms @@ -2615,6 +2906,7 @@ class PyMISP: query['category'] = category query['org'] = org query['tags'] = tags + query['event_tags'] = event_tags query['quickFilter'] = quick_filter query['from'] = self._make_timestamp(date_from) query['to'] = self._make_timestamp(date_to) @@ -2674,22 +2966,29 @@ class PyMISP: elif return_format not in ['json', 'yara-json']: return self._check_response(response) - normalized_response = self._check_json_response(response) + normalized_response: list[dict[str, Any]] | dict[str, Any] + if controller in ['events', 'objects']: + # This one is truly fucked: event returns a list, attributes doesn't. + normalized_response = self._check_json_response(response) + elif controller == 'attributes': + normalized_response = self._check_json_response(response) if 'errors' in normalized_response: return normalized_response if return_format == 'json' and self.global_pythonify or pythonify: # The response is in json, we can convert it to a list of pythonic MISP objects - to_return: List[Union[MISPEvent, MISPAttribute, MISPObject]] = [] + to_return: list[MISPEvent | MISPAttribute | MISPObject] = [] if controller == 'events': + if isinstance(normalized_response, dict): + return normalized_response for e in normalized_response: me = MISPEvent() me.load(e) to_return.append(me) elif controller == 'attributes': # FIXME: obvs, this is hurting my soul. We need something generic. - for a in normalized_response['Attribute']: + for a in normalized_response['Attribute']: # type: ignore[call-overload] ma = MISPAttribute() ma.from_dict(**a) if 'Event' in ma: @@ -2716,6 +3015,8 @@ class PyMISP: ma.Sighting = sightings to_return.append(ma) elif controller == 'objects': + if isinstance(normalized_response, dict): + return normalized_response for o in normalized_response: mo = MISPObject(o['Object']['name']) mo.from_dict(**o) @@ -2725,35 +3026,35 @@ class PyMISP: return normalized_response def search_index(self, - all: Optional[str] = None, - attribute: Optional[str] = None, - email: Optional[str] = None, - published: Optional[bool] = None, - hasproposal: Optional[bool] = None, - eventid: Optional[SearchType] = None, - tags: Optional[SearchParameterTypes] = None, - date_from: Optional[Union[datetime, date, int, str, float, None]] = None, - date_to: Optional[Union[datetime, date, int, str, float, None]] = None, - eventinfo: Optional[str] = None, - threatlevel: Optional[List[SearchType]] = None, - distribution: Optional[List[SearchType]] = None, - analysis: Optional[List[SearchType]] = None, - org: Optional[SearchParameterTypes] = None, - timestamp: Optional[Union[Union[datetime, date, int, str, float, None], - Tuple[Union[datetime, date, int, str, float, None], - Union[datetime, date, int, str, float, None]] - ]] = None, - publish_timestamp: Optional[Union[Union[datetime, date, int, str, float, None], - Tuple[Union[datetime, date, int, str, float, None], - Union[datetime, date, int, str, float, None]] - ]] = None, - sharinggroup: Optional[List[SearchType]] = None, - minimal: Optional[bool] = None, - sort: Optional[str] = None, - desc: Optional[bool] = None, - limit: Optional[int] = None, - page: Optional[int] = None, - pythonify: Optional[bool] = None) -> Union[Dict, List[MISPEvent]]: + all: str | None = None, + attribute: str | None = None, + email: str | None = None, + published: bool | None = None, + hasproposal: bool | None = None, + eventid: SearchType | None = None, + tags: SearchParameterTypes | None = None, + date_from: datetime | date | int | str | float | None | None = None, + date_to: datetime | date | int | str | float | None | None = None, + eventinfo: str | None = None, + threatlevel: list[SearchType] | None = None, + distribution: list[SearchType] | None = None, + analysis: list[SearchType] | None = None, + org: SearchParameterTypes | None = None, + timestamp: None | (datetime | date | int | str | float | None + | tuple[datetime | date | int | str | float | None, + datetime | date | int | str | float | None] + ) = None, + publish_timestamp: None | (datetime | date | int | str | float | None + | tuple[datetime | date | int | str | float | None, + datetime | date | int | str | float | None] + ) = None, + sharinggroup: list[SearchType] | None = None, + minimal: bool | None = None, + sort: str | None = None, + desc: bool | None = None, + limit: int | None = None, + page: int | None = None, + pythonify: bool | None = None) -> dict[str, Any] | list[MISPEvent] | list[dict[str, Any]]: """Search event metadata shown on the event index page. Using ! in front of a value means NOT, except for parameters date_from, date_to and timestamp which cannot be negated. Criteria are AND-ed together; values in lists are OR-ed together. Return matching events @@ -2812,7 +3113,7 @@ class PyMISP: response = self._prepare_request('POST', url, data=query) normalized_response = self._check_json_response(response) - if not (self.global_pythonify or pythonify): + if not (self.global_pythonify or pythonify) or isinstance(normalized_response, dict): return normalized_response to_return = [] for e_meta in normalized_response: @@ -2821,25 +3122,25 @@ class PyMISP: to_return.append(me) return to_return - def search_sightings(self, context: Optional[str] = None, - context_id: Optional[SearchType] = None, - type_sighting: Optional[str] = None, - date_from: Optional[Union[datetime, date, int, str, float, None]] = None, - date_to: Optional[Union[datetime, date, int, str, float, None]] = None, - publish_timestamp: Optional[Union[Union[datetime, date, int, str, float, None], - Tuple[Union[datetime, date, int, str, float, None], - Union[datetime, date, int, str, float, None]] - ]] = None, - last: Optional[Union[Union[datetime, date, int, str, float, None], - Tuple[Union[datetime, date, int, str, float, None], - Union[datetime, date, int, str, float, None]] - ]] = None, - org: Optional[SearchType] = None, - source: Optional[str] = None, - include_attribute: Optional[bool] = None, - include_event_meta: Optional[bool] = None, - pythonify: Optional[bool] = False - ) -> Union[Dict, List[Dict[str, Union[MISPEvent, MISPAttribute, MISPSighting]]]]: + def search_sightings(self, context: str | None = None, + context_id: SearchType | None = None, + type_sighting: str | None = None, + date_from: datetime | date | int | str | float | None | None = None, + date_to: datetime | date | int | str | float | None | None = None, + publish_timestamp: None | (datetime | date | int | str | float | None + | tuple[datetime | date | int | str | float | None, + datetime | date | int | str | float | None] + ) = None, + last: None | (datetime | date | int | str | float | None + | tuple[datetime | date | int | str | float | None, + datetime | date | int | str | float | None] + ) = None, + org: SearchType | None = None, + source: str | None = None, + include_attribute: bool | None = None, + include_event_meta: bool | None = None, + pythonify: bool | None = False + ) -> dict[str, Any] | list[dict[str, MISPEvent | MISPAttribute | MISPSighting]]: '''Search sightings :param context: The context of the search. Can be either "attribute", "event", or nothing (will then match on events and attributes). @@ -2865,7 +3166,7 @@ class PyMISP: [ ... ] >>> misp.search_sightings(context='event', context_id=17, include_event_meta=True, org=2) # return list of sighting for event 17 filtered with org id 2 ''' - query: Dict[str, Any] = {'returnFormat': 'json'} + query: dict[str, Any] = {'returnFormat': 'json'} if context is not None: if context not in ['attribute', 'event']: raise ValueError('context has to be in {}'.format(', '.join(['attribute', 'event']))) @@ -2887,13 +3188,13 @@ class PyMISP: url = urljoin(self.root_url, url_path) response = self._prepare_request('POST', url, data=query) normalized_response = self._check_json_response(response) - if not (self.global_pythonify or pythonify) or 'errors' in normalized_response: + if not (self.global_pythonify or pythonify) or isinstance(normalized_response, dict): return normalized_response if self.global_pythonify or pythonify: to_return = [] for s in normalized_response: - entries: Dict[str, Union[MISPEvent, MISPAttribute, MISPSighting]] = {} + entries: dict[str, MISPEvent | MISPAttribute | MISPSighting] = {} s_data = s['Sighting'] if include_event_meta: e = s_data.pop('Event') @@ -2912,13 +3213,13 @@ class PyMISP: return to_return return normalized_response - def search_logs(self, limit: Optional[int] = None, page: Optional[int] = None, - log_id: Optional[int] = None, title: Optional[str] = None, - created: Optional[Union[datetime, date, int, str, float, None]] = None, model: Optional[str] = None, - action: Optional[str] = None, user_id: Optional[int] = None, - change: Optional[str] = None, email: Optional[str] = None, - org: Optional[str] = None, description: Optional[str] = None, - ip: Optional[str] = None, pythonify: Optional[bool] = False) -> Union[Dict, List[MISPLog]]: + def search_logs(self, limit: int | None = None, page: int | None = None, + log_id: int | None = None, title: str | None = None, + created: datetime | date | int | str | float | None | None = None, model: str | None = None, + action: str | None = None, user_id: int | None = None, + change: str | None = None, email: str | None = None, + org: str | None = None, description: str | None = None, + ip: str | None = None, pythonify: bool | None = False) -> dict[str, Any] | list[MISPLog] | list[dict[str, Any]]: '''Search in logs Note: to run substring queries simply append/prepend/encapsulate the search term with % @@ -2943,12 +3244,12 @@ class PyMISP: query.pop('pythonify') if log_id is not None: query['id'] = query.pop('log_id') - if created is not None and isinstance(created, (datetime)): + if created is not None and isinstance(created, datetime): query['created'] = query.pop('created').timestamp() response = self._prepare_request('POST', 'admin/logs/index', data=query) normalized_response = self._check_json_response(response) - if not (self.global_pythonify or pythonify) or 'errors' in normalized_response: + if not (self.global_pythonify or pythonify) or isinstance(normalized_response, dict): return normalized_response to_return = [] @@ -2958,11 +3259,11 @@ class PyMISP: to_return.append(ml) return to_return - def search_feeds(self, value: Optional[SearchParameterTypes] = None, pythonify: Optional[bool] = False) -> Union[Dict, List[MISPFeed]]: + def search_feeds(self, value: SearchParameterTypes | None = None, pythonify: bool | None = False) -> dict[str, Any] | list[MISPFeed] | list[dict[str, Any]]: '''Search in the feeds cached on the servers''' response = self._prepare_request('POST', 'feeds/searchCaches', data={'value': value}) normalized_response = self._check_json_response(response) - if not (self.global_pythonify or pythonify) or 'errors' in normalized_response: + if not (self.global_pythonify or pythonify) or isinstance(normalized_response, dict): return normalized_response to_return = [] for feed in normalized_response: @@ -2975,14 +3276,14 @@ class PyMISP: # ## BEGIN Communities ### - def communities(self, pythonify: bool = False) -> Union[Dict, List[MISPCommunity]]: + def communities(self, pythonify: bool = False) -> dict[str, Any] | list[MISPCommunity] | list[dict[str, Any]]: """Get all the communities :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM """ r = self._prepare_request('GET', 'communities/index') communities = self._check_json_response(r) - if not (self.global_pythonify or pythonify) or 'errors' in communities: + if not (self.global_pythonify or pythonify) or isinstance(communities, dict): return communities to_return = [] for community in communities: @@ -2991,7 +3292,7 @@ class PyMISP: to_return.append(c) return to_return - def get_community(self, community: Union[MISPCommunity, int, str, UUID], pythonify: bool = False) -> Union[Dict, MISPCommunity]: + def get_community(self, community: MISPCommunity | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPCommunity: """Get a community by id from a MISP instance :param community: community to get @@ -3006,15 +3307,15 @@ class PyMISP: c.from_dict(**community_j) return c - def request_community_access(self, community: Union[MISPCommunity, int, str, UUID], - requestor_email_address: Optional[str] = None, - requestor_gpg_key: Optional[str] = None, - requestor_organisation_name: Optional[str] = None, - requestor_organisation_uuid: Optional[str] = None, - requestor_organisation_description: Optional[str] = None, - message: Optional[str] = None, sync: bool = False, + def request_community_access(self, community: MISPCommunity | int | str | UUID, + requestor_email_address: str | None = None, + requestor_gpg_key: str | None = None, + requestor_organisation_name: str | None = None, + requestor_organisation_uuid: str | None = None, + requestor_organisation_description: str | None = None, + message: str | None = None, sync: bool = False, anonymise_requestor_server: bool = False, - mock: bool = False) -> Dict: + mock: bool = False) -> dict[str, Any] | list[dict[str, Any]]: """Request the access to a community :param community: community to request access @@ -3042,14 +3343,14 @@ class PyMISP: # ## BEGIN Event Delegation ### - def event_delegations(self, pythonify: bool = False) -> Union[Dict, List[MISPEventDelegation]]: + def event_delegations(self, pythonify: bool = False) -> dict[str, Any] | list[MISPEventDelegation] | list[dict[str, Any]]: """Get all the event delegations :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM """ r = self._prepare_request('GET', 'eventDelegations') delegations = self._check_json_response(r) - if not (self.global_pythonify or pythonify) or 'errors' in delegations: + if not (self.global_pythonify or pythonify) or isinstance(delegations, dict): return delegations to_return = [] for delegation in delegations: @@ -3058,7 +3359,7 @@ class PyMISP: to_return.append(d) return to_return - def accept_event_delegation(self, delegation: Union[MISPEventDelegation, int, str], pythonify: bool = False) -> Dict: + def accept_event_delegation(self, delegation: MISPEventDelegation | int | str, pythonify: bool = False) -> dict[str, Any] | list[dict[str, Any]]: """Accept the delegation of an event :param delegation: event delegation to accept @@ -3068,7 +3369,7 @@ class PyMISP: r = self._prepare_request('POST', f'eventDelegations/acceptDelegation/{delegation_id}') return self._check_json_response(r) - def discard_event_delegation(self, delegation: Union[MISPEventDelegation, int, str], pythonify: bool = False) -> Dict: + def discard_event_delegation(self, delegation: MISPEventDelegation | int | str, pythonify: bool = False) -> dict[str, Any] | list[dict[str, Any]]: """Discard the delegation of an event :param delegation: event delegation to discard @@ -3078,10 +3379,10 @@ class PyMISP: r = self._prepare_request('POST', f'eventDelegations/deleteDelegation/{delegation_id}') return self._check_json_response(r) - def delegate_event(self, event: Optional[Union[MISPEvent, int, str, UUID]] = None, - organisation: Optional[Union[MISPOrganisation, int, str, UUID]] = None, - event_delegation: Optional[MISPEventDelegation] = None, - distribution: int = -1, message: str = '', pythonify: bool = False) -> Union[Dict, MISPEventDelegation]: + def delegate_event(self, event: MISPEvent | int | str | UUID | None = None, + organisation: MISPOrganisation | int | str | UUID | None = None, + event_delegation: MISPEventDelegation | None = None, + distribution: int = -1, message: str = '', pythonify: bool = False) -> dict[str, Any] | MISPEventDelegation: """Delegate an event. Either event and organisation OR event_delegation are required :param event: event to delegate @@ -3097,7 +3398,7 @@ class PyMISP: data = {'event_id': event_id, 'org_id': organisation_id, 'distribution': distribution, 'message': message} r = self._prepare_request('POST', f'eventDelegations/delegateEvent/{event_id}', data=data) elif event_delegation: - r = self._prepare_request('POST', f'eventDelegations/delegateEvent/{event_id}', data=event_delegation) + r = self._prepare_request('POST', f'eventDelegations/delegateEvent/{event_delegation.event_id}', data=event_delegation) else: raise PyMISPError('Either event and organisation OR event_delegation are required.') delegation_j = self._check_json_response(r) @@ -3111,7 +3412,7 @@ class PyMISP: # ## BEGIN Others ### - def push_event_to_ZMQ(self, event: Union[MISPEvent, int, str, UUID]) -> Dict: + def push_event_to_ZMQ(self, event: MISPEvent | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Force push an event by id on ZMQ :param event: the event to push @@ -3120,7 +3421,7 @@ class PyMISP: response = self._prepare_request('POST', f'events/pushEventToZMQ/{event_id}.json') return self._check_json_response(response) - def direct_call(self, url: str, data: Optional[Dict] = None, params: Mapping = {}, kw_params: Mapping = {}) -> Any: + def direct_call(self, url: str, data: dict[str, Any] | None = None, params: Mapping[str, Any] = {}, kw_params: Mapping[str, Any] = {}) -> Any: """Very lightweight call that posts a data blob (python dictionary or json string) on the URL :param url: URL to post to @@ -3134,8 +3435,8 @@ class PyMISP: response = self._prepare_request('POST', url, data=data, params=params, kw_params=kw_params) return self._check_response(response, lenient_response_type=True) - def freetext(self, event: Union[MISPEvent, int, str, UUID], string: str, adhereToWarninglists: Union[bool, str] = False, - distribution: Optional[int] = None, returnMetaAttributes: bool = False, pythonify: bool = False, **kwargs) -> Union[Dict, List[MISPAttribute]]: + def freetext(self, event: MISPEvent | int | str | UUID, string: str, adhereToWarninglists: bool | str = False, # type: ignore[no-untyped-def] + distribution: int | None = None, returnMetaAttributes: bool = False, pythonify: bool = False, **kwargs) -> dict[str, Any] | list[MISPAttribute] | list[dict[str, Any]]: """Pass a text to the freetext importer :param event: event @@ -3148,7 +3449,7 @@ class PyMISP: """ event_id = get_uuid_or_id_from_abstract_misp(event) - query: Dict[str, Any] = {"value": string} + query: dict[str, Any] = {"value": string} wl_params = [False, True, 'soft'] if adhereToWarninglists in wl_params: query['adhereToWarninglists'] = adhereToWarninglists @@ -3160,7 +3461,7 @@ class PyMISP: query['returnMetaAttributes'] = returnMetaAttributes r = self._prepare_request('POST', f'events/freeTextImport/{event_id}', data=query, **kwargs) attributes = self._check_json_response(r) - if returnMetaAttributes or not (self.global_pythonify or pythonify) or 'errors' in attributes: + if returnMetaAttributes or not (self.global_pythonify or pythonify) or isinstance(attributes, dict): return attributes to_return = [] for attribute in attributes: @@ -3169,14 +3470,15 @@ class PyMISP: to_return.append(a) return to_return - def upload_stix(self, path: Optional[Union[str, Path, BytesIO, StringIO]] = None, data: Optional[Union[str, bytes]] = None, version: str = '2'): + def upload_stix(self, path: str | Path | BytesIO | StringIO | None = None, + data: str | bytes | None = None, version: str = '2') -> requests.Response: """Upload a STIX file to MISP. :param path: Path to the STIX on the disk (can be a path-like object, or a pseudofile) :param data: stix object :param version: Can be 1 or 2 """ - to_post: Union[str, bytes] + to_post: str | bytes if path is not None: if isinstance(path, (str, Path)): with open(path, 'rb') as f: @@ -3190,17 +3492,17 @@ class PyMISP: if str(version) == '1': url = urljoin(self.root_url, 'events/upload_stix') - response = self._prepare_request('POST', url, data=to_post, output_type='xml', content_type='xml') # type: ignore + response = self._prepare_request('POST', url, data=to_post, output_type='xml', content_type='xml') else: url = urljoin(self.root_url, 'events/upload_stix/2') - response = self._prepare_request('POST', url, data=to_post) # type: ignore + response = self._prepare_request('POST', url, data=to_post) return response # ## END Others ### # ## BEGIN Statistics ### - def attributes_statistics(self, context: str = 'type', percentage: bool = False) -> Dict: + def attributes_statistics(self, context: str = 'type', percentage: bool = False) -> dict[str, Any] | list[dict[str, Any]]: """Get attribute statistics from the MISP instance :param context: "type" or "category" @@ -3216,7 +3518,7 @@ class PyMISP: response = self._prepare_request('GET', path) return self._check_json_response(response) - def tags_statistics(self, percentage: bool = False, name_sort: bool = False) -> Dict: + def tags_statistics(self, percentage: bool = False, name_sort: bool = False) -> dict[str, Any] | list[dict[str, Any]]: """Get tag statistics from the MISP instance :param percentage: get percentages @@ -3235,7 +3537,7 @@ class PyMISP: response = self._prepare_request('GET', f'tags/tagStatistics/{p}/{ns}') return self._check_json_response(response) - def users_statistics(self, context: str = 'data') -> Dict: + def users_statistics(self, context: str = 'data') -> dict[str, Any] | list[dict[str, Any]]: """Get user statistics from the MISP instance :param context: one of 'data', 'orgs', 'users', 'tags', 'attributehistogram', 'sightings', 'galaxyMatrix' @@ -3244,20 +3546,23 @@ class PyMISP: if context not in availables_contexts: raise PyMISPError("context can only be {','.join(availables_contexts)}") response = self._prepare_request('GET', f'users/statistics/{context}') - return self._check_json_response(response) + try: + return self._check_json_response(response) + except PyMISPError: + return self._check_json_response(response) # ## END Statistics ### # ## BEGIN User Settings ### - def user_settings(self, pythonify: bool = False) -> Union[Dict, List[MISPUserSetting]]: + def user_settings(self, pythonify: bool = False) -> dict[str, Any] | list[MISPUserSetting] | list[dict[str, Any]]: """Get all the user settings: https://www.misp-project.org/openapi/#tag/UserSettings/operation/getUserSettings :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM """ r = self._prepare_request('GET', 'userSettings/index') user_settings = self._check_json_response(r) - if not (self.global_pythonify or pythonify) or 'errors' in user_settings: + if not (self.global_pythonify or pythonify) or isinstance(user_settings, dict): return user_settings to_return = [] for user_setting in user_settings: @@ -3266,15 +3571,15 @@ class PyMISP: to_return.append(u) return to_return - def get_user_setting(self, user_setting: str, user: Optional[Union[MISPUser, int, str, UUID]] = None, - pythonify: bool = False) -> Union[Dict, MISPUserSetting]: + def get_user_setting(self, user_setting: str, user: MISPUser | int | str | UUID | None = None, + pythonify: bool = False) -> dict[str, Any] | MISPUserSetting: """Get a user setting: https://www.misp-project.org/openapi/#tag/UserSettings/operation/getUserSettingById :param user_setting: name of user setting :param user: user :param pythonify: Returns a PyMISP Object instead of the plain json output """ - query: Dict[str, Any] = {'setting': user_setting} + query: dict[str, Any] = {'setting': user_setting} if user: query['user_id'] = get_uuid_or_id_from_abstract_misp(user) response = self._prepare_request('POST', 'userSettings/getSetting', data=query) @@ -3285,8 +3590,8 @@ class PyMISP: u.from_dict(**user_setting_j) return u - def set_user_setting(self, user_setting: str, value: Union[str, dict], user: Optional[Union[MISPUser, int, str, UUID]] = None, - pythonify: bool = False) -> Union[Dict, MISPUserSetting]: + def set_user_setting(self, user_setting: str, value: str | dict[str, Any], user: MISPUser | int | str | UUID | None = None, + pythonify: bool = False) -> dict[str, Any] | MISPUserSetting: """Set a user setting: https://www.misp-project.org/openapi/#tag/UserSettings/operation/setUserSetting :param user_setting: name of user setting @@ -3294,9 +3599,9 @@ class PyMISP: :param user: user :param pythonify: Returns a PyMISP Object instead of the plain json output """ - query: Dict[str, Any] = {'setting': user_setting} + query: dict[str, Any] = {'setting': user_setting} if isinstance(value, dict): - value = json.dumps(value) + value = str(dumps(value)) if HAS_ORJSON else dumps(value) query['value'] = value if user: query['user_id'] = get_uuid_or_id_from_abstract_misp(user) @@ -3308,13 +3613,13 @@ class PyMISP: u.from_dict(**user_setting_j) return u - def delete_user_setting(self, user_setting: str, user: Optional[Union[MISPUser, int, str, UUID]] = None) -> Dict: + def delete_user_setting(self, user_setting: str, user: MISPUser | int | str | UUID | None = None) -> dict[str, Any] | list[dict[str, Any]]: """Delete a user setting: https://www.misp-project.org/openapi/#tag/UserSettings/operation/deleteUserSettingById :param user_setting: name of user setting :param user: user """ - query: Dict[str, Any] = {'setting': user_setting} + query: dict[str, Any] = {'setting': user_setting} if user: query['user_id'] = get_uuid_or_id_from_abstract_misp(user) response = self._prepare_request('POST', 'userSettings/delete', data=query) @@ -3324,14 +3629,14 @@ class PyMISP: # ## BEGIN Blocklists ### - def event_blocklists(self, pythonify: bool = False) -> Union[Dict, List[MISPEventBlocklist]]: + def event_blocklists(self, pythonify: bool = False) -> dict[str, Any] | list[MISPEventBlocklist] | list[dict[str, Any]]: """Get all the blocklisted events :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM """ r = self._prepare_request('GET', 'eventBlocklists/index') event_blocklists = self._check_json_response(r) - if not (self.global_pythonify or pythonify) or 'errors' in event_blocklists: + if not (self.global_pythonify or pythonify) or isinstance(event_blocklists, dict): return event_blocklists to_return = [] for event_blocklist in event_blocklists: @@ -3340,14 +3645,14 @@ class PyMISP: to_return.append(ebl) return to_return - def organisation_blocklists(self, pythonify: bool = False) -> Union[Dict, List[MISPOrganisationBlocklist]]: + def organisation_blocklists(self, pythonify: bool = False) -> dict[str, Any] | list[MISPOrganisationBlocklist] | list[dict[str, Any]]: """Get all the blocklisted organisations :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM """ r = self._prepare_request('GET', 'orgBlocklists/index') organisation_blocklists = self._check_json_response(r) - if not (self.global_pythonify or pythonify) or 'errors' in organisation_blocklists: + if not (self.global_pythonify or pythonify) or isinstance(organisation_blocklists, dict): return organisation_blocklists to_return = [] for organisation_blocklist in organisation_blocklists: @@ -3356,7 +3661,7 @@ class PyMISP: to_return.append(obl) return to_return - def _add_entries_to_blocklist(self, blocklist_type: str, uuids: Union[str, List[str]], **kwargs) -> Dict: + def _add_entries_to_blocklist(self, blocklist_type: str, uuids: str | list[str], **kwargs) -> dict[str, Any] | list[dict[str, Any]]: # type: ignore[no-untyped-def] if blocklist_type == 'event': url = 'eventBlocklists/add' elif blocklist_type == 'organisation': @@ -3371,8 +3676,8 @@ class PyMISP: r = self._prepare_request('POST', url, data=data) return self._check_json_response(r) - def add_event_blocklist(self, uuids: Union[str, List[str]], comment: Optional[str] = None, - event_info: Optional[str] = None, event_orgc: Optional[str] = None) -> Dict: + def add_event_blocklist(self, uuids: str | list[str], comment: str | None = None, + event_info: str | None = None, event_orgc: str | None = None) -> dict[str, Any] | list[dict[str, Any]]: """Add a new event in the blocklist :param uuids: UUIDs @@ -3382,8 +3687,8 @@ class PyMISP: """ return self._add_entries_to_blocklist('event', uuids=uuids, comment=comment, event_info=event_info, event_orgc=event_orgc) - def add_organisation_blocklist(self, uuids: Union[str, List[str]], comment: Optional[str] = None, - org_name: Optional[str] = None) -> Dict: + def add_organisation_blocklist(self, uuids: str | list[str], comment: str | None = None, + org_name: str | None = None) -> dict[str, Any] | list[dict[str, Any]]: """Add a new organisation in the blocklist :param uuids: UUIDs @@ -3392,7 +3697,7 @@ class PyMISP: """ return self._add_entries_to_blocklist('organisation', uuids=uuids, comment=comment, org_name=org_name) - def _update_entries_in_blocklist(self, blocklist_type: str, uuid, **kwargs) -> Dict: + def _update_entries_in_blocklist(self, blocklist_type: str, uuid, **kwargs) -> dict[str, Any] | list[dict[str, Any]]: # type: ignore[no-untyped-def] if blocklist_type == 'event': url = f'eventBlocklists/edit/{uuid}' elif blocklist_type == 'organisation': @@ -3403,7 +3708,7 @@ class PyMISP: r = self._prepare_request('POST', url, data=data) return self._check_json_response(r) - def update_event_blocklist(self, event_blocklist: MISPEventBlocklist, event_blocklist_id: Optional[Union[int, str, UUID]] = None, pythonify: bool = False) -> Union[Dict, MISPEventBlocklist]: + def update_event_blocklist(self, event_blocklist: MISPEventBlocklist, event_blocklist_id: int | str | UUID | None = None, pythonify: bool = False) -> dict[str, Any] | MISPEventBlocklist: """Update an event in the blocklist :param event_blocklist: event block list @@ -3421,7 +3726,7 @@ class PyMISP: e.from_dict(**updated_event_blocklist) return e - def update_organisation_blocklist(self, organisation_blocklist: MISPOrganisationBlocklist, organisation_blocklist_id: Optional[Union[int, str, UUID]] = None, pythonify: bool = False) -> Union[Dict, MISPOrganisationBlocklist]: + def update_organisation_blocklist(self, organisation_blocklist: MISPOrganisationBlocklist, organisation_blocklist_id: int | str | UUID | None = None, pythonify: bool = False) -> dict[str, Any] | MISPOrganisationBlocklist: """Update an organisation in the blocklist :param organisation_blocklist: organization block list @@ -3439,7 +3744,7 @@ class PyMISP: o.from_dict(**updated_organisation_blocklist) return o - def delete_event_blocklist(self, event_blocklist: Union[MISPEventBlocklist, str, UUID]) -> Dict: + def delete_event_blocklist(self, event_blocklist: MISPEventBlocklist | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Delete a blocklisted event by id :param event_blocklist: event block list to delete @@ -3448,7 +3753,7 @@ class PyMISP: response = self._prepare_request('POST', f'eventBlocklists/delete/{event_blocklist_id}') return self._check_json_response(response) - def delete_organisation_blocklist(self, organisation_blocklist: Union[MISPOrganisationBlocklist, str, UUID]) -> Dict: + def delete_organisation_blocklist(self, organisation_blocklist: MISPOrganisationBlocklist | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: """Delete a blocklisted organisation by id :param organisation_blocklist: organization block list to delete @@ -3461,7 +3766,8 @@ class PyMISP: # ## BEGIN Global helpers ### - def change_sharing_group_on_entity(self, misp_entity: Union[MISPEvent, MISPAttribute, MISPObject], sharing_group_id, pythonify: bool = False) -> Union[Dict, MISPEvent, MISPObject, MISPAttribute, MISPShadowAttribute]: + def change_sharing_group_on_entity(self, misp_entity: MISPEvent | MISPAttribute | MISPObject, + sharing_group_id: int, pythonify: bool = False) -> dict[str, Any] | MISPEvent | MISPObject | MISPAttribute | MISPShadowAttribute: """Change the sharing group of an event, an attribute, or an object :param misp_entity: entity to change @@ -3483,21 +3789,27 @@ class PyMISP: raise PyMISPError('The misp_entity must be MISPEvent, MISPObject or MISPAttribute') - def tag(self, misp_entity: Union[AbstractMISP, str, dict], tag: Union[MISPTag, str], local: bool = False) -> Dict: + def tag(self, misp_entity: AbstractMISP | str | dict[str, Any], tag: MISPTag | str, + local: bool = False, relationship_type: str | None = None) -> dict[str, Any] | list[dict[str, Any]]: """Tag an event or an attribute. :param misp_entity: a MISPEvent, a MISP Attribute, or a UUID :param tag: tag to add :param local: whether to tag locally + :param relationship_type: Type of relationship between the tag and the attribute or event """ uuid = get_uuid_or_id_from_abstract_misp(misp_entity) if isinstance(tag, MISPTag): - tag = tag.name - to_post = {'uuid': uuid, 'tag': tag, 'local': local} + tag_name = tag.name if 'name' in tag else "" + else: + tag_name = tag + to_post = {'uuid': uuid, 'tag': tag_name, 'local': local} + if relationship_type: + to_post['relationship_type'] = relationship_type response = self._prepare_request('POST', 'tags/attachTagToObject', data=to_post) return self._check_json_response(response) - def untag(self, misp_entity: Union[AbstractMISP, str, dict], tag: Union[MISPTag, str]) -> Dict: + def untag(self, misp_entity: AbstractMISP | str | dict[str, Any], tag: MISPTag | str) -> dict[str, Any] | list[dict[str, Any]]: """Untag an event or an attribute :param misp_entity: misp_entity can be a UUID @@ -3505,17 +3817,16 @@ class PyMISP: """ uuid = get_uuid_or_id_from_abstract_misp(misp_entity) if isinstance(tag, MISPTag): - if 'name' in tag: - tag_name = tag.name + tag_name = tag.name if 'name' in tag else "" else: tag_name = tag to_post = {'uuid': uuid, 'tag': tag_name} response = self._prepare_request('POST', 'tags/removeTagFromObject', data=to_post) return self._check_json_response(response) - def build_complex_query(self, or_parameters: Optional[List[SearchType]] = None, - and_parameters: Optional[List[SearchType]] = None, - not_parameters: Optional[List[SearchType]] = None) -> Dict[str, List[SearchType]]: + def build_complex_query(self, or_parameters: list[SearchType] | None = None, + and_parameters: list[SearchType] | None = None, + not_parameters: list[SearchType] | None = None) -> dict[str, list[SearchType]]: '''Build a complex search query. MISP expects a dictionary with AND, OR and NOT keys.''' to_return = {} if and_parameters: @@ -3539,7 +3850,7 @@ class PyMISP: # ## MISP internal tasks ### - def get_all_functions(self, not_implemented: bool = False): + def get_all_functions(self, not_implemented: bool = False) -> list[str]: '''Get all methods available via the API, including ones that are not implemented.''' response = self._prepare_request('GET', 'servers/queryACL/printAllFunctionNames') functions = self._check_json_response(response) @@ -3556,12 +3867,12 @@ class PyMISP: paths.append(path) if not not_implemented: - return path + return [str(path)] with open(__file__) as f: content = f.read() - not_implemented_paths: List[str] = [] + not_implemented_paths: list[str] = [] for path in paths: if path not in content: not_implemented_paths.append(path) @@ -3570,7 +3881,7 @@ class PyMISP: # ## Internal methods ### - def _old_misp(self, minimal_version_required: tuple, removal_date: Union[str, date, datetime], method: Optional[str] = None, message: Optional[str] = None) -> bool: + def _old_misp(self, minimal_version_required: tuple[int], removal_date: str | date | datetime, method: str | None = None, message: str | None = None) -> bool: if self._misp_version >= minimal_version_required: return False if isinstance(removal_date, (datetime, date)): @@ -3581,13 +3892,13 @@ class PyMISP: warnings.warn(to_print, DeprecationWarning) return True - def _make_misp_bool(self, parameter: Optional[Union[bool, str]] = None) -> int: + def _make_misp_bool(self, parameter: bool | str | None = None) -> int: '''MISP wants 0 or 1 for bool, so we avoid True/False '0', '1' ''' if parameter is None: return 0 return 1 if int(parameter) else 0 - def _make_timestamp(self, value: Union[datetime, date, int, str, float, None]) -> Union[str, int, float, None]: + def _make_timestamp(self, value: datetime | date | int | str | float | None) -> str | int | float | None: '''Catch-all method to normalize anything that can be converted to a timestamp''' if not value: return None @@ -3608,11 +3919,12 @@ class PyMISP: return value return value - def _check_json_response(self, response: requests.Response) -> Dict: # type: ignore + def _check_json_response(self, response: requests.Response) -> dict[str, Any] | list[dict[str, Any]]: r = self._check_response(response, expect_json=True) if isinstance(r, (dict, list)): return r # Else: an exception was raised anyway + raise PyMISPUnexpectedResponse(f'A dict was expected, got a string: {r}') def _check_head_response(self, response: requests.Response) -> bool: if response.status_code == 200: @@ -3622,17 +3934,17 @@ class PyMISP: else: raise MISPServerError(f'Error code {response.status_code} for HEAD request') - def _check_response(self, response: requests.Response, lenient_response_type: bool = False, expect_json: bool = False) -> Union[Dict, str]: + def _check_response(self, response: requests.Response, lenient_response_type: bool = False, expect_json: bool = False) -> dict[str, Any] | str: """Check if the response from the server is not an unexpected error""" if response.status_code >= 500: - headers_without_auth = {i: response.request.headers[i] for i in response.request.headers if i != 'Authorization'} + headers_without_auth = {h_name: h_value for h_name, h_value in response.request.headers.items() if h_value != self.key} logger.critical(everything_broken.format(headers_without_auth, response.request.body, response.text)) raise MISPServerError(f'Error code 500:\n{response.text}') if 400 <= response.status_code < 500: # The server returns a json message with the error details try: - error_message = response.json() + error_message = loads(response.content) except Exception: raise MISPServerError(f'Error code {response.status_code}:\n{response.text}') @@ -3642,7 +3954,7 @@ class PyMISP: # At this point, we had no error. try: - response_json = response.json() + response_json = loads(response.content) logger.debug(response_json) if isinstance(response_json, dict) and response_json.get('response') is not None: # Cleanup. @@ -3661,25 +3973,31 @@ class PyMISP: return {'errors': 'The response is empty.'} return response.text - def __repr__(self): + def __repr__(self) -> str: return f'<{self.__class__.__name__}(url={self.root_url})' - def _prepare_request(self, request_type: str, url: str, data: Union[Iterable, Mapping, AbstractMISP, bytes] = {}, params: Mapping = {}, - kw_params: Mapping = {}, output_type: str = 'json', content_type: str = 'json') -> requests.Response: + def _prepare_request(self, request_type: str, url: str, data: Iterable[Any] | Mapping[str, Any] | AbstractMISP | bytes | None = None, + params: Mapping[str, Any] = {}, kw_params: Mapping[str, Any] = {}, + output_type: str = 'json', content_type: str = 'json') -> requests.Response: '''Prepare a request for python-requests''' if url[0] == '/': # strip it: it will fail if MISP is in a sub directory url = url[1:] + # Cake PHP being an idiot, it doesn't accept %20 (space) in the URL path, + # so we need to make it a + instead and hope for the best + url = url.replace(' ', '+') url = urljoin(self.root_url, url) - if data == {} or isinstance(data, bytes): - d = data - elif data: - if isinstance(data, dict): # Else, we can directly json encode. - # Remove None values. - data = {k: v for k, v in data.items() if v is not None} - d = json.dumps(data, default=pymisp_json_default) + d: bytes | str | None = None + if data is not None: + if isinstance(data, bytes): + d = data + else: + if isinstance(data, dict): + # Remove None values. + data = {k: v for k, v in data.items() if v is not None} + d = dumps(data, default=pymisp_json_default) - logger.debug(f'{request_type} - {url}') + logger.debug('%s - %s', request_type, url) if d is not None: logger.debug(d) @@ -3687,23 +4005,19 @@ class PyMISP: # CakePHP params in URL to_append_url = '/'.join([f'{k}:{v}' for k, v in kw_params.items()]) url = f'{url}/{to_append_url}' + req = requests.Request(request_type, url, data=d, params=params) - user_agent = f'PyMISP {__version__} - Python {".".join(str(x) for x in sys.version_info[:2])}' - if self.tool: - user_agent = f'{user_agent} - {self.tool}' req.auth = self.auth prepped = self.__session.prepare_request(req) prepped.headers.update( - {'Authorization': self.key, - 'Accept': f'application/{output_type}', - 'content-type': f'application/{content_type}', - 'User-Agent': user_agent}) + {'Accept': f'application/{output_type}', + 'content-type': f'application/{content_type}'}) logger.debug(prepped.headers) settings = self.__session.merge_environment_settings(req.url, proxies=self.proxies or {}, stream=None, verify=self.ssl, cert=self.cert) return self.__session.send(prepped, timeout=self.timeout, **settings) - def _csv_to_dict(self, csv_content: str) -> List[dict]: + def _csv_to_dict(self, csv_content: str) -> list[dict[str, Any]]: '''Makes a list of dict out of a csv file (requires headers)''' fieldnames, lines = csv_content.split('\n', 1) fields = fieldnames.split(',') diff --git a/pymisp/data/describeTypes.json b/pymisp/data/describeTypes.json index f30494b..fd91614 100644 --- a/pymisp/data/describeTypes.json +++ b/pymisp/data/describeTypes.json @@ -545,6 +545,10 @@ "default_category": "Other", "to_ids": 0 }, + "integer": { + "default_category": "Other", + "to_ids": 0 + }, "datetime": { "default_category": "Other", "to_ids": 0 @@ -891,6 +895,7 @@ "dns-soa-email", "size-in-bytes", "counter", + "integer", "datetime", "port", "ip-dst|port", @@ -1460,6 +1465,7 @@ "other", "size-in-bytes", "counter", + "integer", "datetime", "cpe", "port", @@ -1473,4 +1479,4 @@ ] } } -} +} \ No newline at end of file diff --git a/pymisp/data/misp-objects b/pymisp/data/misp-objects index 2787dc4..e3288ef 160000 --- a/pymisp/data/misp-objects +++ b/pymisp/data/misp-objects @@ -1 +1 @@ -Subproject commit 2787dc45d7efbf32e0fbe81ea95f0af642ae8963 +Subproject commit e3288ef6e516624e3e335939a2b7fe4aef5ce510 diff --git a/pymisp/exceptions.py b/pymisp/exceptions.py index 58d3a52..948ab12 100644 --- a/pymisp/exceptions.py +++ b/pymisp/exceptions.py @@ -1,9 +1,9 @@ -# -*- coding: utf-8 -*- +from __future__ import annotations class PyMISPError(Exception): - def __init__(self, message): - super(PyMISPError, self).__init__(message) + def __init__(self, message: str) -> None: + super().__init__(message) self.message = message @@ -23,6 +23,22 @@ class NewEventReportError(PyMISPError): pass +class NewAnalystDataError(PyMISPError): + pass + + +class NewNoteError(PyMISPError): + pass + + +class NewOpinionError(PyMISPError): + pass + + +class NewRelationshipError(PyMISPError): + pass + + class UpdateAttributeError(PyMISPError): pass @@ -51,17 +67,29 @@ class NoKey(PyMISPError): pass -class MISPObjectException(PyMISPError): - pass +class MISPAttributeException(PyMISPError): + """A base class for attribute specific exceptions""" +class MISPObjectException(PyMISPError): + """A base class for object specific exceptions""" + + +class InvalidMISPAttribute(MISPAttributeException): + """Exception raised when an attribute doesn't respect the constraints in the definition""" + +class InvalidMISPObjectAttribute(MISPAttributeException): + """Exception raised when an object attribute doesn't respect the constraints in the definition""" class InvalidMISPObject(MISPObjectException): - """Exception raised when an object doesn't respect the contrains in the definition""" - pass + """Exception raised when an object doesn't respect the constraints in the definition""" class UnknownMISPObjectTemplate(MISPObjectException): """Exception raised when the template is unknown""" + + + +class InvalidMISPGalaxy(PyMISPError): pass diff --git a/pymisp/mispevent.py b/pymisp/mispevent.py index 3bc9d13..e19f351 100644 --- a/pymisp/mispevent.py +++ b/pymisp/mispevent.py @@ -1,8 +1,7 @@ -# -*- coding: utf-8 -*- +from __future__ import annotations from datetime import timezone, datetime, date import copy -import json import os import base64 import sys @@ -14,63 +13,115 @@ from collections import defaultdict import logging import hashlib from pathlib import Path -from typing import List, Optional, Union, IO, Dict, Any +from typing import IO, Any, Sequence +import warnings + +try: + # orjson is optional dependency that speedups parsing and encoding JSON + import orjson as json # type: ignore +except ImportError: + import json from .abstract import AbstractMISP, MISPTag -from .exceptions import UnknownMISPObjectTemplate, InvalidMISPObject, PyMISPError, NewEventError, NewAttributeError, NewEventReportError, NewGalaxyClusterError, NewGalaxyClusterRelationError +from .exceptions import (NewNoteError, NewOpinionError, NewRelationshipError, UnknownMISPObjectTemplate, InvalidMISPGalaxy, InvalidMISPAttribute, + InvalidMISPObject, InvalidMISPObjectAttribute, PyMISPError, NewEventError, NewAttributeError, NewEventReportError, + NewGalaxyClusterError, NewGalaxyClusterRelationError, NewAnalystDataError) logger = logging.getLogger('pymisp') +class AnalystDataBehaviorMixin(AbstractMISP): + + # NOTE: edited here must be the property of Abstract MISP + + def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] + super().__init__(**kwargs) + self.uuid: str # Created in the child class + self._analyst_data_object_type: str # Must be defined in the child class + self.Note: list[MISPNote] = [] + self.Opinion: list[MISPOpinion] = [] + self.Relationship: list[MISPRelationship] = [] + + @property + def analyst_data_object_type(self) -> str: + return self._analyst_data_object_type + + @property + def notes(self) -> list[MISPNote]: + return self.Note + + @property + def opinions(self) -> list[MISPOpinion]: + return self.Opinion + + @property + def relationships(self) -> list[MISPRelationship]: + return self.Relationship + + def add_note(self, note: str, language: str | None = None, **kwargs) -> MISPNote: # type: ignore[no-untyped-def] + the_note = MISPNote() + the_note.from_dict(note=note, language=language, + object_uuid=self.uuid, object_type=self.analyst_data_object_type, + **kwargs) + self.notes.append(the_note) + self.edited = True + return the_note + + def add_opinion(self, opinion: int, comment: str | None = None, **kwargs) -> MISPNote: # type: ignore[no-untyped-def] + the_opinion = MISPOpinion() + the_opinion.from_dict(opinion=opinion, comment=comment, + object_uuid=self.uuid, object_type=self.analyst_data_object_type, + **kwargs) + self.opinions.append(the_opinion) + self.edited = True + return the_opinion + + def add_relationship(self, related_object_type: AbstractMISP | str, related_object_uuid: str | None, relationship_type: str, **kwargs) -> MISPNote: # type: ignore[no-untyped-def] + the_relationship = MISPRelationship() + the_relationship.from_dict(related_object_type=related_object_type, related_object_uuid=related_object_uuid, + relationship_type=relationship_type, + object_uuid=self.uuid, object_type=self.analyst_data_object_type, + **kwargs) + self.relationships.append(the_relationship) + self.edited = True + return the_relationship + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + # These members need a fully initialized class to be loaded properly + notes = kwargs.pop('Note', []) + opinions = kwargs.pop('Opinion', []) + relationships = kwargs.pop('Relationship', []) + super().from_dict(**kwargs) + for note in notes: + note.pop('object_uuid', None) + note.pop('object_type', None) + self.add_note(**note) + for opinion in opinions: + opinion.pop('object_uuid', None) + opinion.pop('object_type', None) + self.add_opinion(**opinion) + for relationship in relationships: + relationship.pop('object_uuid', None) + relationship.pop('object_type', None) + self.add_relationship(**relationship) + + try: from dateutil.parser import parse except ImportError: logger.exception("Cannot import dateutil") -try: - import jsonschema # type: ignore -except ImportError: - logger.exception("Cannot import jsonschema") -try: - # pyme renamed to gpg the 2016-10-28 - import gpg # type: ignore - from gpg.constants.sig import mode # type: ignore - has_pyme = True -except ImportError: - try: - # pyme renamed to gpg the 2016-10-28 - import pyme as gpg # type: ignore - from pyme.constants.sig import mode # type: ignore - has_pyme = True - except ImportError: - has_pyme = False - - -def _make_datetime(value) -> datetime: +def _make_datetime(value: int | float | str | datetime | date) -> datetime: if isinstance(value, (int, float)): # Timestamp value = datetime.fromtimestamp(value) elif isinstance(value, str): - if sys.version_info >= (3, 7): - try: - # faster - value = datetime.fromisoformat(value) - except Exception: - value = parse(value) - else: - try: - # faster - if '+' in value or value.find('-', 10) > -1: # date contains `-` char - value = datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f%z") - elif '.' in value: - value = datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f") - elif 'T' in value: - value = datetime.strptime(value, "%Y-%m-%dT%H:%M:%S") - else: - value = datetime.strptime(value, "%Y-%m-%d") - except Exception: - value = parse(value) + try: + # faster + value = datetime.fromisoformat(value) + except Exception: + value = parse(value) # type: ignore[arg-type] elif isinstance(value, datetime): pass elif isinstance(value, date): # NOTE: date has to be *after* datetime, or it will be overwritten @@ -84,7 +135,7 @@ def _make_datetime(value) -> datetime: return value -def make_bool(value: Optional[Union[bool, int, str, dict, list]]) -> bool: +def make_bool(value: bool | int | str | dict[str, Any] | list[Any] | None) -> bool: """Converts the supplied value to a boolean. :param value: Value to interpret as a boolean. An empty string, dict @@ -102,22 +153,22 @@ def make_bool(value: Optional[Union[bool, int, str, dict, list]]) -> bool: return False return True else: - raise PyMISPError('Unable to convert {} to a boolean.'.format(value)) + raise PyMISPError(f'Unable to convert {value} to a boolean.') class MISPOrganisation(AbstractMISP): - _fields_for_feed: set = {'name', 'uuid'} + _fields_for_feed: set[str] = {'name', 'uuid'} def __init__(self) -> None: super().__init__() self.id: int self.name: str - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'Organisation' in kwargs: kwargs = kwargs['Organisation'] - super(MISPOrganisation, self).from_dict(**kwargs) + super().from_dict(**kwargs) def __repr__(self) -> str: if hasattr(self, 'name'): @@ -126,14 +177,14 @@ class MISPOrganisation(AbstractMISP): class MISPSharingGroupOrg(AbstractMISP): - _fields_for_feed: set = {'extend', 'Organisation'} + _fields_for_feed: set[str] = {'extend', 'Organisation'} def __init__(self) -> None: super().__init__() self.extend: bool self.Organisation: MISPOrganisation - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'SharingGroupOrg' in kwargs: kwargs = kwargs['SharingGroupOrg'] if 'Organisation' in kwargs: @@ -146,38 +197,38 @@ class MISPSharingGroupOrg(AbstractMISP): return f'<{self.__class__.__name__}(Org={self.Organisation.name}, extend={self.extend})' return f'<{self.__class__.__name__}(NotInitialized)' - def _to_feed(self) -> Dict: + def _to_feed(self) -> dict[str, Any]: to_return = super()._to_feed() to_return['Organisation'] = self.Organisation._to_feed() return to_return class MISPSharingGroup(AbstractMISP): - _fields_for_feed: set = {'uuid', 'name', 'roaming', 'created', 'organisation_uuid', 'Organisation', 'SharingGroupOrg', 'SharingGroupServer'} + _fields_for_feed: set[str] = {'uuid', 'name', 'roaming', 'created', 'organisation_uuid', 'Organisation', 'SharingGroupOrg', 'SharingGroupServer'} def __init__(self) -> None: super().__init__() self.name: str - self.SharingGroupOrg: List[MISPSharingGroupOrg] = [] + self.SharingGroupOrg: list[MISPSharingGroupOrg] = [] @property - def sgorgs(self) -> List[MISPSharingGroupOrg]: + def sgorgs(self) -> list[MISPSharingGroupOrg]: return self.SharingGroupOrg @sgorgs.setter - def sgorgs(self, sgorgs: List[MISPSharingGroupOrg]): + def sgorgs(self, sgorgs: list[MISPSharingGroupOrg]) -> None: if all(isinstance(x, MISPSharingGroupOrg) for x in sgorgs): self.SharingGroupOrg = sgorgs else: raise PyMISPError('All the attributes have to be of type MISPSharingGroupOrg.') - def add_sgorg(self, sgorg): + def add_sgorg(self, sgorg: dict[str, Any]) -> MISPSharingGroupOrg: misp_sgorg = MISPSharingGroupOrg() misp_sgorg.from_dict(**sgorg) self.SharingGroupOrg.append(misp_sgorg) return misp_sgorg - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'SharingGroupOrg' in kwargs: [self.add_sgorg(sgorg) for sgorg in kwargs.pop('SharingGroupOrg')] if 'SharingGroup' in kwargs: @@ -189,7 +240,7 @@ class MISPSharingGroup(AbstractMISP): return f'<{self.__class__.__name__}(name={self.name})>' return f'<{self.__class__.__name__}(NotInitialized)>' - def _to_feed(self) -> Dict: + def _to_feed(self) -> dict[str, Any]: to_return = super()._to_feed() to_return['SharingGroupOrg'] = [sgorg._to_feed() for sgorg in self.SharingGroupOrg] to_return['Organisation'].pop('id', None) @@ -208,7 +259,7 @@ class MISPShadowAttribute(AbstractMISP): self.type: str self.value: str - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'ShadowAttribute' in kwargs: kwargs = kwargs['ShadowAttribute'] super().from_dict(**kwargs) @@ -226,7 +277,7 @@ class MISPSighting(AbstractMISP): self.id: int self.value: str - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] """Initialize the MISPSighting from a dictionary :param value: Value of the attribute the sighting is related too. Pushing this object @@ -239,7 +290,7 @@ class MISPSighting(AbstractMISP): """ if 'Sighting' in kwargs: kwargs = kwargs['Sighting'] - super(MISPSighting, self).from_dict(**kwargs) + super().from_dict(**kwargs) def __repr__(self) -> str: if hasattr(self, 'value'): @@ -248,15 +299,17 @@ class MISPSighting(AbstractMISP): return '<{self.__class__.__name__}(id={self.id})'.format(self=self) if hasattr(self, 'uuid'): return '<{self.__class__.__name__}(uuid={self.uuid})'.format(self=self) - return '<{self.__class__.__name__}(NotInitialized)'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' -class MISPAttribute(AbstractMISP): - _fields_for_feed: set = {'uuid', 'value', 'category', 'type', 'comment', 'data', - 'deleted', 'timestamp', 'to_ids', 'disable_correlation', - 'first_seen', 'last_seen'} +class MISPAttribute(AnalystDataBehaviorMixin): + _fields_for_feed: set[str] = {'uuid', 'value', 'category', 'type', 'comment', 'data', + 'deleted', 'timestamp', 'to_ids', 'disable_correlation', + 'first_seen', 'last_seen'} - def __init__(self, describe_types: Optional[Dict] = None, strict: bool = False): + _analyst_data_object_type = 'Attribute' + + def __init__(self, describe_types: dict[str, Any] | None = None, strict: bool = False): """Represents an Attribute :param describe_types: Use it if you want to overwrite the default describeTypes.json file (you don't) @@ -264,41 +317,66 @@ class MISPAttribute(AbstractMISP): """ super().__init__() if describe_types: - self.describe_types: Dict[str, Any] = describe_types - self.__categories: List[str] = self.describe_types['categories'] - self.__category_type_mapping: Dict[str, List[str]] = self.describe_types['category_type_mappings'] - self.__sane_default: Dict[str, Dict[str, Union[str, int]]] = self.describe_types['sane_defaults'] + self.describe_types: dict[str, Any] = describe_types + self.__categories: list[str] = self.describe_types['categories'] + self.__category_type_mapping: dict[str, list[str]] = self.describe_types['category_type_mappings'] + self.__sane_default: dict[str, dict[str, str | int]] = self.describe_types['sane_defaults'] self.__strict: bool = strict - self.data: Optional[BytesIO] = None + self.data: BytesIO | None = None self.first_seen: datetime self.last_seen: datetime self.uuid: str = str(uuid.uuid4()) - self.ShadowAttribute: List[MISPShadowAttribute] = [] + self.ShadowAttribute: list[MISPShadowAttribute] = [] self.SharingGroup: MISPSharingGroup - self.Sighting: List[MISPSighting] = [] - self.Tag: List[MISPTag] = [] + self.Sighting: list[MISPSighting] = [] + self.Tag: list[MISPTag] = [] + self.Galaxy: list[MISPGalaxy] = [] + + self.expand: str + self.timestamp: float | int | datetime # For search self.Event: MISPEvent - self.RelatedAttribute: List[MISPAttribute] + self.RelatedAttribute: list[MISPAttribute] # For malware sample - self._malware_binary: Optional[BytesIO] + self._malware_binary: BytesIO | None - def add_tag(self, tag: Optional[Union[str, MISPTag, Dict]] = None, **kwargs) -> MISPTag: + def add_tag(self, tag: str | MISPTag | dict[str, Any] | None = None, **kwargs) -> MISPTag: # type: ignore[no-untyped-def] return super()._add_tag(tag, **kwargs) @property - def tags(self) -> List[MISPTag]: + def tags(self) -> list[MISPTag]: """Returns a list of tags associated to this Attribute""" return self.Tag @tags.setter - def tags(self, tags: List[MISPTag]): + def tags(self, tags: list[MISPTag]) -> None: """Set a list of prepared MISPTag.""" super()._set_tags(tags) - def _prepare_data(self, data: Optional[Union[Path, str, bytes, BytesIO]]): + def add_galaxy(self, galaxy: MISPGalaxy | dict[str, Any] | None = None, **kwargs) -> MISPGalaxy: # type: ignore[no-untyped-def] + """Add a galaxy to the Attribute, either by passing a MISPGalaxy or a dictionary""" + if isinstance(galaxy, MISPGalaxy): + self.galaxies.append(galaxy) + return galaxy + if isinstance(galaxy, dict): + misp_galaxy = MISPGalaxy() + misp_galaxy.from_dict(**galaxy) + elif kwargs: + misp_galaxy = MISPGalaxy() + misp_galaxy.from_dict(**kwargs) + else: + raise InvalidMISPGalaxy("A Galaxy to add to an existing Attribute needs to be either a MISPGalaxy or a plain python dictionary") + self.galaxies.append(misp_galaxy) + return misp_galaxy + + @property + def galaxies(self) -> list[MISPGalaxy]: + """Returns a list of galaxies associated to this Attribute""" + return self.Galaxy + + def _prepare_data(self, data: Path | str | bytes | BytesIO | None) -> None: if not data: super().__setattr__('data', None) return @@ -330,7 +408,7 @@ class MISPAttribute(AbstractMISP): # not a encrypted zip file, assuming it is a new malware sample self._prepare_new_malware_sample() - def __setattr__(self, name: str, value: Any): + def __setattr__(self, name: str, value: Any) -> None: if name in ['first_seen', 'last_seen']: _datetime = _make_datetime(value) @@ -346,10 +424,10 @@ class MISPAttribute(AbstractMISP): else: super().__setattr__(name, value) - def hash_values(self, algorithm: str = 'sha512') -> List[str]: + def hash_values(self, algorithm: str = 'sha512') -> list[str]: """Compute the hash of every value for fast lookups""" if algorithm not in hashlib.algorithms_available: - raise PyMISPError('The algorithm {} is not available for hashing.'.format(algorithm)) + raise PyMISPError(f'The algorithm {algorithm} is not available for hashing.') if '|' in self.type or self.type == 'malware-sample': hashes = [] for v in self.value.split('|'): @@ -365,13 +443,13 @@ class MISPAttribute(AbstractMISP): h.update(to_encode.encode("utf-8")) return [h.hexdigest()] - def _set_default(self): + def _set_default(self) -> None: if not hasattr(self, 'comment'): self.comment = '' if not hasattr(self, 'timestamp'): self.timestamp = datetime.timestamp(datetime.now()) - def _to_feed(self, with_distribution=False) -> Dict: + def _to_feed(self, with_distribution: bool=False) -> dict[str, Any]: if with_distribution: self._fields_for_feed.add('distribution') to_return = super()._to_feed() @@ -387,12 +465,12 @@ class MISPAttribute(AbstractMISP): return to_return @property - def known_types(self) -> List[str]: + def known_types(self) -> list[str]: """Returns a list of all the known MISP attributes types""" return self.describe_types['types'] @property - def malware_binary(self) -> Optional[BytesIO]: + def malware_binary(self) -> BytesIO | None: """Returns a BytesIO of the malware, if the attribute has one. Decrypts, unpacks and caches the binary on the first invocation, which may require some time for large attachments (~1s/MB). @@ -414,11 +492,11 @@ class MISPAttribute(AbstractMISP): return None @property - def shadow_attributes(self) -> List[MISPShadowAttribute]: + def shadow_attributes(self) -> list[MISPShadowAttribute]: return self.ShadowAttribute @shadow_attributes.setter - def shadow_attributes(self, shadow_attributes: List[MISPShadowAttribute]): + def shadow_attributes(self, shadow_attributes: list[MISPShadowAttribute]) -> None: """Set a list of prepared MISPShadowAttribute.""" if all(isinstance(x, MISPShadowAttribute) for x in shadow_attributes): self.ShadowAttribute = shadow_attributes @@ -426,26 +504,26 @@ class MISPAttribute(AbstractMISP): raise PyMISPError('All the attributes have to be of type MISPShadowAttribute.') @property - def sightings(self) -> List[MISPSighting]: + def sightings(self) -> list[MISPSighting]: return self.Sighting @sightings.setter - def sightings(self, sightings: List[MISPSighting]): + def sightings(self, sightings: list[MISPSighting]) -> None: """Set a list of prepared MISPSighting.""" if all(isinstance(x, MISPSighting) for x in sightings): self.Sighting = sightings else: raise PyMISPError('All the attributes have to be of type MISPSighting.') - def delete(self): + def delete(self) -> None: """Mark the attribute as deleted (soft delete)""" self.deleted = True - def add_proposal(self, shadow_attribute=None, **kwargs) -> MISPShadowAttribute: + def add_proposal(self, shadow_attribute=None, **kwargs) -> MISPShadowAttribute: # type: ignore[no-untyped-def] """Alias for add_shadow_attribute""" return self.add_shadow_attribute(shadow_attribute, **kwargs) - def add_shadow_attribute(self, shadow_attribute: Optional[Union[MISPShadowAttribute, Dict]] = None, **kwargs) -> MISPShadowAttribute: + def add_shadow_attribute(self, shadow_attribute: MISPShadowAttribute | dict[str, Any] | None = None, **kwargs) -> MISPShadowAttribute: # type: ignore[no-untyped-def] """Add a shadow attribute to the attribute (by name or a MISPShadowAttribute object)""" if isinstance(shadow_attribute, MISPShadowAttribute): misp_shadow_attribute = shadow_attribute @@ -456,12 +534,12 @@ class MISPAttribute(AbstractMISP): misp_shadow_attribute = MISPShadowAttribute() misp_shadow_attribute.from_dict(**kwargs) else: - raise PyMISPError("The shadow_attribute is in an invalid format (can be either string, MISPShadowAttribute, or an expanded dict): {}".format(shadow_attribute)) + raise PyMISPError(f"The shadow_attribute is in an invalid format (can be either string, MISPShadowAttribute, or an expanded dict[str, Any]): {shadow_attribute}") self.shadow_attributes.append(misp_shadow_attribute) self.edited = True return misp_shadow_attribute - def add_sighting(self, sighting: Optional[Union[MISPSighting, dict]] = None, **kwargs) -> MISPSighting: + def add_sighting(self, sighting: MISPSighting | dict[str, Any] | None = None, **kwargs) -> MISPSighting: # type: ignore[no-untyped-def] """Add a sighting to the attribute (by name or a MISPSighting object)""" if isinstance(sighting, MISPSighting): misp_sighting = sighting @@ -472,12 +550,12 @@ class MISPAttribute(AbstractMISP): misp_sighting = MISPSighting() misp_sighting.from_dict(**kwargs) else: - raise PyMISPError("The sighting is in an invalid format (can be either string, MISPShadowAttribute, or an expanded dict): {}".format(sighting)) + raise PyMISPError(f"The sighting is in an invalid format (can be either string, MISPShadowAttribute, or an expanded dict[str, Any]): {sighting}") self.sightings.append(misp_sighting) self.edited = True return misp_sighting - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'Attribute' in kwargs: kwargs = kwargs['Attribute'] if kwargs.get('type') and kwargs.get('category'): @@ -530,13 +608,13 @@ class MISPAttribute(AbstractMISP): self.to_ids = make_bool(self.to_ids) if not isinstance(self.to_ids, bool): - raise NewAttributeError('{} is invalid, to_ids has to be True or False'.format(self.to_ids)) + raise NewAttributeError(f'{self.to_ids} is invalid, to_ids has to be True or False') self.distribution = kwargs.pop('distribution', None) if self.distribution is not None: self.distribution = int(self.distribution) if self.distribution not in [0, 1, 2, 3, 4, 5]: - raise NewAttributeError('{} is invalid, the distribution has to be in 0, 1, 2, 3, 4, 5'.format(self.distribution)) + raise NewAttributeError(f'{self.distribution} is invalid, the distribution has to be in 0, 1, 2, 3, 4, 5') # other possible values if kwargs.get('data'): @@ -555,10 +633,7 @@ class MISPAttribute(AbstractMISP): fs = kwargs.pop('first_seen') try: # Faster - if sys.version_info >= (3, 7): - self.first_seen = datetime.fromisoformat(fs) - else: - self.first_seen = datetime.strptime(fs, "%Y-%m-%dT%H:%M:%S.%f%z") + self.first_seen = datetime.fromisoformat(fs) except Exception: # Use __setattr__ self.first_seen = fs @@ -567,10 +642,7 @@ class MISPAttribute(AbstractMISP): ls = kwargs.pop('last_seen') try: # Faster - if sys.version_info >= (3, 7): - self.last_seen = datetime.fromisoformat(ls) - else: - self.last_seen = datetime.strptime(ls, "%Y-%m-%dT%H:%M:%S.%f%z") + self.last_seen = datetime.fromisoformat(ls) except Exception: # Use __setattr__ self.last_seen = ls @@ -584,10 +656,12 @@ class MISPAttribute(AbstractMISP): raise NewAttributeError('If the distribution is set to sharing group, a sharing group ID is required.') elif not self.sharing_group_id: # Cannot be None or 0 either. - raise NewAttributeError('If the distribution is set to sharing group, a sharing group ID is required (cannot be {}).'.format(self.sharing_group_id)) + raise NewAttributeError(f'If the distribution is set to sharing group, a sharing group ID is required (cannot be {self.sharing_group_id}).') if kwargs.get('Tag'): [self.add_tag(tag) for tag in kwargs.pop('Tag')] + if kwargs.get('Galaxy'): + [self.add_galaxy(galaxy) for galaxy in kwargs.pop('Galaxy')] if kwargs.get('Sighting'): [self.add_sighting(sighting) for sighting in kwargs.pop('Sighting')] if kwargs.get('ShadowAttribute'): @@ -603,16 +677,16 @@ class MISPAttribute(AbstractMISP): super().from_dict(**kwargs) - def to_dict(self, json_format: bool = False) -> Dict: + def to_dict(self, json_format: bool = False) -> dict[str, Any]: to_return = super().to_dict(json_format) if self.data: to_return['data'] = base64.b64encode(self.data.getvalue()).decode() return to_return - def _prepare_new_malware_sample(self): + def _prepare_new_malware_sample(self) -> None: if '|' in self.value: # Get the filename, ignore the md5, because humans. - self.malware_filename, md5 = self.value.split('|') + self.malware_filename, md5 = self.value.rsplit('|', 1) else: # Assuming the user only passed the filename self.malware_filename = self.value @@ -620,7 +694,7 @@ class MISPAttribute(AbstractMISP): self._malware_binary = self.data self.encrypt = True - def __is_misp_encrypted_file(self, f) -> bool: + def __is_misp_encrypted_file(self, f: ZipFile) -> bool: files_list = f.namelist() if len(files_list) != 2: return False @@ -635,48 +709,16 @@ class MISPAttribute(AbstractMISP): return False return True - def __repr__(self): + def __repr__(self) -> str: if hasattr(self, 'value'): return '<{self.__class__.__name__}(type={self.type}, value={self.value})'.format(self=self) - return '<{self.__class__.__name__}(NotInitialized)'.format(self=self) - - def verify(self, gpg_uid): # pragma: no cover - # Not used - if not has_pyme: - raise PyMISPError('pyme is required, please install: pip install --pre pyme3. You will also need libgpg-error-dev and libgpgme11-dev.') - signed_data = self._serialize() - with gpg.Context() as c: - keys = list(c.keylist(gpg_uid)) - try: - c.verify(signed_data, signature=base64.b64decode(self.sig), verify=keys[:1]) - return {self.uuid: True} - except Exception: - return {self.uuid: False} - - def _serialize(self): # pragma: no cover - # Not used - return '{type}{category}{to_ids}{uuid}{timestamp}{comment}{deleted}{value}'.format( - type=self.type, category=self.category, to_ids=self.to_ids, uuid=self.uuid, timestamp=self.timestamp, - comment=self.comment, deleted=self.deleted, value=self.value).encode() - - def sign(self, gpg_uid, passphrase=None): # pragma: no cover - # Not used - if not has_pyme: - raise PyMISPError('pyme is required, please install: pip install --pre pyme3. You will also need libgpg-error-dev and libgpgme11-dev.') - to_sign = self._serialize() - with gpg.Context() as c: - keys = list(c.keylist(gpg_uid)) - c.signers = keys[:1] - if passphrase: - c.set_passphrase_cb(lambda *args: passphrase) - signed, _ = c.sign(to_sign, mode=mode.DETACH) - self.sig = base64.b64encode(signed).decode() + return f'<{self.__class__.__name__}(NotInitialized)' class MISPObjectReference(AbstractMISP): - _fields_for_feed: set = {'uuid', 'timestamp', 'relationship_type', 'comment', - 'object_uuid', 'referenced_uuid'} + _fields_for_feed: set[str] = {'uuid', 'timestamp', 'relationship_type', 'comment', + 'object_uuid', 'referenced_uuid'} def __init__(self) -> None: super().__init__() @@ -685,30 +727,33 @@ class MISPObjectReference(AbstractMISP): self.referenced_uuid: str self.relationship_type: str - def _set_default(self): + def _set_default(self) -> None: if not hasattr(self, 'comment'): self.comment = '' if not hasattr(self, 'timestamp'): self.timestamp = datetime.timestamp(datetime.now()) - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'ObjectReference' in kwargs: kwargs = kwargs['ObjectReference'] - super(MISPObjectReference, self).from_dict(**kwargs) + super().from_dict(**kwargs) def __repr__(self) -> str: if hasattr(self, 'referenced_uuid') and hasattr(self, 'object_uuid'): return '<{self.__class__.__name__}(object_uuid={self.object_uuid}, referenced_uuid={self.referenced_uuid}, relationship_type={self.relationship_type})'.format(self=self) - return '<{self.__class__.__name__}(NotInitialized)'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' -class MISPObject(AbstractMISP): +class MISPObject(AnalystDataBehaviorMixin): - _fields_for_feed: set = {'name', 'meta-category', 'description', 'template_uuid', - 'template_version', 'uuid', 'timestamp', 'comment', - 'first_seen', 'last_seen', 'deleted'} + _fields_for_feed: set[str] = {'name', 'meta-category', 'description', 'template_uuid', + 'template_version', 'uuid', 'timestamp', 'comment', + 'first_seen', 'last_seen', 'deleted'} - def __init__(self, name: str, strict: bool = False, standalone: bool = True, default_attributes_parameters: Dict = {}, **kwargs): + _analyst_data_object_type = 'Object' + + def __init__(self, name: str, strict: bool = False, standalone: bool = True, # type: ignore[no-untyped-def] + default_attributes_parameters: dict[str, Any] = {}, **kwargs) -> None: ''' Master class representing a generic MISP object :param name: Name of the object @@ -724,7 +769,8 @@ class MISPObject(AbstractMISP): self.name: str = name self._known_template: bool = False self.id: int - self._definition: Optional[Dict] + self._definition: dict[str, Any] | None + self.timestamp: float | int | datetime misp_objects_template_custom = kwargs.pop('misp_objects_template_custom', None) misp_objects_path_custom = kwargs.pop('misp_objects_path_custom', None) @@ -737,12 +783,12 @@ class MISPObject(AbstractMISP): self.uuid: str = str(uuid.uuid4()) self.first_seen: datetime self.last_seen: datetime - self.__fast_attribute_access: dict = defaultdict(list) # Hashtable object_relation: [attributes] - self.ObjectReference: List[MISPObjectReference] = [] + self.__fast_attribute_access: dict[str, Any] = defaultdict(list) # Hashtable object_relation: [attributes] + self.ObjectReference: list[MISPObjectReference] = [] self._standalone: bool = False - self.Attribute: List[MISPObjectAttribute] = [] + self.Attribute: list[MISPObjectAttribute] = [] self.SharingGroup: MISPSharingGroup - self._default_attributes_parameters: dict + self._default_attributes_parameters: dict[str, Any] if isinstance(default_attributes_parameters, MISPAttribute): # Just make sure we're not modifying an existing MISPAttribute self._default_attributes_parameters = default_attributes_parameters.to_dict() @@ -769,7 +815,7 @@ class MISPObject(AbstractMISP): self.sharing_group_id = 0 self.standalone = standalone - def _load_template_path(self, template_path: Union[Path, str]) -> bool: + def _load_template_path(self, template_path: Path | str) -> bool: template = self._load_json(template_path) if not template: self._definition = None @@ -777,20 +823,20 @@ class MISPObject(AbstractMISP): self._load_template(template) return True - def _load_template(self, template: Dict) -> None: + def _load_template(self, template: dict[str, Any]) -> None: self._definition = template setattr(self, 'meta-category', self._definition['meta-category']) self.template_uuid = self._definition['uuid'] self.description = self._definition['description'] self.template_version = self._definition['version'] - def _set_default(self): + def _set_default(self) -> None: if not hasattr(self, 'comment'): self.comment = '' if not hasattr(self, 'timestamp'): self.timestamp = datetime.timestamp(datetime.now()) - def _to_feed(self, with_distribution=False) -> Dict: + def _to_feed(self, with_distribution: bool=False) -> dict[str, Any]: if with_distribution: self._fields_for_feed.add('distribution') if not hasattr(self, 'template_uuid'): # workaround for old events where the template_uuid was not yet mandatory @@ -799,7 +845,7 @@ class MISPObject(AbstractMISP): self.description = '' if not hasattr(self, 'meta-category'): # workaround for old events where meta-category is not always set setattr(self, 'meta-category', 'misc') - to_return = super(MISPObject, self)._to_feed() + to_return = super()._to_feed() if self.references: to_return['ObjectReference'] = [reference._to_feed() for reference in self.references] if with_distribution: @@ -809,7 +855,7 @@ class MISPObject(AbstractMISP): pass return to_return - def __setattr__(self, name, value): + def __setattr__(self, name: str, value: Any) -> None: if name in ['first_seen', 'last_seen']: value = _make_datetime(value) @@ -819,12 +865,12 @@ class MISPObject(AbstractMISP): logger.warning(f'first_seen ({value}) has to be before last_seen ({self.last_seen})') super().__setattr__(name, value) - def force_misp_objects_path_custom(self, misp_objects_path_custom: Union[Path, str], object_name: Optional[str] = None): + def force_misp_objects_path_custom(self, misp_objects_path_custom: Path | str, object_name: str | None = None) -> None: if object_name: self.name = object_name self._set_template(misp_objects_path_custom) - def _set_template(self, misp_objects_path_custom: Optional[Union[Path, str]] = None, misp_objects_template_custom: Optional[Dict] = None): + def _set_template(self, misp_objects_path_custom: Path | str | None = None, misp_objects_template_custom: dict[str, Any] | None = None) -> None: if misp_objects_template_custom: # A complete template was given to the constructor self._load_template(misp_objects_template_custom) @@ -841,26 +887,27 @@ class MISPObject(AbstractMISP): self._known_template = self._load_template_path(self.misp_objects_path / self.name / 'definition.json') if not self._known_template and self._strict: - raise UnknownMISPObjectTemplate('{} is unknown in the MISP object directory.'.format(self.name)) + raise UnknownMISPObjectTemplate(f'{self.name} is unknown in the MISP object directory.') else: # Then we have no meta-category, template_uuid, description and template_version pass - def delete(self): + def delete(self) -> None: """Mark the object as deleted (soft delete)""" self.deleted = True - [a.delete() for a in self.attributes] + for a in self.attributes: + a.delete() @property - def disable_validation(self): + def disable_validation(self) -> None: self._strict = False @property - def attributes(self) -> List['MISPObjectAttribute']: + def attributes(self) -> list[MISPObjectAttribute]: return self.Attribute @attributes.setter - def attributes(self, attributes: List['MISPObjectAttribute']): + def attributes(self, attributes: list[MISPObjectAttribute]) -> None: if all(isinstance(x, MISPObjectAttribute) for x in attributes): self.Attribute = attributes self.__fast_attribute_access = defaultdict(list) @@ -868,22 +915,22 @@ class MISPObject(AbstractMISP): raise PyMISPError('All the attributes have to be of type MISPObjectAttribute.') @property - def references(self) -> List[MISPObjectReference]: + def references(self) -> list[MISPObjectReference]: return self.ObjectReference @references.setter - def references(self, references: List[MISPObjectReference]): + def references(self, references: list[MISPObjectReference]) -> None: if all(isinstance(x, MISPObjectReference) for x in references): self.ObjectReference = references else: raise PyMISPError('All the attributes have to be of type MISPObjectReference.') @property - def standalone(self): + def standalone(self) -> bool: return self._standalone @standalone.setter - def standalone(self, new_standalone: bool): + def standalone(self, new_standalone: bool) -> None: if self._standalone != new_standalone: if new_standalone: self.update_not_jsonable("ObjectReference") @@ -893,7 +940,7 @@ class MISPObject(AbstractMISP): else: pass - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'Object' in kwargs: kwargs = kwargs['Object'] if self._known_template: @@ -916,7 +963,7 @@ class MISPObject(AbstractMISP): self.distribution = kwargs.pop('distribution') self.distribution = int(self.distribution) if self.distribution not in [0, 1, 2, 3, 4, 5]: - raise NewAttributeError('{} is invalid, the distribution has to be in 0, 1, 2, 3, 4, 5'.format(self.distribution)) + raise NewAttributeError(f'{self.distribution} is invalid, the distribution has to be in 0, 1, 2, 3, 4, 5') if kwargs.get('timestamp'): ts = kwargs.pop('timestamp') @@ -929,10 +976,7 @@ class MISPObject(AbstractMISP): fs = kwargs.pop('first_seen') try: # Faster - if sys.version_info >= (3, 7): - self.first_seen = datetime.fromisoformat(fs) - else: - self.first_seen = datetime.strptime(fs, "%Y-%m-%dT%H:%M:%S.%f%z") + self.first_seen = datetime.fromisoformat(fs) except Exception: # Use __setattr__ self.first_seen = fs @@ -941,10 +985,7 @@ class MISPObject(AbstractMISP): ls = kwargs.pop('last_seen') try: # Faster - if sys.version_info >= (3, 7): - self.last_seen = datetime.fromisoformat(ls) - else: - self.last_seen = datetime.strptime(ls, "%Y-%m-%dT%H:%M:%S.%f%z") + self.last_seen = datetime.fromisoformat(ls) except Exception: # Use __setattr__ self.last_seen = ls @@ -964,7 +1005,7 @@ class MISPObject(AbstractMISP): super().from_dict(**kwargs) - def add_reference(self, referenced_uuid: Union[AbstractMISP, str], relationship_type: str, comment: Optional[str] = None, **kwargs) -> MISPObjectReference: + def add_reference(self, referenced_uuid: AbstractMISP | str, relationship_type: str, comment: str | None = None, **kwargs) -> MISPObjectReference: # type: ignore[no-untyped-def] """Add a link (uuid) to another object""" if isinstance(referenced_uuid, AbstractMISP): # Allow to pass an object or an attribute instead of its UUID @@ -986,22 +1027,42 @@ class MISPObject(AbstractMISP): self.edited = True return reference - def get_attributes_by_relation(self, object_relation: str) -> List[MISPAttribute]: + def get_attribute_by_id(self, attribute_id: str | int) -> MISPObjectAttribute: + """Get an object attribute by ID + + :param attribute_id: The ID of the seeking object attribute""" + for attribute in self.attributes: + if hasattr(attribute, 'id') and attribute.id == attribute_id: + return attribute + + raise InvalidMISPObjectAttribute(f'Object attribute with {attribute_id} does not exist in this event') + + def get_attribute_by_uuid(self, attribute_uuid: str) -> MISPObjectAttribute: + """Get an object attribute by UUID + + :param attribute_uuid: The UUID of the seeking object attribute""" + for attribute in self.attributes: + if hasattr(attribute, 'uuid') and attribute.uuid == attribute_uuid: + return attribute + + raise InvalidMISPObjectAttribute(f'Object attribute with {attribute_uuid} does not exist in this event') + + def get_attributes_by_relation(self, object_relation: str) -> list[MISPAttribute]: '''Returns the list of attributes with the given object relation in the object''' return self._fast_attribute_access.get(object_relation, []) @property - def _fast_attribute_access(self) -> Dict: + def _fast_attribute_access(self) -> dict[str, Any]: if not self.__fast_attribute_access: for a in self.attributes: self.__fast_attribute_access[a.object_relation].append(a) return self.__fast_attribute_access - def has_attributes_by_relation(self, list_of_relations: List[str]) -> bool: + def has_attributes_by_relation(self, list_of_relations: list[str]) -> bool: '''True if all the relations in the list are defined in the object''' return all(relation in self._fast_attribute_access for relation in list_of_relations) - def add_attribute(self, object_relation: str, simple_value: Optional[Union[str, int, float]] = None, **value) -> Optional[MISPAttribute]: + def add_attribute(self, object_relation: str, simple_value: str | int | float | None = None, **value) -> MISPAttribute | None: # type: ignore[no-untyped-def] """Add an attribute. :param object_relation: The object relation of the attribute you're adding to the object :param simple_value: The value @@ -1015,7 +1076,7 @@ class MISPObject(AbstractMISP): if simple_value is not None: # /!\ The value *can* be 0 value['value'] = simple_value if value.get('value') is None: - logger.warning("The value of the attribute you're trying to add is None, skipping it. Object relation: {}".format(object_relation)) + logger.warning(f"The value of the attribute you're trying to add is None, skipping it. Object relation: {object_relation}") return None else: if isinstance(value['value'], bytes): @@ -1031,14 +1092,14 @@ class MISPObject(AbstractMISP): if isinstance(value['value'], str): value['value'] = value['value'].strip().strip('\x00') if value['value'] == '': - logger.warning("The value of the attribute you're trying to add is an empty string, skipping it. Object relation: {}".format(object_relation)) + logger.warning(f"The value of the attribute you're trying to add is an empty string, skipping it. Object relation: {object_relation}") return None if self._known_template and self._definition: if object_relation in self._definition['attributes']: attribute = MISPObjectAttribute(self._definition['attributes'][object_relation]) else: # Woopsie, this object_relation is unknown, no sane defaults for you. - logger.warning("The template ({}) doesn't have the object_relation ({}) you're trying to add. If you are creating a new event to push to MISP, please review your code so it matches the template.".format(self.name, object_relation)) + logger.warning(f"The template ({self.name}) doesn't have the object_relation ({object_relation}) you're trying to add. If you are creating a new event to push to MISP, please review your code so it matches the template.") attribute = MISPObjectAttribute({}) else: attribute = MISPObjectAttribute({}) @@ -1049,29 +1110,31 @@ class MISPObject(AbstractMISP): self.edited = True return attribute - def add_attributes(self, object_relation: str, *attributes) -> List[Optional[MISPAttribute]]: + def add_attributes(self, object_relation: str, *attributes: Sequence[str | dict[str, Any] | MISPAttribute]) -> list[MISPAttribute | None]: '''Add multiple attributes with the same object_relation. Helper for object_relation when multiple is True in the template. It is the same as calling multiple times add_attribute with the same object_relation. ''' to_return = [] for attribute in attributes: - if isinstance(attribute, dict): - a = self.add_attribute(object_relation, **attribute) + if isinstance(attribute, MISPAttribute): + a = self.add_attribute(object_relation, **attribute.to_dict()) + elif isinstance(attribute, dict): + a = self.add_attribute(object_relation, **attribute) # type: ignore[misc] else: a = self.add_attribute(object_relation, value=attribute) to_return.append(a) return to_return - def to_dict(self, json_format: bool = False, strict: bool = False) -> Dict: + def to_dict(self, json_format: bool = False, strict: bool = False) -> dict[str, Any]: if strict or self._strict and self._known_template: self._validate() - return super(MISPObject, self).to_dict(json_format) + return super().to_dict(json_format) - def to_json(self, sort_keys: bool = False, indent: Optional[int] = None, strict: bool = False): + def to_json(self, sort_keys: bool = False, indent: int | None = None, strict: bool = False) -> str: if strict or self._strict and self._known_template: self._validate() - return super(MISPObject, self).to_json(sort_keys=sort_keys, indent=indent) + return super().to_json(sort_keys=sort_keys, indent=indent) def _validate(self) -> bool: if not self._definition: @@ -1080,7 +1143,7 @@ class MISPObject(AbstractMISP): if self._definition.get('required'): required_missing = set(self._definition['required']) - set(self._fast_attribute_access.keys()) if required_missing: - raise InvalidMISPObject('{} are required.'.format(required_missing)) + raise InvalidMISPObject(f'{required_missing} are required.') if self._definition.get('requiredOneOf'): if not set(self._definition['requiredOneOf']) & set(self._fast_attribute_access.keys()): # We ecpect at least one of the object_relation in requiredOneOf, and it isn't the case @@ -1091,20 +1154,27 @@ class MISPObject(AbstractMISP): continue if not self._definition['attributes'][rel].get('multiple'): # object_relation's here more than once, but it isn't allowed in the template. - raise InvalidMISPObject('Multiple occurrences of {} is not allowed'.format(rel)) + raise InvalidMISPObject(f'Multiple occurrences of {rel} is not allowed') return True def __repr__(self) -> str: if hasattr(self, 'name'): return '<{self.__class__.__name__}(name={self.name})'.format(self=self) - return '<{self.__class__.__name__}(NotInitialized)'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' -class MISPEventReport(AbstractMISP): +class MISPEventReport(AnalystDataBehaviorMixin): - _fields_for_feed: set = {'uuid', 'name', 'content', 'timestamp', 'deleted'} + _fields_for_feed: set[str] = {'uuid', 'name', 'content', 'timestamp', 'deleted'} + _analyst_data_object_type = 'EventReport' - def from_dict(self, **kwargs): + timestamp: float | int | datetime + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.uuid: str = str(uuid.uuid4()) + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'EventReport' in kwargs: kwargs = kwargs['EventReport'] @@ -1112,7 +1182,7 @@ class MISPEventReport(AbstractMISP): if self.distribution is not None: self.distribution = int(self.distribution) if self.distribution not in [0, 1, 2, 3, 4, 5]: - raise NewEventReportError('{} is invalid, the distribution has to be in 0, 1, 2, 3, 4, 5'.format(self.distribution)) + raise NewEventReportError(f'{self.distribution} is invalid, the distribution has to be in 0, 1, 2, 3, 4, 5') if kwargs.get('sharing_group_id'): self.sharing_group_id = int(kwargs.pop('sharing_group_id')) @@ -1123,7 +1193,7 @@ class MISPEventReport(AbstractMISP): raise NewEventReportError('If the distribution is set to sharing group, a sharing group ID is required.') elif not self.sharing_group_id: # Cannot be None or 0 either. - raise NewEventReportError('If the distribution is set to sharing group, a sharing group ID is required (cannot be {}).'.format(self.sharing_group_id)) + raise NewEventReportError(f'If the distribution is set to sharing group, a sharing group ID is required (cannot be {self.sharing_group_id}).') self.name = kwargs.pop('name', None) if self.name is None: @@ -1151,9 +1221,9 @@ class MISPEventReport(AbstractMISP): def __repr__(self) -> str: if hasattr(self, 'name'): return '<{self.__class__.__name__}(name={self.name})'.format(self=self) - return '<{self.__class__.__name__}(NotInitialized)'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' - def _set_default(self): + def _set_default(self) -> None: if not hasattr(self, 'timestamp'): self.timestamp = datetime.timestamp(datetime.now()) if not hasattr(self, 'name'): @@ -1179,15 +1249,15 @@ class MISPGalaxyClusterElement(AbstractMISP): def __repr__(self) -> str: if hasattr(self, 'key') and hasattr(self, 'value'): return '<{self.__class__.__name__}(key={self.key}, value={self.value})'.format(self=self) - return '<{self.__class__.__name__}(NotInitialized)'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' - def __setattr__(self, key, value): + def __setattr__(self, key: str, value: Any) -> None: if key == "value" and isinstance(value, list): raise PyMISPError("You tried to set a list to a cluster element's value. " "Instead, create seperate elements for each value") super().__setattr__(key, value) - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if kwargs.get('id'): self.id = int(kwargs.pop('id')) if kwargs.get('galaxy_cluster_id'): @@ -1211,7 +1281,7 @@ class MISPGalaxyClusterRelation(AbstractMISP): def __repr__(self) -> str: if hasattr(self, "referenced_galaxy_cluster_type"): return '<{self.__class__.__name__}(referenced_galaxy_cluster_type={self.referenced_galaxy_cluster_type})'.format(self=self) - return '<{self.__class__.__name__}(NotInitialized)'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' def __init__(self) -> None: super().__init__() @@ -1219,9 +1289,9 @@ class MISPGalaxyClusterRelation(AbstractMISP): self.referenced_galaxy_cluster_uuid: str self.distribution: int = 0 self.referenced_galaxy_cluster_type: str - self.Tag: List[MISPTag] = [] + self.Tag: list[MISPTag] = [] - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] # Default values for a valid event to send to a MISP instance self.distribution = int(kwargs.pop('distribution', 0)) if self.distribution not in [0, 1, 2, 3, 4, 5]: @@ -1236,7 +1306,7 @@ class MISPGalaxyClusterRelation(AbstractMISP): raise NewGalaxyClusterRelationError('If the distribution is set to sharing group, a sharing group ID is required.') elif not self.sharing_group_id: # Cannot be None or 0 either. - raise NewGalaxyClusterRelationError('If the distribution is set to sharing group, a sharing group ID is required (cannot be {}).'.format(self.sharing_group_id)) + raise NewGalaxyClusterRelationError(f'If the distribution is set to sharing group, a sharing group ID is required (cannot be {self.sharing_group_id}).') if kwargs.get('id'): self.id = int(kwargs.pop('id')) @@ -1257,16 +1327,16 @@ class MISPGalaxyClusterRelation(AbstractMISP): self.SharingGroup.from_dict(**kwargs.pop('SharingGroup')) super().from_dict(**kwargs) - def add_tag(self, tag: Optional[Union[str, MISPTag, Dict]] = None, **kwargs) -> MISPTag: + def add_tag(self, tag: str | MISPTag | dict[str, Any] | None = None, **kwargs) -> MISPTag: # type: ignore[no-untyped-def] return super()._add_tag(tag, **kwargs) @property - def tags(self) -> List[MISPTag]: + def tags(self) -> list[MISPTag]: """Returns a list of tags associated to this Attribute""" return self.Tag @tags.setter - def tags(self, tags: List[MISPTag]): + def tags(self, tags: list[MISPTag]) -> None: """Set a list of prepared MISPTag.""" super()._set_tags(tags) @@ -1293,12 +1363,16 @@ class MISPGalaxyCluster(AbstractMISP): :type cluster_relations: list[MISPGalaxyClusterRelation], optional """ + id: int | str | None + tag_name: str + galaxy_id: str | None + def __init__(self) -> None: super().__init__() self.Galaxy: MISPGalaxy - self.GalaxyElement: List[MISPGalaxyClusterElement] = [] - self.meta: Dict = {} - self.GalaxyClusterRelation: List[MISPGalaxyClusterRelation] = [] + self.GalaxyElement: list[MISPGalaxyClusterElement] = [] + self.meta: dict[str, Any] = {} + self.GalaxyClusterRelation: list[MISPGalaxyClusterRelation] = [] self.Org: MISPOrganisation self.Orgc: MISPOrganisation self.SharingGroup: MISPSharingGroup @@ -1307,22 +1381,22 @@ class MISPGalaxyCluster(AbstractMISP): self.default = False @property - def cluster_elements(self) -> List[MISPGalaxyClusterElement]: + def cluster_elements(self) -> list[MISPGalaxyClusterElement]: return self.GalaxyElement @cluster_elements.setter - def cluster_elements(self, cluster_elements: List[MISPGalaxyClusterElement]): + def cluster_elements(self, cluster_elements: list[MISPGalaxyClusterElement]) -> None: self.GalaxyElement = cluster_elements @property - def cluster_relations(self) -> List[MISPGalaxyClusterRelation]: + def cluster_relations(self) -> list[MISPGalaxyClusterRelation]: return self.GalaxyClusterRelation @cluster_relations.setter - def cluster_relations(self, cluster_relations: List[MISPGalaxyClusterRelation]): + def cluster_relations(self, cluster_relations: list[MISPGalaxyClusterRelation]) -> None: self.GalaxyClusterRelation = cluster_relations - def parse_meta_as_elements(self): + def parse_meta_as_elements(self) -> None: """Function to parse the meta field into GalaxyClusterElements""" # Parse the cluster elements from the kwargs meta fields for key, value in self.meta.items(): @@ -1333,7 +1407,7 @@ class MISPGalaxyCluster(AbstractMISP): self.add_cluster_element(key=key, value=v) @property - def elements_meta(self) -> Dict: + def elements_meta(self) -> dict[str, Any]: """Function to return the galaxy cluster elements as a dictionary structure of lists that comes from a MISPGalaxy within a MISPEvent. Lossy, you lose the element ID """ @@ -1342,7 +1416,7 @@ class MISPGalaxyCluster(AbstractMISP): response[element.key].append(element.value) return dict(response) - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'GalaxyCluster' in kwargs: kwargs = kwargs['GalaxyCluster'] self.default = kwargs.pop('default', False) @@ -1368,7 +1442,7 @@ class MISPGalaxyCluster(AbstractMISP): raise NewGalaxyClusterError('If the distribution is set to sharing group, a sharing group ID is required.') elif not self.sharing_group_id: # Cannot be None or 0 either. - raise NewGalaxyClusterError('If the distribution is set to sharing group, a sharing group ID is required (cannot be {}).'.format(self.sharing_group_id)) + raise NewGalaxyClusterError(f'If the distribution is set to sharing group, a sharing group ID is required (cannot be {self.sharing_group_id}).') if 'uuid' in kwargs: self.uuid = kwargs.pop('uuid') @@ -1392,7 +1466,7 @@ class MISPGalaxyCluster(AbstractMISP): self.SharingGroup.from_dict(**kwargs.pop('SharingGroup')) super().from_dict(**kwargs) - def add_cluster_element(self, key: str, value: str, **kwargs) -> MISPGalaxyClusterElement: + def add_cluster_element(self, key: str, value: str, **kwargs) -> MISPGalaxyClusterElement: # type: ignore[no-untyped-def] """Add a cluster relation to a MISPGalaxyCluster, key and value are required :param key: The key name of the element @@ -1406,7 +1480,7 @@ class MISPGalaxyCluster(AbstractMISP): self.cluster_elements.append(cluster_element) return cluster_element - def add_cluster_relation(self, referenced_galaxy_cluster_uuid: Union["MISPGalaxyCluster", str, UUID], referenced_galaxy_cluster_type: str, galaxy_cluster_uuid: Optional[str] = None, **kwargs: Dict) -> MISPGalaxyClusterRelation: + def add_cluster_relation(self, referenced_galaxy_cluster_uuid: MISPGalaxyCluster | str | UUID, referenced_galaxy_cluster_type: str, galaxy_cluster_uuid: str | None = None, **kwargs: dict[str, Any]) -> MISPGalaxyClusterRelation: """Add a cluster relation to a MISPGalaxyCluster. :param referenced_galaxy_cluster_uuid: UUID of the related cluster @@ -1436,18 +1510,20 @@ class MISPGalaxyCluster(AbstractMISP): def __repr__(self) -> str: if hasattr(self, 'value'): return '<{self.__class__.__name__}(value={self.value})'.format(self=self) - return '<{self.__class__.__name__}(NotInitialized)'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' class MISPGalaxy(AbstractMISP): """Galaxy class, used to view a galaxy and respective clusters""" + id: str | None + def __init__(self) -> None: super().__init__() - self.GalaxyCluster: List[MISPGalaxyCluster] = [] + self.GalaxyCluster: list[MISPGalaxyCluster] = [] self.name: str - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] """Galaxy could be in one of the following formats: {'Galaxy': {}, 'GalaxyCluster': []} {'Galaxy': {'GalaxyCluster': []}} @@ -1462,10 +1538,10 @@ class MISPGalaxy(AbstractMISP): super().from_dict(**kwargs) @property - def clusters(self) -> List[MISPGalaxyCluster]: + def clusters(self) -> list[MISPGalaxyCluster]: return self.GalaxyCluster - def add_galaxy_cluster(self, **kwargs) -> MISPGalaxyCluster: + def add_galaxy_cluster(self, **kwargs) -> MISPGalaxyCluster: # type: ignore[no-untyped-def] """Add a MISP galaxy cluster into a MISPGalaxy. Supports all other parameters supported by MISPGalaxyCluster""" @@ -1477,15 +1553,17 @@ class MISPGalaxy(AbstractMISP): def __repr__(self) -> str: if hasattr(self, 'name'): return '<{self.__class__.__name__}(name={self.name})'.format(self=self) - return '<{self.__class__.__name__}(NotInitialized)'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' -class MISPEvent(AbstractMISP): +class MISPEvent(AnalystDataBehaviorMixin): - _fields_for_feed: set = {'uuid', 'info', 'threat_level_id', 'analysis', 'timestamp', - 'publish_timestamp', 'published', 'date', 'extends_uuid'} + _fields_for_feed: set[str] = {'uuid', 'info', 'threat_level_id', 'analysis', 'timestamp', + 'publish_timestamp', 'published', 'date', 'extends_uuid'} - def __init__(self, describe_types: Optional[Dict] = None, strict_validation: bool = False, **kwargs): + _analyst_data_object_type = 'Event' + + def __init__(self, describe_types: dict[str, Any] | None = None, strict_validation: bool = False, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(**kwargs) self.__schema_file = 'schema.json' if strict_validation else 'schema-lax.json' @@ -1495,29 +1573,32 @@ class MISPEvent(AbstractMISP): self.uuid: str = str(uuid.uuid4()) self.date: date - self.Attribute: List[MISPAttribute] = [] - self.Object: List[MISPObject] = [] - self.RelatedEvent: List[MISPEvent] = [] - self.ShadowAttribute: List[MISPShadowAttribute] = [] + self.Attribute: list[MISPAttribute] = [] + self.Object: list[MISPObject] = [] + self.RelatedEvent: list[MISPEvent] = [] + self.ShadowAttribute: list[MISPShadowAttribute] = [] self.SharingGroup: MISPSharingGroup - self.EventReport: List[MISPEventReport] = [] - self.Tag: List[MISPTag] = [] - self.Galaxy: List[MISPGalaxy] = [] + self.EventReport: list[MISPEventReport] = [] + self.Tag: list[MISPTag] = [] + self.Galaxy: list[MISPGalaxy] = [] - def add_tag(self, tag: Optional[Union[str, MISPTag, dict]] = None, **kwargs) -> MISPTag: + self.publish_timestamp: float | int | datetime + self.timestamp: float | int | datetime + + def add_tag(self, tag: str | MISPTag | dict[str, Any] | None = None, **kwargs) -> MISPTag: # type: ignore[no-untyped-def] return super()._add_tag(tag, **kwargs) @property - def tags(self) -> List[MISPTag]: + def tags(self) -> list[MISPTag]: """Returns a list of tags associated to this Event""" return self.Tag @tags.setter - def tags(self, tags: List[MISPTag]): + def tags(self, tags: list[MISPTag]) -> None: """Set a list of prepared MISPTag.""" super()._set_tags(tags) - def _set_default(self): + def _set_default(self) -> None: """There are a few keys that could, or need to be set by default for the feed generator""" if not hasattr(self, 'published'): self.published = True @@ -1539,7 +1620,7 @@ class MISPEvent(AbstractMISP): self.threat_level_id = 4 @property - def manifest(self) -> Dict: + def manifest(self) -> dict[str, Any]: required = ['info', 'Orgc'] for r in required: if not hasattr(self, r): @@ -1559,8 +1640,8 @@ class MISPEvent(AbstractMISP): } } - def attributes_hashes(self, algorithm: str = 'sha512') -> List[str]: - to_return: List[str] = [] + def attributes_hashes(self, algorithm: str = 'sha512') -> list[str]: + to_return: list[str] = [] for attribute in self.attributes: to_return += attribute.hash_values(algorithm) for obj in self.objects: @@ -1568,12 +1649,13 @@ class MISPEvent(AbstractMISP): to_return += attribute.hash_values(algorithm) return to_return - def to_feed(self, valid_distributions: List[int] = [0, 1, 2, 3, 4, 5], with_meta: bool = False, with_distribution=False, with_local_tags: bool = True) -> Dict: + def to_feed(self, valid_distributions: list[int] = [0, 1, 2, 3, 4, 5], with_meta: bool = False, with_distribution: bool=False, with_local_tags: bool = True, with_event_reports: bool = True) -> dict[str, Any]: """ Generate a json output for MISP Feed. :param valid_distributions: only makes sense if the distribution key is set; i.e., the event is exported from a MISP instance. :param with_distribution: exports distribution and Sharing Group info; otherwise all SharingGroup information is discarded (protecting privacy) :param with_local_tags: tag export includes local exportable tags along with global exportable tags + :param with_event_reports: include event reports in the returned MISP event """ required = ['info', 'Orgc'] for r in required: @@ -1627,10 +1709,21 @@ class MISPEvent(AbstractMISP): except AttributeError: pass + if with_event_reports and self.event_reports: + to_return['EventReport'] = [] + for event_report in self.event_reports: + if (valid_distributions and event_report.get('distribution') is not None and event_report.distribution not in valid_distributions): + continue + if not with_distribution: + event_report.pop('distribution', None) + event_report.pop('SharingGroup', None) + event_report.pop('sharing_group_id', None) + to_return['EventReport'].append(event_report.to_dict()) + return {'Event': to_return} @property - def known_types(self) -> List[str]: + def known_types(self) -> list[str]: return self.describe_types['types'] @property @@ -1642,68 +1735,75 @@ class MISPEvent(AbstractMISP): return self.Orgc @orgc.setter - def orgc(self, orgc: MISPOrganisation): + def orgc(self, orgc: MISPOrganisation) -> None: if isinstance(orgc, MISPOrganisation): self.Orgc = orgc else: raise PyMISPError('Orgc must be of type MISPOrganisation.') @property - def attributes(self) -> List[MISPAttribute]: + def attributes(self) -> list[MISPAttribute]: return self.Attribute @attributes.setter - def attributes(self, attributes: List[MISPAttribute]): + def attributes(self, attributes: list[MISPAttribute]) -> None: if all(isinstance(x, MISPAttribute) for x in attributes): self.Attribute = attributes else: raise PyMISPError('All the attributes have to be of type MISPAttribute.') @property - def event_reports(self) -> List[MISPEventReport]: + def event_reports(self) -> list[MISPEventReport]: return self.EventReport @property - def shadow_attributes(self) -> List[MISPShadowAttribute]: + def shadow_attributes(self) -> list[MISPShadowAttribute]: return self.ShadowAttribute @shadow_attributes.setter - def shadow_attributes(self, shadow_attributes: List[MISPShadowAttribute]): + def shadow_attributes(self, shadow_attributes: list[MISPShadowAttribute]) -> None: if all(isinstance(x, MISPShadowAttribute) for x in shadow_attributes): self.ShadowAttribute = shadow_attributes else: raise PyMISPError('All the attributes have to be of type MISPShadowAttribute.') @property - def related_events(self) -> List['MISPEvent']: + def related_events(self) -> list[MISPEvent]: return self.RelatedEvent @property - def galaxies(self) -> List[MISPGalaxy]: + def galaxies(self) -> list[MISPGalaxy]: return self.Galaxy + @galaxies.setter + def galaxies(self, galaxies: list[MISPGalaxy]) -> None: + if all(isinstance(x, MISPGalaxy) for x in galaxies): + self.Galaxy = galaxies + else: + raise PyMISPError('All the attributes have to be of type MISPGalaxy.') + @property - def objects(self) -> List[MISPObject]: + def objects(self) -> list[MISPObject]: return self.Object @objects.setter - def objects(self, objects: List[MISPObject]): + def objects(self, objects: list[MISPObject]) -> None: if all(isinstance(x, MISPObject) for x in objects): self.Object = objects else: raise PyMISPError('All the attributes have to be of type MISPObject.') - def load_file(self, event_path: Union[Path, str], validate: bool = False, metadata_only: bool = False): + def load_file(self, event_path: Path | str, validate: bool = False, metadata_only: bool = False) -> None: """Load a JSON dump from a file on the disk""" if not os.path.exists(event_path): raise PyMISPError('Invalid path, unable to load the event.') with open(event_path, 'rb') as f: self.load(f, validate, metadata_only) - def load(self, json_event: Union[IO, str, bytes, dict], validate: bool = False, metadata_only: bool = False): + def load(self, json_event: IO[bytes] | IO[str] | str | bytes | dict[str, Any], validate: bool = False, metadata_only: bool = False) -> None: """Load a JSON dump from a pseudo file or a JSON string""" if isinstance(json_event, (BufferedIOBase, TextIOBase)): - json_event = json_event.read() # type: ignore + json_event = json_event.read() if isinstance(json_event, (str, bytes)): json_event = json.loads(json_event) @@ -1719,21 +1819,17 @@ class MISPEvent(AbstractMISP): event.pop('Object', None) self.from_dict(**event) if validate: - json_schema = self._load_json(self.resources_path / self.__schema_file) - jsonschema.validate({"Event": self.jsonable()}, json_schema) + warnings.warn('The validate parameter is deprecated because PyMISP is more flexible at loading event than the schema') - def __setattr__(self, name, value): + def __setattr__(self, name: str, value: Any) -> None: if name in ['date']: if isinstance(value, date): pass elif isinstance(value, str): - if sys.version_info >= (3, 7): - try: - # faster - value = date.fromisoformat(value) - except Exception: - value = parse(value).date() - else: + try: + # faster + value = date.fromisoformat(value) + except Exception: value = parse(value).date() elif isinstance(value, (int, float)): value = date.fromtimestamp(value) @@ -1743,7 +1839,7 @@ class MISPEvent(AbstractMISP): raise NewEventError(f'Invalid format for the date: {type(value)} - {value}') super().__setattr__(name, value) - def set_date(self, d: Optional[Union[str, int, float, datetime, date]] = None, ignore_invalid: bool = False): + def set_date(self, d: str | int | float | datetime | date | None = None, ignore_invalid: bool = False) -> None: """Set a date for the event :param d: String, datetime, or date object @@ -1756,7 +1852,7 @@ class MISPEvent(AbstractMISP): else: raise NewEventError(f'Invalid format for the date: {type(d)} - {d}') - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'Event' in kwargs: kwargs = kwargs['Event'] # Required value @@ -1815,7 +1911,7 @@ class MISPEvent(AbstractMISP): for rel_event in kwargs.pop('RelatedEvent'): sub_event = MISPEvent() sub_event.load(rel_event) - self.RelatedEvent.append({'Event': sub_event}) + self.RelatedEvent.append({'Event': sub_event}) # type: ignore[arg-type] if kwargs.get('Tag'): [self.add_tag(tag) for tag in kwargs.pop('Tag')] if kwargs.get('Object'): @@ -1830,9 +1926,9 @@ class MISPEvent(AbstractMISP): self.SharingGroup = MISPSharingGroup() self.SharingGroup.from_dict(**kwargs.pop('SharingGroup')) - super(MISPEvent, self).from_dict(**kwargs) + super().from_dict(**kwargs) - def to_dict(self, json_format: bool = False) -> Dict: + def to_dict(self, json_format: bool = False) -> dict[str, Any]: to_return = super().to_dict(json_format) if to_return.get('date'): @@ -1840,17 +1936,17 @@ class MISPEvent(AbstractMISP): self.date = self.date.date() to_return['date'] = self.date.isoformat() if to_return.get('publish_timestamp'): - to_return['publish_timestamp'] = self._datetime_to_timestamp(self.publish_timestamp) + to_return['publish_timestamp'] = str(self._datetime_to_timestamp(self.publish_timestamp)) if to_return.get('sighting_timestamp'): - to_return['sighting_timestamp'] = self._datetime_to_timestamp(self.sighting_timestamp) + to_return['sighting_timestamp'] = str(self._datetime_to_timestamp(self.sighting_timestamp)) return to_return - def add_proposal(self, shadow_attribute=None, **kwargs) -> MISPShadowAttribute: + def add_proposal(self, shadow_attribute=None, **kwargs) -> MISPShadowAttribute: # type: ignore[no-untyped-def] """Alias for add_shadow_attribute""" return self.add_shadow_attribute(shadow_attribute, **kwargs) - def add_shadow_attribute(self, shadow_attribute=None, **kwargs) -> MISPShadowAttribute: + def add_shadow_attribute(self, shadow_attribute=None, **kwargs) -> MISPShadowAttribute: # type: ignore[no-untyped-def] """Add a tag to the attribute (by name or a MISPTag object)""" if isinstance(shadow_attribute, MISPShadowAttribute): misp_shadow_attribute = shadow_attribute @@ -1861,26 +1957,26 @@ class MISPEvent(AbstractMISP): misp_shadow_attribute = MISPShadowAttribute() misp_shadow_attribute.from_dict(**kwargs) else: - raise PyMISPError("The shadow_attribute is in an invalid format (can be either string, MISPShadowAttribute, or an expanded dict): {}".format(shadow_attribute)) + raise PyMISPError(f"The shadow_attribute is in an invalid format (can be either string, MISPShadowAttribute, or an expanded dict[str, Any]): {shadow_attribute}") self.shadow_attributes.append(misp_shadow_attribute) self.edited = True return misp_shadow_attribute - def get_attribute_tag(self, attribute_identifier: str) -> List[MISPTag]: + def get_attribute_tag(self, attribute_identifier: str) -> list[MISPTag]: """Return the tags associated to an attribute or an object attribute. :param attribute_identifier: can be an ID, UUID, or the value. """ - tags: List[MISPTag] = [] + tags: list[MISPTag] = [] for a in self.attributes + [attribute for o in self.objects for attribute in o.attributes]: - if ((hasattr(a, 'id') and a.id == attribute_identifier) + if ((hasattr(a, 'id') and str(a.id) == attribute_identifier) or (hasattr(a, 'uuid') and a.uuid == attribute_identifier) or (hasattr(a, 'value') and attribute_identifier == a.value or (isinstance(a.value, str) and attribute_identifier in a.value.split('|')))): tags += a.tags return tags - def add_attribute_tag(self, tag: Union[MISPTag, str], attribute_identifier: str) -> List[MISPAttribute]: + def add_attribute_tag(self, tag: MISPTag | str, attribute_identifier: str) -> list[MISPAttribute]: """Add a tag to an existing attribute. Raise an Exception if the attribute doesn't exist. :param tag: Tag name as a string, MISPTag instance, or dictionary @@ -1888,7 +1984,7 @@ class MISPEvent(AbstractMISP): """ attributes = [] for a in self.attributes + [attribute for o in self.objects for attribute in o.attributes]: - if ((hasattr(a, 'id') and a.id == attribute_identifier) + if ((hasattr(a, 'id') and str(a.id) == attribute_identifier) or (hasattr(a, 'uuid') and a.uuid == attribute_identifier) or (hasattr(a, 'value') and attribute_identifier == a.value or (isinstance(a.value, str) and attribute_identifier in a.value.split('|')))): @@ -1896,35 +1992,35 @@ class MISPEvent(AbstractMISP): attributes.append(a) if not attributes: - raise PyMISPError('No attribute with identifier {} found.'.format(attribute_identifier)) + raise PyMISPError(f'No attribute with identifier {attribute_identifier} found.') self.edited = True return attributes - def publish(self): + def publish(self) -> None: """Mark the attribute as published""" self.published = True - def unpublish(self): + def unpublish(self) -> None: """Mark the attribute as un-published (set publish flag to false)""" self.published = False - def delete_attribute(self, attribute_id: str): + def delete_attribute(self, attribute_id: str) -> None: """Delete an attribute :param attribute_id: ID or UUID """ for a in self.attributes: - if ((hasattr(a, 'id') and a.id == attribute_id) + if ((hasattr(a, 'id') and str(a.id) == attribute_id) or (hasattr(a, 'uuid') and a.uuid == attribute_id)): a.delete() break else: - raise PyMISPError('No attribute with UUID/ID {} found.'.format(attribute_id)) + raise PyMISPError(f'No attribute with UUID/ID {attribute_id} found.') - def add_attribute(self, type: str, value: Union[str, int, float], **kwargs) -> Union[MISPAttribute, List[MISPAttribute]]: + def add_attribute(self, type: str, value: str | int | float, **kwargs) -> MISPAttribute | list[MISPAttribute]: # type: ignore[no-untyped-def] """Add an attribute. type and value are required but you can pass all other parameters supported by MISPAttribute""" - attr_list: List[MISPAttribute] = [] + attr_list: list[MISPAttribute] = [] if isinstance(value, list): attr_list = [self.add_attribute(type=type, value=a, **kwargs) for a in value] else: @@ -1936,7 +2032,7 @@ class MISPEvent(AbstractMISP): return attr_list return attribute - def add_event_report(self, name: str, content: str, **kwargs) -> MISPEventReport: + def add_event_report(self, name: str, content: str, **kwargs) -> MISPEventReport: # type: ignore[no-untyped-def] """Add an event report. name and value are requred but you can pass all other parameters supported by MISPEventReport""" event_report = MISPEventReport() @@ -1945,22 +2041,51 @@ class MISPEvent(AbstractMISP): self.edited = True return event_report - def add_galaxy(self, **kwargs) -> MISPGalaxy: - """Add a MISP galaxy and sub-clusters into an event. + def add_galaxy(self, galaxy: MISPGalaxy | dict[str, Any] | None = None, **kwargs) -> MISPGalaxy: # type: ignore[no-untyped-def] + """Add a galaxy and sub-clusters into an event, either by passing + a MISPGalaxy or a dictionary. Supports all other parameters supported by MISPGalaxy""" - galaxy = MISPGalaxy() - galaxy.from_dict(**kwargs) - self.galaxies.append(galaxy) - return galaxy + if isinstance(galaxy, MISPGalaxy): + self.galaxies.append(galaxy) + return galaxy + if isinstance(galaxy, dict): + misp_galaxy = MISPGalaxy() + misp_galaxy.from_dict(**galaxy) + elif kwargs: + misp_galaxy = MISPGalaxy() + misp_galaxy.from_dict(**kwargs) + else: + raise InvalidMISPGalaxy("A Galaxy to add to an existing Event needs to be either a MISPGalaxy or a plain python dictionary") + self.galaxies.append(misp_galaxy) + return misp_galaxy - def get_object_by_id(self, object_id: Union[str, int]) -> MISPObject: + def get_attribute_by_id(self, attribute_id: str | int) -> MISPAttribute: + """Get an attribute by ID + + :param attribute_id: The ID of the seeking attribute""" + for attribute in self.attributes: + if hasattr(attribute, 'id') and int(attribute.id) == int(attribute_id): + return attribute + raise InvalidMISPAttribute(f'Attribute with {attribute_id} does not exist in this event') + + def get_attribute_by_uuid(self, attribute_uuid: str) -> MISPAttribute: + """Get an attribute by UUID + + :param attribute_uuid: The UUID of the seeking attribute""" + for attribute in self.attributes: + if hasattr(attribute, 'uuid') and attribute.uuid == attribute_uuid: + return attribute + + raise InvalidMISPAttribute(f'Attribute with {attribute_uuid} does not exist in this event') + + def get_object_by_id(self, object_id: str | int) -> MISPObject: """Get an object by ID :param object_id: the ID is the one set by the server when creating the new object""" for obj in self.objects: if hasattr(obj, 'id') and int(obj.id) == int(object_id): return obj - raise InvalidMISPObject('Object with {} does not exist in this event'.format(object_id)) + raise InvalidMISPObject(f'Object with {object_id} does not exist in this event') def get_object_by_uuid(self, object_uuid: str) -> MISPObject: """Get an object by UUID @@ -1969,9 +2094,9 @@ class MISPEvent(AbstractMISP): for obj in self.objects: if hasattr(obj, 'uuid') and obj.uuid == object_uuid: return obj - raise InvalidMISPObject('Object with {} does not exist in this event'.format(object_uuid)) + raise InvalidMISPObject(f'Object with {object_uuid} does not exist in this event') - def get_objects_by_name(self, object_name: str) -> List[MISPObject]: + def get_objects_by_name(self, object_name: str) -> list[MISPObject]: """Get objects by name :param object_name: name is set by the server when creating the new object""" @@ -1981,7 +2106,7 @@ class MISPEvent(AbstractMISP): objects.append(obj) return objects - def add_object(self, obj: Union[MISPObject, dict, None] = None, **kwargs) -> MISPObject: + def add_object(self, obj: MISPObject | dict[str, Any] | None = None, **kwargs) -> MISPObject: # type: ignore[no-untyped-def] """Add an object to the Event, either by passing a MISPObject, or a dictionary""" if isinstance(obj, MISPObject): misp_obj = obj @@ -2002,20 +2127,20 @@ class MISPEvent(AbstractMISP): self.edited = True return misp_obj - def delete_object(self, object_id: str): + def delete_object(self, object_id: str) -> None: """Delete an object :param object_id: ID or UUID """ for o in self.objects: - if ((hasattr(o, 'id') and o.id == object_id) + if ((hasattr(o, 'id') and object_id.isdigit() and int(o.id) == int(object_id)) or (hasattr(o, 'uuid') and o.uuid == object_id)): o.delete() break else: - raise PyMISPError('No object with UUID/ID {} found.'.format(object_id)) + raise PyMISPError(f'No object with UUID/ID {object_id} found.') - def run_expansions(self): + def run_expansions(self) -> None: for index, attribute in enumerate(self.attributes): if 'expand' not in attribute: continue @@ -2025,7 +2150,7 @@ class MISPEvent(AbstractMISP): try: from .tools import make_binary_objects except ImportError as e: - logger.info('Unable to load make_binary_objects: {}'.format(e)) + logger.info(f'Unable to load make_binary_objects: {e}') continue file_object, bin_type_object, bin_section_objects = make_binary_objects(pseudofile=attribute.malware_binary, filename=attribute.malware_filename) self.add_object(file_object) @@ -2036,107 +2161,52 @@ class MISPEvent(AbstractMISP): self.add_object(bin_section_object) self.attributes.pop(index) else: - logger.warning('No expansions for this data type ({}). Open an issue if needed.'.format(attribute.type)) + logger.warning(f'No expansions for this data type ({attribute.type}). Open an issue if needed.') def __repr__(self) -> str: if hasattr(self, 'info'): return '<{self.__class__.__name__}(info={self.info})'.format(self=self) - return '<{self.__class__.__name__}(NotInitialized)'.format(self=self) - - def _serialize(self): # pragma: no cover - return '{date}{threat_level_id}{info}{uuid}{analysis}{timestamp}'.format( - date=self.date, threat_level_id=self.threat_level_id, info=self.info, - uuid=self.uuid, analysis=self.analysis, timestamp=self.timestamp).encode() - - def _serialize_sigs(self): # pragma: no cover - # Not used - all_sigs = self.sig - for a in self.attributes: - all_sigs += a.sig - return all_sigs.encode() - - def sign(self, gpg_uid, passphrase=None): # pragma: no cover - # Not used - if not has_pyme: - raise PyMISPError('pyme is required, please install: pip install --pre pyme3. You will also need libgpg-error-dev and libgpgme11-dev.') - to_sign = self._serialize() - with gpg.Context() as c: - keys = list(c.keylist(gpg_uid)) - c.signers = keys[:1] - if passphrase: - c.set_passphrase_cb(lambda *args: passphrase) - signed, _ = c.sign(to_sign, mode=mode.DETACH) - self.sig = base64.b64encode(signed).decode() - for a in self.attributes: - a.sign(gpg_uid, passphrase) - to_sign_global = self._serialize_sigs() - with gpg.Context() as c: - keys = list(c.keylist(gpg_uid)) - c.signers = keys[:1] - if passphrase: - c.set_passphrase_cb(lambda *args: passphrase) - signed, _ = c.sign(to_sign_global, mode=mode.DETACH) - self.global_sig = base64.b64encode(signed).decode() - - def verify(self, gpg_uid): # pragma: no cover - # Not used - if not has_pyme: - raise PyMISPError('pyme is required, please install: pip install --pre pyme3. You will also need libgpg-error-dev and libgpgme11-dev.') - to_return = {} - signed_data = self._serialize() - with gpg.Context() as c: - keys = list(c.keylist(gpg_uid)) - try: - c.verify(signed_data, signature=base64.b64decode(self.sig), verify=keys[:1]) - to_return[self.uuid] = True - except Exception: - to_return[self.uuid] = False - for a in self.attributes: - to_return.update(a.verify(gpg_uid)) - to_verify_global = self._serialize_sigs() - with gpg.Context() as c: - keys = list(c.keylist(gpg_uid)) - try: - c.verify(to_verify_global, signature=base64.b64decode(self.global_sig), verify=keys[:1]) - to_return['global'] = True - except Exception: - to_return['global'] = False - return to_return + return f'<{self.__class__.__name__}(NotInitialized)' class MISPObjectTemplate(AbstractMISP): - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'ObjectTemplate' in kwargs: kwargs = kwargs['ObjectTemplate'] super().from_dict(**kwargs) def __repr__(self) -> str: - return '<{self.__class__.__name__}(self.name)'.format(self=self) + return f'<{self.__class__.__name__}(self.name)' class MISPUser(AbstractMISP): - def __init__(self, **kwargs: Dict) -> None: + authkey: str + + def __init__(self, **kwargs: dict[str, Any]) -> None: super().__init__(**kwargs) self.email: str + self.password: str | None - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'User' in kwargs: kwargs = kwargs['User'] super().from_dict(**kwargs) - if hasattr(self, 'password') and set(self.password) == set(['*']): + if hasattr(self, 'password') and self.password and set(self.password) == {'*', }: self.password = None def __repr__(self) -> str: if hasattr(self, 'email'): return '<{self.__class__.__name__}(email={self.email})'.format(self=self) - return '<{self.__class__.__name__}(NotInitialized)'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' class MISPFeed(AbstractMISP): - def from_dict(self, **kwargs): + settings: str + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'Feed' in kwargs: kwargs = kwargs['Feed'] super().from_dict(**kwargs) @@ -2144,13 +2214,13 @@ class MISPFeed(AbstractMISP): try: self.settings = json.loads(self.settings) except json.decoder.JSONDecodeError as e: - logger.error("Failed to parse feed settings: {}".format(self.settings)) + logger.error(f"Failed to parse feed settings: {self.settings}") raise e class MISPWarninglist(AbstractMISP): - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'Warninglist' in kwargs: kwargs = kwargs['Warninglist'] super().from_dict(**kwargs) @@ -2161,18 +2231,18 @@ class MISPTaxonomy(AbstractMISP): enabled: bool namespace: str - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'Taxonomy' in kwargs: kwargs = kwargs['Taxonomy'] super().from_dict(**kwargs) - def __repr__(self): + def __repr__(self) -> str: return f'<{self.__class__.__name__}(namespace={self.namespace})>' class MISPNoticelist(AbstractMISP): - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'Noticelist' in kwargs: kwargs = kwargs['Noticelist'] super().from_dict(**kwargs) @@ -2180,7 +2250,7 @@ class MISPNoticelist(AbstractMISP): class MISPCorrelationExclusion(AbstractMISP): - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'CorrelationExclusion' in kwargs: kwargs = kwargs['CorrelationExclusion'] super().from_dict(**kwargs) @@ -2188,12 +2258,12 @@ class MISPCorrelationExclusion(AbstractMISP): class MISPRole(AbstractMISP): - def __init__(self, **kwargs: Dict) -> None: + def __init__(self, **kwargs: dict[str, Any]) -> None: super().__init__(**kwargs) self.perm_admin: int self.perm_site_admin: int - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'Role' in kwargs: kwargs = kwargs['Role'] super().from_dict(**kwargs) @@ -2201,7 +2271,7 @@ class MISPRole(AbstractMISP): class MISPServer(AbstractMISP): - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'Server' in kwargs: kwargs = kwargs['Server'] super().from_dict(**kwargs) @@ -2209,13 +2279,13 @@ class MISPServer(AbstractMISP): class MISPLog(AbstractMISP): - def __init__(self, **kwargs: Dict) -> None: + def __init__(self, **kwargs: dict[str, Any]) -> None: super().__init__(**kwargs) self.model: str self.action: str self.title: str - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'Log' in kwargs: kwargs = kwargs['Log'] super().from_dict(**kwargs) @@ -2226,13 +2296,13 @@ class MISPLog(AbstractMISP): class MISPEventDelegation(AbstractMISP): - def __init__(self, **kwargs: Dict) -> None: + def __init__(self, **kwargs: dict[str, Any]) -> None: super().__init__(**kwargs) self.org_id: int self.requester_org_id: int self.event_id: int - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'EventDelegation' in kwargs: kwargs = kwargs['EventDelegation'] super().from_dict(**kwargs) @@ -2243,15 +2313,15 @@ class MISPEventDelegation(AbstractMISP): class MISPObjectAttribute(MISPAttribute): - _fields_for_feed: set = {'uuid', 'value', 'category', 'type', 'comment', 'data', - 'deleted', 'timestamp', 'to_ids', 'disable_correlation', - 'first_seen', 'last_seen', 'object_relation'} + _fields_for_feed: set[str] = {'uuid', 'value', 'category', 'type', 'comment', 'data', + 'deleted', 'timestamp', 'to_ids', 'disable_correlation', + 'first_seen', 'last_seen', 'object_relation'} - def __init__(self, definition): + def __init__(self, definition: dict[str, Any]) -> None: super().__init__() self._definition = definition - def from_dict(self, object_relation: str, value: Union[str, int, float], **kwargs): # type: ignore + def from_dict(self, object_relation: str, value: str | int | float, **kwargs): # type: ignore # NOTE: Signature of "from_dict" incompatible with supertype "MISPAttribute" self.object_relation = object_relation self.value = value @@ -2278,90 +2348,296 @@ class MISPObjectAttribute(MISPAttribute): raise NewAttributeError("The type of the attribute is required. Is the object template missing?") super().from_dict(**{**self, **kwargs}) - def __repr__(self): + def __repr__(self) -> str: if hasattr(self, 'value'): return '<{self.__class__.__name__}(object_relation={self.object_relation}, value={self.value})'.format(self=self) - return '<{self.__class__.__name__}(NotInitialized)'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' class MISPCommunity(AbstractMISP): - def from_dict(self, **kwargs): + def __init__(self, **kwargs: dict[str, Any]) -> None: + super().__init__(**kwargs) + self.name: str + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'Community' in kwargs: kwargs = kwargs['Community'] super().from_dict(**kwargs) - def __repr__(self): + def __repr__(self) -> str: return f'<{self.__class__.__name__}(name={self.name}, uuid={self.uuid})' class MISPUserSetting(AbstractMISP): - def from_dict(self, **kwargs): + def __init__(self, **kwargs: dict[str, Any]) -> None: + super().__init__(**kwargs) + self.setting: str + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'UserSetting' in kwargs: kwargs = kwargs['UserSetting'] super().from_dict(**kwargs) - def __repr__(self): + def __repr__(self) -> str: return f'<{self.__class__.__name__}(name={self.setting}' class MISPInbox(AbstractMISP): - def __init__(self, **kwargs: Dict) -> None: + def __init__(self, **kwargs: dict[str, Any]) -> None: super().__init__(**kwargs) - self.data: Dict + self.data: dict[str, Any] + self.type: str - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'Inbox' in kwargs: kwargs = kwargs['Inbox'] super().from_dict(**kwargs) - def __repr__(self): + def __repr__(self) -> str: return f'<{self.__class__.__name__}(name={self.type})>' class MISPEventBlocklist(AbstractMISP): - def __init__(self, **kwargs: Dict) -> None: + def __init__(self, **kwargs: dict[str, Any]) -> None: super().__init__(**kwargs) self.event_uuid: str - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'EventBlocklist' in kwargs: kwargs = kwargs['EventBlocklist'] super().from_dict(**kwargs) - def __repr__(self): + def __repr__(self) -> str: return f'<{self.__class__.__name__}(event_uuid={self.event_uuid}' class MISPOrganisationBlocklist(AbstractMISP): - def __init__(self, **kwargs: Dict) -> None: + def __init__(self, **kwargs: dict[str, Any]) -> None: super().__init__(**kwargs) self.org_uuid: str - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'OrgBlocklist' in kwargs: kwargs = kwargs['OrgBlocklist'] super().from_dict(**kwargs) - def __repr__(self): + def __repr__(self) -> str: return f'<{self.__class__.__name__}(org_uuid={self.org_uuid}' class MISPDecayingModel(AbstractMISP): - def __init__(self, **kwargs: Dict) -> None: + def __init__(self, **kwargs: dict[str, Any]) -> None: super().__init__(**kwargs) self.uuid: str self.id: int - def from_dict(self, **kwargs): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] if 'DecayingModel' in kwargs: kwargs = kwargs['DecayingModel'] super().from_dict(**kwargs) - def __repr__(self): + def __repr__(self) -> str: return f'<{self.__class__.__name__}(uuid={self.uuid})>' + + +class MISPAnalystData(AbstractMISP): + + _fields_for_feed: set[str] = {'uuid', 'object_uuid', 'object_type', 'authors', + 'created', 'distribution', 'sharing_group_id', 'note_type_name'} + + valid_object_type = {'Attribute', 'Event', 'EventReport', 'GalaxyCluster', 'Galaxy', + 'Object', 'Note', 'Opinion', 'Relationship', 'Organisation', + 'SharingGroup'} + + @property + def org(self) -> MISPOrganisation: + return self.Org + + @property + def orgc(self) -> MISPOrganisation: + return self.Orgc + + @orgc.setter + def orgc(self, orgc: MISPOrganisation) -> None: + if isinstance(orgc, MISPOrganisation): + self.Orgc = orgc + else: + raise PyMISPError('Orgc must be of type MISPOrganisation.') + + def __new__(cls, *args, **kwargs): + if cls is MISPAnalystData: + raise TypeError(f"only children of '{cls.__name__}' may be instantiated") + return object.__new__(cls) + + def __init__(self, **kwargs: dict[str, Any]) -> None: + super().__init__(**kwargs) + self.uuid = str(uuid.uuid4()) + self.object_uuid: str + self.object_type: str + self.authors: str + self.created: float | int | datetime + self.modified: float | int | datetime + self.SharingGroup: MISPSharingGroup + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + self.distribution = kwargs.pop('distribution', None) + if self.distribution is not None: + self.distribution = int(self.distribution) + if self.distribution not in [0, 1, 2, 3, 4, 5]: + raise NewAnalystDataError(f'{self.distribution} is invalid, the distribution has to be in 0, 1, 2, 3, 4, 5') + + if kwargs.get('sharing_group_id'): + self.sharing_group_id = int(kwargs.pop('sharing_group_id')) + + if self.distribution == 4: + # The distribution is set to sharing group, a sharing_group_id is required. + if not hasattr(self, 'sharing_group_id'): + raise NewAnalystDataError('If the distribution is set to sharing group, a sharing group ID is required.') + elif not self.sharing_group_id: + # Cannot be None or 0 either. + raise NewAnalystDataError(f'If the distribution is set to sharing group, a sharing group ID is required (cannot be {self.sharing_group_id}).') + + self.object_uuid = kwargs.pop('object_uuid', None) + if self.object_uuid is None: + raise NewAnalystDataError('The UUID for which this element is attached is required.') + self.object_type = kwargs.pop('object_type', None) + if self.object_type is None: + raise NewAnalystDataError('The element type for which this element is attached is required.') + if self.object_type not in self.valid_object_type: + raise NewAnalystDataError('The element type is not a valid type. Actual: {self.object_type}.') + + if kwargs.get('id'): + self.id = int(kwargs.pop('id')) + if kwargs.get('created'): + ts = kwargs.pop('created') + if isinstance(ts, datetime): + self.created = ts + else: + self.created = datetime.fromisoformat(ts + '+00:00') # Force UTC TZ + if kwargs.get('modified'): + ts = kwargs.pop('modified') + if isinstance(ts, datetime): + self.modified = ts + else: + self.modified = datetime.fromisoformat(ts + '+00:00') # Force UTC TZ + + if kwargs.get('Org'): + self.Org = MISPOrganisation() + self.Org.from_dict(**kwargs.pop('Org')) + if kwargs.get('Orgc'): + self.Orgc = MISPOrganisation() + self.Orgc.from_dict(**kwargs.pop('Orgc')) + if kwargs.get('SharingGroup'): + self.SharingGroup = MISPSharingGroup() + self.SharingGroup.from_dict(**kwargs.pop('SharingGroup')) + + super().from_dict(**kwargs) + + def _set_default(self) -> None: + if not hasattr(self, 'created'): + self.created = datetime.timestamp(datetime.now()) + if not hasattr(self, 'modified'): + self.modified = self.created + + +class MISPNote(AnalystDataBehaviorMixin, MISPAnalystData): + + _fields_for_feed: set[str] = MISPAnalystData._fields_for_feed.union({'note', 'language'}) + + _analyst_data_object_type = 'Note' + + def __init__(self, **kwargs: dict[str, Any]) -> None: + self.note: str + self.language: str + super().__init__(**kwargs) + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'Note' in kwargs: + kwargs = kwargs['Note'] + self.note = kwargs.pop('note', None) + if self.note is None: + raise NewNoteError('The text note of the note is required.') + super().from_dict(**kwargs) + + def __repr__(self) -> str: + if hasattr(self, 'note'): + return '<{self.__class__.__name__}(note={self.note})'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' + + +class MISPOpinion(AnalystDataBehaviorMixin, MISPAnalystData): + + _fields_for_feed: set[str] = MISPAnalystData._fields_for_feed.union({'opinion', 'comment'}) + + _analyst_data_object_type = 'Opinion' + + def __init__(self, **kwargs: dict[str, Any]) -> None: + self.opinion: int + self.comment: str + super().__init__(**kwargs) + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'Opinion' in kwargs: + kwargs = kwargs['Opinion'] + self.opinion = kwargs.pop('opinion', None) + if self.opinion is not None: + self.opinion = int(self.opinion) + if not (0 <= self.opinion <= 100): + raise NewOpinionError('The opinion value must be between 0 and 100 included.') + else: + raise NewOpinionError('The opinion value is required.') + + self.comment = kwargs.pop('comment', None) + if self.comment is None: + raise NewOpinionError('The text comment is required.') + + return super().from_dict(**kwargs) + + def __repr__(self) -> str: + if hasattr(self, 'opinion'): + return '<{self.__class__.__name__}([opinion={self.opinion}] comment={self.comment})'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' + + +class MISPRelationship(AnalystDataBehaviorMixin, MISPAnalystData): + + _fields_for_feed: set[str] = MISPAnalystData._fields_for_feed.union({'related_object_uuid', 'related_object_type', 'relationship_type'}) + + _analyst_data_object_type = 'Relationship' + + def __init__(self, **kwargs: dict[str, Any]) -> None: + self.related_object_uuid: str + self.related_object_type: str + self.relationship_type: str + super().__init__(**kwargs) + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'Relationship' in kwargs: + kwargs = kwargs['Relationship'] + self.related_object_type = kwargs.pop('related_object_type', None) + if self.related_object_type is None: + raise NewRelationshipError('The target object type for this relationship is required.') + + self.related_object_uuid = kwargs.pop('related_object_uuid', None) + if self.related_object_uuid is None: + if not isinstance(self.related_object_type, AbstractMISP): + raise NewRelationshipError('The target UUID for this relationship is required.') + else: + self.related_object_uuid = self.related_object_type.uuid + self.related_object_type = self.related_object_type._analyst_data_object_type + + if self.related_object_type not in self.valid_object_type: + raise NewAnalystDataError(f'The target object type is not a valid type. Actual: {self.related_object_type}.') + + return super().from_dict(**kwargs) + + def __repr__(self) -> str: + if hasattr(self, 'related_object_uuid') and hasattr(self, 'object_uuid'): + return '<{self.__class__.__name__}(object_uuid={self.object_uuid}, related_object_type={self.related_object_type}, related_object_uuid={self.related_object_uuid}, relationship_type={self.relationship_type})'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' diff --git a/pymisp/tools/__init__.py b/pymisp/tools/__init__.py index cd5c1c9..45907b3 100644 --- a/pymisp/tools/__init__.py +++ b/pymisp/tools/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from .vtreportobject import VTReportObject # noqa from .neo4j import Neo4j # noqa from .fileobject import FileObject # noqa @@ -41,3 +43,13 @@ try: except ImportError: # Requires lief, optional [fileobjects] pass + +__all__ = ['VTReportObject', 'Neo4j', 'FileObject', 'make_binary_objects', + 'AbstractMISPObjectGenerator', 'GenericObjectGenerator', + 'load_openioc', 'load_openioc_file', 'SBSignatureObject', + 'Fail2BanObject', 'DomainIPObject', 'ASNObject', 'GeolocationObject', + 'GitVulnFinderObject', 'VehicleObject', 'CSVLoader', + 'SSHAuthorizedKeysObject', 'feed_meta_generator', 'update_objects', + 'EMailObject', 'URLObject', 'PEObject', 'PESectionObject', 'ELFObject', + 'ELFSectionObject', 'MachOObject', 'MachOSectionObject' + ] diff --git a/pymisp/tools/_psl_faup.py b/pymisp/tools/_psl_faup.py index 18365a0..388fed4 100644 --- a/pymisp/tools/_psl_faup.py +++ b/pymisp/tools/_psl_faup.py @@ -1,36 +1,37 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations import ipaddress import socket import idna from publicsuffixlist import PublicSuffixList # type: ignore -from urllib.parse import urlparse, urlunparse +from urllib.parse import urlparse, urlunparse, ParseResult class UrlNotDecoded(Exception): pass -class PSLFaup(object): +class PSLFaup: """ Fake Faup Python Library using PSL for Windows support """ - def __init__(self): + def __init__(self) -> None: self.decoded = False self.psl = PublicSuffixList() - self._url = None - self._retval = {} - self.ip_as_host = False + self._url: ParseResult | None = None + self._retval: dict[str, str | int | None | bytes] = {} + self.ip_as_host = '' - def _clear(self): + def _clear(self) -> None: self.decoded = False self._url = None self._retval = {} - self.ip_as_host = False + self.ip_as_host = '' - def decode(self, url) -> None: + def decode(self, url: str) -> None: """ This function creates a dict of all the url fields. :param url: The URL to normalize @@ -42,10 +43,15 @@ class PSLFaup(object): url = '//' + url self._url = urlparse(url) - self.ip_as_host = False + if self._url is None: + raise UrlNotDecoded("Unable to parse URL") + + self.ip_as_host = '' + if self._url.hostname is None: + raise UrlNotDecoded("Unable to parse URL") hostname = _ensure_str(self._url.hostname) try: - ipv4_bytes = socket.inet_aton(_ensure_str(hostname)) + ipv4_bytes = socket.inet_aton(hostname) ipv4 = ipaddress.IPv4Address(ipv4_bytes) self.ip_as_host = ipv4.compressed except (OSError, ValueError): @@ -60,61 +66,70 @@ class PSLFaup(object): self._retval = {} @property - def url(self): - if not self.decoded: + def url(self) -> bytes | None: + if not self.decoded or not self._url: raise UrlNotDecoded("You must call faup.decode() first") - netloc = self.get_host() + ('' if self.get_port() is None else ':{}'.format(self.get_port())) - return _ensure_bytes( - urlunparse( - (self.get_scheme(), netloc, self.get_resource_path(), - '', self.get_query_string(), self.get_fragment(),) + if host := self.get_host(): + netloc = host + ('' if self.get_port() is None else f':{self.get_port()}') + return _ensure_bytes( + urlunparse( + (self.get_scheme(), netloc, self.get_resource_path(), + '', self.get_query_string(), self.get_fragment(),) + ) ) - ) + return None - def get_scheme(self): + def get_scheme(self) -> str: """ Get the scheme of the url given in the decode function :returns: The URL scheme """ - if not self.decoded: + if not self.decoded or not self._url: raise UrlNotDecoded("You must call faup.decode() first") - return _ensure_str(self._url.scheme) + return _ensure_str(self._url.scheme if self._url.scheme else '') - def get_credential(self): - if not self.decoded: + def get_credential(self) -> str | None: + if not self.decoded or not self._url: raise UrlNotDecoded("You must call faup.decode() first") - if self._url.password: + if self._url.username and self._url.password: return _ensure_str(self._url.username) + ':' + _ensure_str(self._url.password) if self._url.username: return _ensure_str(self._url.username) + return None - def get_subdomain(self): - if not self.decoded: + def get_subdomain(self) -> str | None: + if not self.decoded or not self._url: raise UrlNotDecoded("You must call faup.decode() first") if self.get_host() is not None and not self.ip_as_host: - if self.get_domain() in self.get_host(): - return self.get_host().rsplit(self.get_domain(), 1)[0].rstrip('.') or None + domain = self.get_domain() + host = self.get_host() + if domain and host and domain in host: + return host.rsplit(domain, 1)[0].rstrip('.') or None + return None - def get_domain(self): - if not self.decoded: + def get_domain(self) -> str | None: + if not self.decoded or not self._url: raise UrlNotDecoded("You must call faup.decode() first") if self.get_host() is not None and not self.ip_as_host: return self.psl.privatesuffix(self.get_host()) + return None - def get_domain_without_tld(self): - if not self.decoded: + def get_domain_without_tld(self) -> str | None: + if not self.decoded or not self._url: raise UrlNotDecoded("You must call faup.decode() first") if self.get_tld() is not None and not self.ip_as_host: - return self.get_domain().rsplit(self.get_tld(), 1)[0].rstrip('.') + if domain := self.get_domain(): + return domain.rsplit(self.get_tld(), 1)[0].rstrip('.') + return None - def get_host(self): - if not self.decoded: + def get_host(self) -> str | None: + if not self.decoded or not self._url: raise UrlNotDecoded("You must call faup.decode() first") if self._url.hostname is None: @@ -124,45 +139,48 @@ class PSLFaup(object): else: return _ensure_str(idna.encode(self._url.hostname, uts46=True)) - def get_unicode_host(self): - if not self.decoded: + def get_unicode_host(self) -> str | None: + if not self.decoded or not self._url: raise UrlNotDecoded("You must call faup.decode() first") if not self.ip_as_host: - return idna.decode(self.get_host(), uts46=True) + if host := self.get_host(): + return idna.decode(host, uts46=True) + return None - def get_tld(self): - if not self.decoded: + def get_tld(self) -> str | None: + if not self.decoded or not self._url: raise UrlNotDecoded("You must call faup.decode() first") if self.get_host() is not None and not self.ip_as_host: return self.psl.publicsuffix(self.get_host()) + return None - def get_port(self): - if not self.decoded: + def get_port(self) -> int | None: + if not self.decoded or not self._url: raise UrlNotDecoded("You must call faup.decode() first") return self._url.port - def get_resource_path(self): - if not self.decoded: + def get_resource_path(self) -> str: + if not self.decoded or not self._url: raise UrlNotDecoded("You must call faup.decode() first") return _ensure_str(self._url.path) - def get_query_string(self): - if not self.decoded: + def get_query_string(self) -> str: + if not self.decoded or not self._url: raise UrlNotDecoded("You must call faup.decode() first") return _ensure_str(self._url.query) - def get_fragment(self): - if not self.decoded: + def get_fragment(self) -> str: + if not self.decoded or not self._url: raise UrlNotDecoded("You must call faup.decode() first") return _ensure_str(self._url.fragment) - def get(self): + def get(self) -> dict[str, str | int | None | bytes]: self._retval["scheme"] = self.get_scheme() self._retval["tld"] = self.get_tld() self._retval["domain"] = self.get_domain() @@ -177,14 +195,14 @@ class PSLFaup(object): return self._retval -def _ensure_bytes(binary) -> bytes: +def _ensure_bytes(binary: str | bytes) -> bytes: if isinstance(binary, bytes): return binary else: return binary.encode('utf-8') -def _ensure_str(string) -> str: +def _ensure_str(string: str | bytes) -> str: if isinstance(string, str): return string else: diff --git a/pymisp/tools/abstractgenerator.py b/pymisp/tools/abstractgenerator.py index 582356e..a3ca26f 100644 --- a/pymisp/tools/abstractgenerator.py +++ b/pymisp/tools/abstractgenerator.py @@ -1,16 +1,19 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations + +from datetime import datetime, date +from dateutil.parser import parse + +from typing import Any from .. import MISPObject from ..exceptions import InvalidMISPObject -from datetime import datetime, date -from dateutil.parser import parse -from typing import Union, Optional class AbstractMISPObjectGenerator(MISPObject): - def _detect_epoch(self, timestamp: Union[str, int, float]) -> bool: + def _detect_epoch(self, timestamp: str | int | float) -> bool: try: tmp = float(timestamp) if tmp < 30000000: @@ -21,7 +24,7 @@ class AbstractMISPObjectGenerator(MISPObject): except ValueError: return False - def _sanitize_timestamp(self, timestamp: Optional[Union[datetime, date, dict, str, int, float]] = None) -> datetime: + def _sanitize_timestamp(self, timestamp: datetime | date | dict[str, Any] | str | int | float | None = None) -> datetime: if not timestamp: return datetime.now() @@ -42,9 +45,9 @@ class AbstractMISPObjectGenerator(MISPObject): else: raise Exception(f'Unable to convert {timestamp} to a datetime.') - def generate_attributes(self): + def generate_attributes(self) -> None: """Contains the logic where all the values of the object are gathered""" - if hasattr(self, '_parameters'): + if hasattr(self, '_parameters') and self._definition is not None: for object_relation in self._definition['attributes']: value = self._parameters.pop(object_relation, None) if not value: diff --git a/pymisp/tools/asnobject.py b/pymisp/tools/asnobject.py index 909d06b..685da7a 100644 --- a/pymisp/tools/asnobject.py +++ b/pymisp/tools/asnobject.py @@ -1,20 +1,24 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations + +import logging + +from typing import Any from .abstractgenerator import AbstractMISPObjectGenerator -import logging logger = logging.getLogger('pymisp') class ASNObject(AbstractMISPObjectGenerator): - def __init__(self, parameters: dict, strict: bool = True, **kwargs): + def __init__(self, parameters: dict[str, Any], strict: bool = True, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__('asn', strict=strict, **kwargs) self._parameters = parameters self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: first = self._sanitize_timestamp(self._parameters.pop('first-seen', None)) self._parameters['first-seen'] = first last = self._sanitize_timestamp(self._parameters.pop('last-seen', None)) diff --git a/pymisp/tools/create_misp_object.py b/pymisp/tools/create_misp_object.py index b4d36ce..d2f402c 100644 --- a/pymisp/tools/create_misp_object.py +++ b/pymisp/tools/create_misp_object.py @@ -1,24 +1,25 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations + +import logging from io import BytesIO +from typing import Any, TYPE_CHECKING -from . import FileObject from ..exceptions import MISPObjectException -import logging -from typing import Optional - +from . import FileObject logger = logging.getLogger('pymisp') try: - import lief # type: ignore + import lief + import lief.logging lief.logging.disable() HAS_LIEF = True from .peobject import make_pe_objects from .elfobject import make_elf_objects from .machoobject import make_macho_objects - except AttributeError: HAS_LIEF = False logger.critical('You need lief >= 0.11.0. The quick and dirty fix is: pip3 install --force pymisp[fileobjects]') @@ -26,49 +27,43 @@ except AttributeError: except ImportError: HAS_LIEF = False +if TYPE_CHECKING: + from . import PEObject, ELFObject, MachOObject, PESectionObject, ELFSectionObject, MachOSectionObject + class FileTypeNotImplemented(MISPObjectException): pass -def make_binary_objects(filepath: Optional[str] = None, pseudofile: Optional[BytesIO] = None, filename: Optional[str] = None, standalone: bool = True, default_attributes_parameters: dict = {}): +def make_binary_objects(filepath: str | None = None, + pseudofile: BytesIO | bytes | None = None, + filename: str | None = None, + standalone: bool = True, + default_attributes_parameters: dict[str, Any] = {}) -> tuple[FileObject, PEObject | ELFObject | MachOObject | None, list[PESectionObject] | list[ELFSectionObject] | list[MachOSectionObject]]: misp_file = FileObject(filepath=filepath, pseudofile=pseudofile, filename=filename, standalone=standalone, default_attributes_parameters=default_attributes_parameters) - if HAS_LIEF and (filepath or (pseudofile and filename)): - try: - if filepath: - lief_parsed = lief.parse(filepath=filepath) - elif pseudofile and filename: - lief_parsed = lief.parse(raw=pseudofile.getvalue(), name=filename) - else: - logger.critical('You need either a filepath, or a pseudofile and a filename.') - lief_parsed = None - if isinstance(lief_parsed, lief.PE.Binary): - return make_pe_objects(lief_parsed, misp_file, standalone, default_attributes_parameters) - elif isinstance(lief_parsed, lief.ELF.Binary): - return make_elf_objects(lief_parsed, misp_file, standalone, default_attributes_parameters) - elif isinstance(lief_parsed, lief.MachO.Binary): - return make_macho_objects(lief_parsed, misp_file, standalone, default_attributes_parameters) - except lief.bad_format as e: - logger.warning('Bad format: {}'.format(e)) - except lief.bad_file as e: - logger.warning('Bad file: {}'.format(e)) - except lief.conversion_error as e: - logger.warning('Conversion file: {}'.format(e)) - except lief.builder_error as e: - logger.warning('Builder file: {}'.format(e)) - except lief.parser_error as e: - logger.warning('Parser error: {}'.format(e)) - except lief.integrity_error as e: - logger.warning('Integrity error: {}'.format(e)) - except lief.pe_error as e: - logger.warning('PE error: {}'.format(e)) - except lief.type_error as e: - logger.warning('Type error: {}'.format(e)) - except lief.exception as e: - logger.warning('Lief exception: {}'.format(e)) - except FileTypeNotImplemented as e: - logger.warning(e) + if HAS_LIEF and (filepath or pseudofile): + if filepath: + lief_parsed = lief.parse(filepath=filepath) + elif pseudofile: + if isinstance(pseudofile, bytes): + lief_parsed = lief.parse(raw=pseudofile) + else: # BytesIO + lief_parsed = lief.parse(obj=pseudofile) + else: + logger.critical('You need either a filepath, or a pseudofile and a filename.') + lief_parsed = None + + if isinstance(lief_parsed, lief.lief_errors): + logger.warning('Got an error parsing the file: {lief_parsed}') + elif isinstance(lief_parsed, lief.PE.Binary): + return make_pe_objects(lief_parsed, misp_file, standalone, default_attributes_parameters) + elif isinstance(lief_parsed, lief.ELF.Binary): + return make_elf_objects(lief_parsed, misp_file, standalone, default_attributes_parameters) + elif isinstance(lief_parsed, lief.MachO.Binary): + return make_macho_objects(lief_parsed, misp_file, standalone, default_attributes_parameters) + else: + logger.critical(f'Unexpected type from lief: {type(lief_parsed)}') if not HAS_LIEF: logger.warning('Please install lief, documentation here: https://github.com/lief-project/LIEF') return misp_file, None, [] diff --git a/pymisp/tools/csvloader.py b/pymisp/tools/csvloader.py index b02ad69..e0452ec 100644 --- a/pymisp/tools/csvloader.py +++ b/pymisp/tools/csvloader.py @@ -1,49 +1,55 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- + +from __future__ import annotations from pathlib import Path + import csv from pymisp import MISPObject class CSVLoader(): - def __init__(self, template_name: str, csv_path: Path, fieldnames: list = [], has_fieldnames=False, - delimiter: str = ',', quotechar: str = '"'): + def __init__(self, template_name: str, csv_path: Path, + fieldnames: list[str] | None = None, has_fieldnames: bool=False, + delimiter: str = ',', quotechar: str = '"') -> None: self.template_name = template_name self.delimiter = delimiter self.quotechar = quotechar self.csv_path = csv_path - self.fieldnames = [f.strip().lower() for f in fieldnames] + self.fieldnames = [] + if fieldnames: + self.fieldnames = [f.strip() for f in fieldnames] if not self.fieldnames: - # If the user doesn't pass fieldnames, we assume the CSV has them. + # If the user doesn't pass fieldnames, they must be in the CSV. self.has_fieldnames = True else: self.has_fieldnames = has_fieldnames - def load(self): + def load(self) -> list[MISPObject]: objects = [] with open(self.csv_path, newline='') as csvfile: reader = csv.reader(csvfile, delimiter=self.delimiter, quotechar=self.quotechar) if self.has_fieldnames: - # The file has fieldnames, we either ignore it, or validate its validity - fieldnames = [f.strip().lower() for f in reader.__next__()] + # The file has fieldnames, we either ignore it, or use them as object-relation + fieldnames = [f.strip() for f in reader.__next__()] if not self.fieldnames: self.fieldnames = fieldnames if not self.fieldnames: raise Exception('No fieldnames, impossible to create objects.') - else: - # Check if the CSV file has a header, and if it matches with the object template - tmp_object = MISPObject(self.template_name) - if not tmp_object._definition['attributes']: - raise Exception(f'Unable to find the object template ({self.template_name}), impossible to create objects.') - allowed_fieldnames = list(tmp_object._definition['attributes'].keys()) - for fieldname in self.fieldnames: - if fieldname not in allowed_fieldnames: - raise Exception(f'{fieldname} is not a valid object relation for {self.template_name}: {allowed_fieldnames}') + + # Check if the CSV file has a header, and if it matches with the object template + tmp_object = MISPObject(self.template_name) + + if not tmp_object._definition or not tmp_object._definition['attributes']: + raise Exception(f'Unable to find the object template ({self.template_name}), impossible to create objects.') + allowed_fieldnames = list(tmp_object._definition['attributes'].keys()) + for fieldname in self.fieldnames: + if fieldname not in allowed_fieldnames: + raise Exception(f'{fieldname} is not a valid object relation for {self.template_name}: {allowed_fieldnames}') for row in reader: tmp_object = MISPObject(self.template_name) diff --git a/pymisp/tools/domainipobject.py b/pymisp/tools/domainipobject.py index 2fe9a3e..2269342 100644 --- a/pymisp/tools/domainipobject.py +++ b/pymisp/tools/domainipobject.py @@ -1,20 +1,24 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations + +import logging + +from typing import Any from .abstractgenerator import AbstractMISPObjectGenerator -import logging logger = logging.getLogger('pymisp') class DomainIPObject(AbstractMISPObjectGenerator): - def __init__(self, parameters: dict, strict: bool = True, **kwargs): + def __init__(self, parameters: dict[str, Any], strict: bool = True, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__('domain-ip', strict=strict, **kwargs) self._parameters = parameters self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: first = self._sanitize_timestamp(self._parameters.pop('first-seen', None)) self._parameters['first-seen'] = first last = self._sanitize_timestamp(self._parameters.pop('last-seen', None)) diff --git a/pymisp/tools/elfobject.py b/pymisp/tools/elfobject.py index 91a9311..c0b6ceb 100644 --- a/pymisp/tools/elfobject.py +++ b/pymisp/tools/elfobject.py @@ -1,16 +1,19 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- +from __future__ import annotations + +import logging + +from hashlib import md5, sha1, sha256, sha512 +from io import BytesIO +from pathlib import Path +from typing import Any + +from . import FileObject from .abstractgenerator import AbstractMISPObjectGenerator from ..exceptions import InvalidMISPObject -from io import BytesIO -from hashlib import md5, sha1, sha256, sha512 -import logging -from typing import Union, Optional -from pathlib import Path -from . import FileObject -import lief # type: ignore +import lief try: import pydeep # type: ignore @@ -21,7 +24,10 @@ except ImportError: logger = logging.getLogger('pymisp') -def make_elf_objects(lief_parsed: lief.Binary, misp_file: FileObject, standalone: bool = True, default_attributes_parameters: dict = {}): +def make_elf_objects(lief_parsed: lief.ELF.Binary, + misp_file: FileObject, + standalone: bool = True, + default_attributes_parameters: dict[str, Any] = {}) -> tuple[FileObject, ELFObject, list[ELFSectionObject]]: elf_object = ELFObject(parsed=lief_parsed, standalone=standalone, default_attributes_parameters=default_attributes_parameters) misp_file.add_reference(elf_object.uuid, 'includes', 'ELF indicators') elf_sections = [] @@ -32,29 +38,39 @@ def make_elf_objects(lief_parsed: lief.Binary, misp_file: FileObject, standalone class ELFObject(AbstractMISPObjectGenerator): - def __init__(self, parsed: Optional[lief.ELF.Binary] = None, filepath: Optional[Union[Path, str]] = None, pseudofile: Optional[Union[BytesIO, bytes]] = None, **kwargs): + __elf: lief.ELF.Binary + + def __init__(self, parsed: lief.ELF.Binary | None = None, # type: ignore[no-untyped-def] + filepath: Path | str | None = None, + pseudofile: BytesIO | bytes | list[int] | None = None, **kwargs) -> None: """Creates an ELF object, with lief""" super().__init__('elf', **kwargs) if not HAS_PYDEEP: - logger.warning("Please install pydeep: pip install git+https://github.com/kbandla/pydeep.git") + logger.warning("pydeep is missing, please install pymisp this way: pip install pymisp[fileobjects]") if pseudofile: if isinstance(pseudofile, BytesIO): - self.__elf = lief.ELF.parse(raw=pseudofile.getvalue()) + e = lief.ELF.parse(obj=pseudofile) elif isinstance(pseudofile, bytes): - self.__elf = lief.ELF.parse(raw=pseudofile) + e = lief.ELF.parse(raw=list(pseudofile)) + elif isinstance(pseudofile, list): + e = lief.ELF.parse(raw=pseudofile) else: - raise InvalidMISPObject('Pseudo file can be BytesIO or bytes got {}'.format(type(pseudofile))) + raise InvalidMISPObject(f'Pseudo file can be BytesIO or bytes got {type(pseudofile)}') + if not e: + raise InvalidMISPObject('Unable to parse pseudofile') + self.__elf = e elif filepath: - self.__elf = lief.ELF.parse(filepath) + if e := lief.ELF.parse(filepath): + self.__elf = e elif parsed: # Got an already parsed blob if isinstance(parsed, lief.ELF.Binary): self.__elf = parsed else: - raise InvalidMISPObject('Not a lief.ELF.Binary: {}'.format(type(parsed))) + raise InvalidMISPObject(f'Not a lief.ELF.Binary: {type(parsed)}') self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: # General information self.add_attribute('type', value=str(self.__elf.header.file_type).split('.')[1]) self.add_attribute('entrypoint-address', value=self.__elf.entrypoint) @@ -68,7 +84,7 @@ class ELFObject(AbstractMISPObjectGenerator): if not section.name: continue s = ELFSectionObject(section, standalone=self._standalone, default_attributes_parameters=self._default_attributes_parameters) - self.add_reference(s.uuid, 'includes', 'Section {} of ELF'.format(pos)) + self.add_reference(s.uuid, 'includes', f'Section {pos} of ELF') pos += 1 self.sections.append(s) self.add_attribute('number-sections', value=len(self.sections)) @@ -76,22 +92,22 @@ class ELFObject(AbstractMISPObjectGenerator): class ELFSectionObject(AbstractMISPObjectGenerator): - def __init__(self, section: lief.ELF.Section, **kwargs): + def __init__(self, section: lief.ELF.Section, **kwargs) -> None: # type: ignore[no-untyped-def] """Creates an ELF Section object. Object generated by ELFObject.""" # Python3 way # super().__init__('pe-section') - super(ELFSectionObject, self).__init__('elf-section', **kwargs) + super().__init__('elf-section', **kwargs) self.__section = section self.__data = bytes(self.__section.content) self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: self.add_attribute('name', value=self.__section.name) self.add_attribute('type', value=str(self.__section.type).split('.')[1]) for flag in self.__section.flags_list: self.add_attribute('flag', value=str(flag).split('.')[1]) - size = self.add_attribute('size-in-bytes', value=self.__section.size) - if int(size.value) > 0: + self.add_attribute('size-in-bytes', value=self.__section.size) + if int(self.__section.size) > 0: self.add_attribute('entropy', value=self.__section.entropy) self.add_attribute('md5', value=md5(self.__data).hexdigest()) self.add_attribute('sha1', value=sha1(self.__data).hexdigest()) diff --git a/pymisp/tools/emailobject.py b/pymisp/tools/emailobject.py index 4b0cc58..4c5dab0 100644 --- a/pymisp/tools/emailobject.py +++ b/pymisp/tools/emailobject.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- + +from __future__ import annotations import re import logging @@ -9,15 +10,17 @@ from email import policy, message_from_bytes from email.message import EmailMessage from io import BytesIO from pathlib import Path -from typing import Union, List, Tuple, Dict, cast, Any, Optional +from typing import cast, Any from extract_msg import openMsg -from extract_msg.message import Message as MsgObj +from extract_msg.msg_classes import MessageBase +from extract_msg.attachments import AttachmentBase, SignedAttachment +from extract_msg.properties import FixedLengthProp from RTFDE.exceptions import MalformedEncapsulatedRtf, NotEncapsulatedRtf # type: ignore from RTFDE.deencapsulate import DeEncapsulator # type: ignore from oletools.common.codepages import codepage2codec # type: ignore -from ..exceptions import InvalidMISPObject, PyMISPNotImplementedYet, MISPObjectException, NewAttributeError +from ..exceptions import InvalidMISPObject, MISPObjectException, NewAttributeError from .abstractgenerator import AbstractMISPObjectGenerator logger = logging.getLogger('pymisp') @@ -28,15 +31,14 @@ class MISPMsgConverstionError(MISPObjectException): class EMailObject(AbstractMISPObjectGenerator): - def __init__(self, filepath: Optional[Union[Path, str]]=None, pseudofile: Optional[BytesIO]=None, - attach_original_email: bool = True, **kwargs): + def __init__(self, filepath: Path | str | None=None, pseudofile: BytesIO | bytes | None=None, # type: ignore[no-untyped-def] + attach_original_email: bool = True, **kwargs) -> None: super().__init__('email', **kwargs) self.attach_original_email = attach_original_email - self.encapsulated_body: Union[str, None] = None - self.eml_from_msg: Union[bool, None] = None - self.raw_emails: Dict[str, Union[BytesIO, None]] = {'msg': None, - 'eml': None} + self.encapsulated_body: str | None = None + self.eml_from_msg: bool | None = None + self.raw_emails: dict[str, BytesIO | None] = {'msg': None, 'eml': None} self.__pseudofile = self.create_pseudofile(filepath, pseudofile) self.email = self.parse_email() @@ -65,7 +67,7 @@ class EMailObject(AbstractMISPObjectGenerator): return message except ValueError as _e: # Exception logger.debug("Email not in .msg format or is a corrupted .msg. Attempting to decode email from other formats.") - logger.debug("Error: {} ".format(_e)) + logger.debug(f"Error: {_e} ") try: if content_in_bytes[:3] == b'\xef\xbb\xbf': # utf-8-sig byte-order mark (BOM) eml_bytes = content_in_bytes.decode("utf_8_sig").encode("utf-8") @@ -77,11 +79,11 @@ class EMailObject(AbstractMISPObjectGenerator): return eml except UnicodeDecodeError: pass - raise PyMISPNotImplementedYet("EmailObject does not know how to decode data passed to it. Object may not be an email. If this is an email please submit it as an issue to PyMISP so we can add support.") + raise InvalidMISPObject("EmailObject does not know how to decode data passed to it. Object may not be an email. If this is an email please submit it as an issue to PyMISP so we can add support.") @staticmethod - def create_pseudofile(filepath: Optional[Union[Path, str]] = None, - pseudofile: Optional[BytesIO] = None) -> BytesIO: + def create_pseudofile(filepath: Path | str | None = None, + pseudofile: BytesIO | bytes | None = None) -> BytesIO: """Creates a pseudofile using directly passed data or data loaded from file path. """ if filepath: @@ -89,18 +91,21 @@ class EMailObject(AbstractMISPObjectGenerator): return BytesIO(f.read()) elif pseudofile and isinstance(pseudofile, BytesIO): return pseudofile + elif pseudofile and isinstance(pseudofile, bytes): + return BytesIO(pseudofile) else: raise InvalidMISPObject('File buffer (BytesIO) or a path is required.') def _msg_to_eml(self, msg_bytes: bytes) -> EmailMessage: """Converts a msg into an eml.""" - msg_obj = openMsg(msg_bytes) + # NOTE: openMsg returns a MessageBase, not a MSGFile + msg_obj: MessageBase = openMsg(msg_bytes) # type: ignore # msg obj stores the original raw header here message, body, attachments = self._extract_msg_objects(msg_obj) eml = self._build_eml(message, body, attachments) return eml - def _extract_msg_objects(self, msg_obj: MsgObj) -> Tuple[EmailMessage, Dict, List[Any]]: + def _extract_msg_objects(self, msg_obj: MessageBase) -> tuple[EmailMessage, dict[str, Any], list[AttachmentBase] | list[SignedAttachment]]: """Extracts email objects needed to construct an eml from a msg.""" message: EmailMessage = email.message_from_string(msg_obj.header.as_string(), policy=policy.default) # type: ignore body = {} @@ -111,8 +116,11 @@ class EMailObject(AbstractMISPObjectGenerator): "cte": "base64"} if msg_obj.htmlBody is not None: try: - _html_encoding_raw = msg_obj.props['3FDE0003'].value - _html_encoding = codepage2codec(_html_encoding_raw) + if isinstance(msg_obj.props['3FDE0003'], FixedLengthProp): + _html_encoding_raw = msg_obj.props['3FDE0003'].value + _html_encoding = codepage2codec(_html_encoding_raw) + else: + _html_encoding = msg_obj.stringEncoding except KeyError: _html_encoding = msg_obj.stringEncoding body['html'] = {'obj': msg_obj.htmlBody.decode(), @@ -145,17 +153,16 @@ class EMailObject(AbstractMISPObjectGenerator): attachments = msg_obj.attachments return message, body, attachments - def _build_eml(self, message: EmailMessage, body: dict, attachments: list) -> EmailMessage: + def _build_eml(self, message: EmailMessage, body: dict[str, Any], attachments: list[Any]) -> EmailMessage: """Constructs an eml file from objects extracted from a msg.""" # Order the body objects by increasing complexity and toss any missing objects - body_objects: List[dict] = [body.get('text', {}), - body.get('html', {}), - body.get('rtf', {})] - body_objects = [i for i in body_objects if i != {}] + body_objects: list[dict[str, Any]] = [i for i in [body.get('text'), + body.get('html'), + body.get('rtf')] if i is not None] # If this a non-multipart email then we only need to attach the payload if message.get_content_maintype() != 'multipart': for _body in body_objects: - if "text/{0}".format(_body['subtype']) == message.get_content_type(): + if "text/{}".format(_body['subtype']) == message.get_content_type(): message.set_content(**_body) return message raise MISPMsgConverstionError("Unable to find appropriate eml payload in message body.") @@ -167,8 +174,8 @@ class EMailObject(AbstractMISPObjectGenerator): if isinstance(body.get('html', None), dict): _html = body.get('html', {}).get('obj') for attch in attachments: - if _html.find("cid:{0}".format(attch.cid)) != -1: - _content_type = attch._getStringStream('__substg1.0_370E') + if _html.find(f"cid:{attch.cid}") != -1: + _content_type = attch.getStringStream('__substg1.0_370E') maintype, subtype = _content_type.split("/", 1) related_content[attch.cid] = (attch, {'obj': attch.data, @@ -189,11 +196,19 @@ class EMailObject(AbstractMISPObjectGenerator): for mime_items in related_content.values(): if isinstance(mime_items[1], dict): message.add_related(**mime_items[1]) - cur_attach = message.get_payload()[-1] + if p := message.get_payload(): + if isinstance(p, list): + cur_attach = p[-1] + else: + cur_attach = p self._update_content_disp_properties(mime_items[0], cur_attach) if body.get('text', None): # Now add the HTML as an alternative within the related obj - related = message.get_payload()[0] + if p := message.get_payload(): + if isinstance(p, list): + related = p[0] + else: + related = p related.add_alternative(**body.get('html')) else: for mime_dict in body_objects: @@ -205,20 +220,25 @@ class EMailObject(AbstractMISPObjectGenerator): message.add_alternative(**mime_dict) for attch in attachments: # Add attachments at the end. if attch.cid not in related_content.keys(): - _content_type = attch._getStringStream('__substg1.0_370E') + _content_type = attch.getStringStream('__substg1.0_370E') maintype, subtype = _content_type.split("/", 1) message.add_attachment(attch.data, maintype=maintype, subtype=subtype, cid=attch.cid, filename=attch.longFilename) - cur_attach = message.get_payload()[-1] + if p := message.get_payload(): + if isinstance(p, list): + cur_attach = p[-1] + else: + cur_attach = p self._update_content_disp_properties(attch, cur_attach) - message.set_boundary(_orig_boundry) # Set back original boundary + if _orig_boundry is not None: + message.set_boundary(_orig_boundry) # Set back original boundary return message @staticmethod - def _update_content_disp_properties(msg_attch, eml_attch): + def _update_content_disp_properties(msg_attch: AttachmentBase, eml_attch: EmailMessage) -> None: """Set Content-Disposition params on binary eml objects You currently have to set non-filename content-disp params by hand in python. @@ -228,14 +248,14 @@ class EMailObject(AbstractMISPObjectGenerator): for num, name in attch_cont_disp_props.items(): try: eml_attch.set_param(name, - email.utils.format_datetime(msg_attch.props[num].value), + email.utils.format_datetime(msg_attch.props.getValue(num)), header='Content-Disposition') except KeyError: # It's fine if they don't have those values pass @property - def attachments(self) -> List[Tuple[str, BytesIO]]: + def attachments(self) -> list[tuple[str | None, BytesIO]]: to_return = [] try: for attachment in self.email.iter_attachments(): @@ -249,7 +269,7 @@ class EMailObject(AbstractMISPObjectGenerator): pass return to_return - def generate_attributes(self): + def generate_attributes(self) -> None: # Attach original & Converted if self.attach_original_email is not None: @@ -261,21 +281,30 @@ class EMailObject(AbstractMISPObjectGenerator): data=self.raw_emails.get('msg')) message = self.email + body: EmailMessage - for _pref, body in message._find_body(message, preferencelist=['plain', 'html']): - comment = "{0} body".format(body.get_content_type()) + if body := message.get_body(preferencelist=['plain']): + comment = f"{body.get_content_type()} body" if self.encapsulated_body == body.get_content_type(): comment += " De-Encapsulated from RTF in original msg." self.add_attribute("email-body", body.get_content(), comment=comment) - headers = ["{}: {}".format(k, v) for k, v in message.items()] + if body := message.get_body(preferencelist=['html']): + comment = f"{body.get_content_type()} body" + if self.encapsulated_body == body.get_content_type(): + comment += " De-Encapsulated from RTF in original msg." + self.add_attribute("email-body", + body.get_content(), + comment=comment) + + headers = [f"{k}: {v}" for k, v in message.items()] if headers: self.add_attribute("header", "\n".join(headers)) - if "Date" in message and message.get('date').datetime is not None: - self.add_attribute("send-date", message.get('date').datetime) + if "Date" in message and message['date'].datetime is not None: + self.add_attribute("send-date", message['date'].datetime) if "To" in message: self.__add_emails("to", message["To"]) @@ -319,31 +348,32 @@ class EMailObject(AbstractMISPObjectGenerator): self.__generate_received() - def __add_emails(self, typ: str, data: str, insert_display_names: bool = True): - addresses = [] - display_names = [] + def __add_emails(self, typ: str, data: str, insert_display_names: bool = True) -> None: + addresses: list[dict[str, str]] = [] + display_names: list[dict[str, str]] = [] for realname, address in email.utils.getaddresses([data]): if address and realname: - addresses.append({"value": address, "comment": "{} <{}>".format(realname, address)}) + addresses.append({"value": address, "comment": f"{realname} <{address}>"}) elif address: addresses.append({"value": address}) else: # parsing failed, skip continue if realname: - display_names.append({"value": realname, "comment": "{} <{}>".format(realname, address)}) + display_names.append({"value": realname, "comment": f"{realname} <{address}>"}) - if addresses: - self.add_attributes(typ, *addresses) + for a in addresses: + self.add_attribute(typ, **a) if insert_display_names and display_names: try: - self.add_attributes("{}-display-name".format(typ), *display_names) + for d in display_names: + self.add_attribute(f"{typ}-display-name", **d) except NewAttributeError: # email object doesn't support display name for all email addrs pass - def __generate_received(self): + def __generate_received(self) -> None: """ Extract IP addresses from received headers that are not private. Also extract hostnames or domains. """ diff --git a/pymisp/tools/ext_lookups.py b/pymisp/tools/ext_lookups.py index 75ca0e1..c4e81ac 100644 --- a/pymisp/tools/ext_lookups.py +++ b/pymisp/tools/ext_lookups.py @@ -1,5 +1,6 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations try: from pymispgalaxies import Clusters # type: ignore @@ -14,7 +15,7 @@ except ImportError: has_pymispgalaxies = False -def revert_tag_from_galaxies(tag): +def revert_tag_from_galaxies(tag: str) -> list[str]: clusters = Clusters() try: return clusters.revert_machinetag(tag) @@ -22,7 +23,7 @@ def revert_tag_from_galaxies(tag): return [] -def revert_tag_from_taxonomies(tag): +def revert_tag_from_taxonomies(tag: str) -> list[str]: taxonomies = Taxonomies() try: return taxonomies.revert_machinetag(tag) @@ -30,7 +31,7 @@ def revert_tag_from_taxonomies(tag): return [] -def search_taxonomies(query): +def search_taxonomies(query: str) -> list[str]: taxonomies = Taxonomies() found = taxonomies.search(query) if not found: @@ -38,6 +39,6 @@ def search_taxonomies(query): return found -def search_galaxies(query): +def search_galaxies(query: str) -> list[str]: clusters = Clusters() return clusters.search(query) diff --git a/pymisp/tools/fail2banobject.py b/pymisp/tools/fail2banobject.py index 5a5a5b3..a8e3fda 100644 --- a/pymisp/tools/fail2banobject.py +++ b/pymisp/tools/fail2banobject.py @@ -1,20 +1,24 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations + +import logging + +from typing import Any from .abstractgenerator import AbstractMISPObjectGenerator -import logging logger = logging.getLogger('pymisp') class Fail2BanObject(AbstractMISPObjectGenerator): - def __init__(self, parameters: dict, strict: bool = True, **kwargs): + def __init__(self, parameters: dict[str, Any], strict: bool = True, **kwargs): # type: ignore[no-untyped-def] super().__init__('fail2ban', strict=strict, **kwargs) self._parameters = parameters self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: timestamp = self._sanitize_timestamp(self._parameters.pop('processing-timestamp', None)) self._parameters['processing-timestamp'] = timestamp super().generate_attributes() diff --git a/pymisp/tools/feed.py b/pymisp/tools/feed.py index f3d937a..0452a25 100644 --- a/pymisp/tools/feed.py +++ b/pymisp/tools/feed.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- + +from __future__ import annotations from pathlib import Path from pymisp import MISPEvent import json -from typing import List -def feed_meta_generator(path: Path): +def feed_meta_generator(path: Path) -> None: manifests = {} - hashes: List[str] = [] + hashes: list[str] = [] for f_name in path.glob('*.json'): if str(f_name.name) == 'manifest.json': diff --git a/pymisp/tools/fileobject.py b/pymisp/tools/fileobject.py index b427db8..f8b277e 100644 --- a/pymisp/tools/fileobject.py +++ b/pymisp/tools/fileobject.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- + +from __future__ import annotations from ..exceptions import InvalidMISPObject from .abstractgenerator import AbstractMISPObjectGenerator @@ -10,7 +11,6 @@ import math from collections import Counter import logging from pathlib import Path -from typing import Union, Optional logger = logging.getLogger('pymisp') @@ -22,7 +22,7 @@ except ImportError: HAS_PYDEEP = False try: - import magic # type: ignore + import magic HAS_MAGIC = True except ImportError: HAS_MAGIC = False @@ -30,12 +30,14 @@ except ImportError: class FileObject(AbstractMISPObjectGenerator): - def __init__(self, filepath: Optional[Union[Path, str]] = None, pseudofile: Optional[BytesIO] = None, filename: Optional[str] = None, **kwargs) -> None: + def __init__(self, filepath: Path | str | None = None, # type: ignore[no-untyped-def] + pseudofile: BytesIO | bytes | None = None, + filename: str | None = None, **kwargs) -> None: super().__init__('file', **kwargs) if not HAS_PYDEEP: - logger.warning("Please install pydeep: pip install git+https://github.com/kbandla/pydeep.git") + logger.warning("pydeep is missing, please install pymisp this way: pip install pymisp[fileobjects]") if not HAS_MAGIC: - logger.warning("Please install python-magic: pip install python-magic.") + logger.warning("python-magic is missing, please install pymisp this way: pip install pymisp[fileobjects]") if filename: # Useful in case the file is copied with a pre-defined name by a script but we want to keep the original name self.__filename = filename @@ -55,10 +57,10 @@ class FileObject(AbstractMISPObjectGenerator): self.__data = self.__pseudofile.getvalue() self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: self.add_attribute('filename', value=self.__filename) - size = self.add_attribute('size-in-bytes', value=len(self.__data)) - if int(size.value) > 0: + self.add_attribute('size-in-bytes', value=len(self.__data)) + if len(self.__data) > 0: self.add_attribute('entropy', value=self.__entropy_H(self.__data)) self.add_attribute('md5', value=md5(self.__data).hexdigest()) self.add_attribute('sha1', value=sha1(self.__data).hexdigest()) diff --git a/pymisp/tools/genericgenerator.py b/pymisp/tools/genericgenerator.py index eeed742..811f604 100644 --- a/pymisp/tools/genericgenerator.py +++ b/pymisp/tools/genericgenerator.py @@ -1,14 +1,16 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations + +from typing import Any from .abstractgenerator import AbstractMISPObjectGenerator -from typing import List class GenericObjectGenerator(AbstractMISPObjectGenerator): # FIXME: this method is different from the master one, and that's probably not a good idea. - def generate_attributes(self, attributes: List[dict]): # type: ignore + def generate_attributes(self, attributes: list[dict[str, Any]]) -> None: # type: ignore[override] """Generates MISPObjectAttributes from a list of dictionaries. Each entry if the list must be in one of the two following formats: * {: } diff --git a/pymisp/tools/geolocationobject.py b/pymisp/tools/geolocationobject.py index 9ecb460..fc995c9 100644 --- a/pymisp/tools/geolocationobject.py +++ b/pymisp/tools/geolocationobject.py @@ -1,20 +1,24 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations + +import logging + +from typing import Any from .abstractgenerator import AbstractMISPObjectGenerator -import logging logger = logging.getLogger('pymisp') class GeolocationObject(AbstractMISPObjectGenerator): - def __init__(self, parameters: dict, strict: bool = True, **kwargs): + def __init__(self, parameters: dict[str, Any], strict: bool = True, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__('geolocation', strict=strict, **kwargs) self._parameters = parameters self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: first = self._sanitize_timestamp(self._parameters.pop('first-seen', None)) self._parameters['first-seen'] = first last = self._sanitize_timestamp(self._parameters.pop('last-seen', None)) diff --git a/pymisp/tools/git_vuln_finder_object.py b/pymisp/tools/git_vuln_finder_object.py index 451d62a..21ec512 100644 --- a/pymisp/tools/git_vuln_finder_object.py +++ b/pymisp/tools/git_vuln_finder_object.py @@ -1,20 +1,24 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations + +import logging + +from typing import Any from .abstractgenerator import AbstractMISPObjectGenerator -import logging logger = logging.getLogger('pymisp') class GitVulnFinderObject(AbstractMISPObjectGenerator): - def __init__(self, parameters: dict, strict: bool = True, **kwargs): + def __init__(self, parameters: dict[str, Any], strict: bool = True, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__('git-vuln-finder', strict=strict, **kwargs) self._parameters = parameters self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: authored_date = self._sanitize_timestamp(self._parameters.pop('authored_date', None)) self._parameters['authored_date'] = authored_date committed_date = self._sanitize_timestamp(self._parameters.pop('committed_date', None)) diff --git a/pymisp/tools/load_warninglists.py b/pymisp/tools/load_warninglists.py index 89ec192..feb5ea3 100644 --- a/pymisp/tools/load_warninglists.py +++ b/pymisp/tools/load_warninglists.py @@ -1,26 +1,30 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations + +from ..api import PyMISP try: - from pymispwarninglists import WarningLists # type: ignore + from pymispwarninglists import WarningLists, WarningList # type: ignore has_pymispwarninglists = True except ImportError: has_pymispwarninglists = False -def from_instance(pymisp_instance, slow_search=False): +def from_instance(pymisp_instance: PyMISP, slow_search: bool=False) -> WarningLists: """Load the warnindlist from an existing MISP instance :pymisp_instance: Already instantialized PyMISP instance.""" - warninglists_index = pymisp_instance.get_warninglists()['Warninglists'] + warninglists_index = pymisp_instance.warninglists(pythonify=True) all_warningslists = [] for warninglist in warninglists_index: - wl = pymisp_instance.get_warninglist(warninglist['Warninglist']['id'])['Warninglist'] - wl['list'] = wl.pop('WarninglistEntry') - all_warningslists.append(wl) + if isinstance(warninglist, WarningList): + wl = pymisp_instance.get_warninglist(warninglist['Warninglist']['id'])['Warninglist'] + wl['list'] = wl.pop('WarninglistEntry') + all_warningslists.append(wl) return WarningLists(slow_search, all_warningslists) -def from_package(slow_search=False): +def from_package(slow_search: bool=False) -> WarningLists: return WarningLists(slow_search) diff --git a/pymisp/tools/machoobject.py b/pymisp/tools/machoobject.py index e2f3747..ad68e46 100644 --- a/pymisp/tools/machoobject.py +++ b/pymisp/tools/machoobject.py @@ -1,16 +1,20 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- + +from __future__ import annotations + +import logging + +from hashlib import md5, sha1, sha256, sha512 +from io import BytesIO +from pathlib import Path +from typing import Any from ..exceptions import InvalidMISPObject -from .abstractgenerator import AbstractMISPObjectGenerator -from io import BytesIO -from hashlib import md5, sha1, sha256, sha512 -import logging -from typing import Optional, Union -from pathlib import Path -from . import FileObject -import lief # type: ignore +from . import FileObject +from .abstractgenerator import AbstractMISPObjectGenerator + +import lief try: import pydeep # type: ignore @@ -21,7 +25,10 @@ except ImportError: logger = logging.getLogger('pymisp') -def make_macho_objects(lief_parsed: lief.Binary, misp_file: FileObject, standalone: bool = True, default_attributes_parameters: dict = {}): +def make_macho_objects(lief_parsed: lief.MachO.Binary, + misp_file: FileObject, + standalone: bool = True, + default_attributes_parameters: dict[str, Any] = {}) -> tuple[FileObject, MachOObject, list[MachOSectionObject]]: macho_object = MachOObject(parsed=lief_parsed, standalone=standalone, default_attributes_parameters=default_attributes_parameters) misp_file.add_reference(macho_object.uuid, 'includes', 'MachO indicators') macho_sections = [] @@ -32,31 +39,43 @@ def make_macho_objects(lief_parsed: lief.Binary, misp_file: FileObject, standalo class MachOObject(AbstractMISPObjectGenerator): - def __init__(self, parsed: Optional[lief.MachO.Binary] = None, filepath: Optional[Union[Path, str]] = None, pseudofile: Optional[BytesIO] = None, **kwargs): + __macho: lief.MachO.Binary + + def __init__(self, parsed: lief.MachO.Binary | lief.MachO.FatBinary | None = None, # type: ignore[no-untyped-def] + filepath: Path | str | None = None, + pseudofile: BytesIO | list[int] | None = None, + **kwargs) -> None: """Creates an MachO object, with lief""" super().__init__('macho', **kwargs) if not HAS_PYDEEP: - logger.warning("Please install pydeep: pip install git+https://github.com/kbandla/pydeep.git") + logger.warning("pydeep is missing, please install pymisp this way: pip install pymisp[fileobjects]") if pseudofile: if isinstance(pseudofile, BytesIO): - self.__macho = lief.MachO.parse(raw=pseudofile.getvalue()) + m = lief.MachO.parse(obj=pseudofile) elif isinstance(pseudofile, bytes): - self.__macho = lief.MachO.parse(raw=pseudofile) + m = lief.MachO.parse(raw=list(pseudofile)) + elif isinstance(pseudofile, list): + m = lief.MachO.parse(raw=pseudofile) else: - raise InvalidMISPObject('Pseudo file can be BytesIO or bytes got {}'.format(type(pseudofile))) + raise InvalidMISPObject(f'Pseudo file can be BytesIO or bytes got {type(pseudofile)}') + if not m: + raise InvalidMISPObject('Unable to parse pseudofile') + self.__macho = m.at(0) elif filepath: - self.__macho = lief.MachO.parse(filepath) + if m := lief.MachO.parse(filepath): + self.__macho = m.at(0) elif parsed: # Got an already parsed blob - if isinstance(parsed, lief.MachO.Binary): + if isinstance(parsed, lief.MachO.FatBinary): + self.__macho = parsed.at(0) + elif isinstance(parsed, lief.MachO.Binary): self.__macho = parsed else: - raise InvalidMISPObject('Not a lief.MachO.Binary: {}'.format(type(parsed))) + raise InvalidMISPObject(f'Not a lief.MachO.Binary: {type(parsed)}') self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: self.add_attribute('type', value=str(self.__macho.header.file_type).split('.')[1]) - self.add_attribute('name', value=self.__macho.name) # General information if self.__macho.has_entrypoint: self.add_attribute('entrypoint-address', value=self.__macho.entrypoint) @@ -66,7 +85,7 @@ class MachOObject(AbstractMISPObjectGenerator): pos = 0 for section in self.__macho.sections: s = MachOSectionObject(section, standalone=self._standalone, default_attributes_parameters=self._default_attributes_parameters) - self.add_reference(s.uuid, 'includes', 'Section {} of MachO'.format(pos)) + self.add_reference(s.uuid, 'includes', f'Section {pos} of MachO') pos += 1 self.sections.append(s) self.add_attribute('number-sections', value=len(self.sections)) @@ -74,19 +93,19 @@ class MachOObject(AbstractMISPObjectGenerator): class MachOSectionObject(AbstractMISPObjectGenerator): - def __init__(self, section: lief.MachO.Section, **kwargs): + def __init__(self, section: lief.MachO.Section, **kwargs) -> None: # type: ignore[no-untyped-def] """Creates an MachO Section object. Object generated by MachOObject.""" # Python3 way # super().__init__('pe-section') - super(MachOSectionObject, self).__init__('macho-section', **kwargs) + super().__init__('macho-section', **kwargs) self.__section = section self.__data = bytes(self.__section.content) self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: self.add_attribute('name', value=self.__section.name) - size = self.add_attribute('size-in-bytes', value=self.__section.size) - if int(size.value) > 0: + self.add_attribute('size-in-bytes', value=self.__section.size) + if int(self.__section.size) > 0: self.add_attribute('entropy', value=self.__section.entropy) self.add_attribute('md5', value=md5(self.__data).hexdigest()) self.add_attribute('sha1', value=sha1(self.__data).hexdigest()) diff --git a/pymisp/tools/microblogobject.py b/pymisp/tools/microblogobject.py index 14adfb0..63d20a1 100644 --- a/pymisp/tools/microblogobject.py +++ b/pymisp/tools/microblogobject.py @@ -1,22 +1,24 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- +from __future__ import annotations + +import logging +from typing import Any # NOTE: Reference on how this module is used: https://vvx7.io/posts/2020/05/misp-slack-bot/ from .abstractgenerator import AbstractMISPObjectGenerator -import logging logger = logging.getLogger('pymisp') class MicroblogObject(AbstractMISPObjectGenerator): - def __init__(self, parameters: dict, strict: bool = True, **kwargs): + def __init__(self, parameters: dict[str, Any], strict: bool = True, **kwargs): # type: ignore[no-untyped-def] super().__init__('microblog', strict=strict, **kwargs) self._parameters = parameters self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: # Raw post. if 'post' in self._parameters: self.add_attribute('post', value=self._parameters['post']) @@ -32,7 +34,7 @@ class MicroblogObject(AbstractMISPObjectGenerator): # Original URL location of the microblog post (potentially malicious. if 'url' in self._parameters: if isinstance(self._parameters.get('url'), list): - for i in self._parameters.get('url'): + for i in self._parameters['url']: self.add_attribute('url', value=i) else: self.add_attribute('url', value=self._parameters['url']) @@ -40,7 +42,7 @@ class MicroblogObject(AbstractMISPObjectGenerator): # Archive of the original document (Internet Archive, Archive.is, etc). if 'archive' in self._parameters: if isinstance(self._parameters.get('archive'), list): - for i in self._parameters.get('archive'): + for i in self._parameters['archive']: self.add_attribute('archive', value=i) else: self.add_attribute('archive', value=self._parameters['archive']) @@ -74,7 +76,7 @@ class MicroblogObject(AbstractMISPObjectGenerator): "Instagram", "Forum", "Other"] if 'type' in self._parameters: if isinstance(self._parameters.get('type'), list): - for i in self._parameters.get('type'): + for i in self._parameters['type']: if i in type_allowed_values: self.add_attribute('type', value=i) else: @@ -85,7 +87,7 @@ class MicroblogObject(AbstractMISPObjectGenerator): type_allowed_values = ["Informative", "Malicious", "Misinformation", "Disinformation", "Unknown"] if 'state' in self._parameters: if isinstance(self._parameters.get('state'), list): - for i in self._parameters.get('state'): + for i in self._parameters['state']: if i in type_allowed_values: self.add_attribute('state', value=i) else: @@ -100,7 +102,7 @@ class MicroblogObject(AbstractMISPObjectGenerator): type_allowed_values = ["Verified", "Unverified", "Unknown"] if 'verified-username' in self._parameters: if isinstance(self._parameters.get('verified-username'), list): - for i in self._parameters.get('verified-username'): + for i in self._parameters['verified-username']: if i in type_allowed_values: self.add_attribute('verified-username', value=i) else: @@ -110,7 +112,7 @@ class MicroblogObject(AbstractMISPObjectGenerator): # embedded-link. if 'embedded-link' in self._parameters: if isinstance(self._parameters.get('embedded-link'), list): - for i in self._parameters.get('embedded-link'): + for i in self._parameters['embedded-link']: self.add_attribute('embedded-link', value=i) else: self.add_attribute('embedded-link', value=self._parameters['embedded-link']) @@ -118,7 +120,7 @@ class MicroblogObject(AbstractMISPObjectGenerator): # embedded-safe-link if 'embedded-safe-link' in self._parameters: if isinstance(self._parameters.get('embedded-safe-link'), list): - for i in self._parameters.get('embedded-safe-link'): + for i in self._parameters['embedded-safe-link']: self.add_attribute('embedded-safe-link', value=i) else: self.add_attribute('embedded-safe-link', value=self._parameters['embedded-safe-link']) @@ -126,7 +128,7 @@ class MicroblogObject(AbstractMISPObjectGenerator): # Hashtag into the microblog post. if 'hashtag' in self._parameters: if isinstance(self._parameters.get('hashtag'), list): - for i in self._parameters.get('hashtag'): + for i in self._parameters['hashtag']: self.add_attribute('hashtag', value=i) else: self.add_attribute('hashtag', value=self._parameters['hashtag']) @@ -134,7 +136,7 @@ class MicroblogObject(AbstractMISPObjectGenerator): # username quoted if 'username-quoted' in self._parameters: if isinstance(self._parameters.get('username-quoted'), list): - for i in self._parameters.get('username-quoted'): + for i in self._parameters['username-quoted']: self.add_attribute('username-quoted', value=i) else: self.add_attribute('username-quoted', value=self._parameters['username-quoted']) diff --git a/pymisp/tools/neo4j.py b/pymisp/tools/neo4j.py index 2656a5b..7a71978 100644 --- a/pymisp/tools/neo4j.py +++ b/pymisp/tools/neo4j.py @@ -1,7 +1,8 @@ -# -*- coding: utf-8 -*- +from __future__ import annotations import glob import os + from .. import MISPEvent try: @@ -13,23 +14,23 @@ except ImportError: class Neo4j(): - def __init__(self, host='localhost:7474', username='neo4j', password='neo4j'): + def __init__(self, host: str='localhost:7474', username: str='neo4j', password: str='neo4j') -> None: if not has_py2neo: raise Exception('py2neo is required, please install: pip install py2neo') authenticate(host, username, password) - self.graph = Graph("http://{}/db/data/".format(host)) + self.graph = Graph(f"http://{host}/db/data/") - def load_events_directory(self, directory): - self.events = [] + def load_events_directory(self, directory: str) -> None: + self.events: list[MISPEvent] = [] for path in glob.glob(os.path.join(directory, '*.json')): e = MISPEvent() e.load(path) self.import_event(e) - def del_all(self): + def del_all(self) -> None: self.graph.delete_all() - def import_event(self, event): + def import_event(self, event: MISPEvent) -> None: tx = self.graph.begin() event_node = Node('Event', uuid=event.uuid, name=event.info) # event_node['distribution'] = event.distribution diff --git a/pymisp/tools/openioc.py b/pymisp/tools/openioc.py index 662b444..488c5b0 100755 --- a/pymisp/tools/openioc.py +++ b/pymisp/tools/openioc.py @@ -1,5 +1,4 @@ - -# -*- coding: utf-8 -*- +from __future__ import annotations import os @@ -156,7 +155,7 @@ def extract_field(report, field_name): def load_openioc_file(openioc_path): if not os.path.exists(openioc_path): raise Exception("Path doesn't exists.") - with open(openioc_path, 'r') as f: + with open(openioc_path) as f: return load_openioc(f) diff --git a/pymisp/tools/peobject.py b/pymisp/tools/peobject.py index 7d0ab3d..08f35cf 100644 --- a/pymisp/tools/peobject.py +++ b/pymisp/tools/peobject.py @@ -1,19 +1,22 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from ..exceptions import InvalidMISPObject -from .abstractgenerator import AbstractMISPObjectGenerator -from io import BytesIO -from hashlib import md5, sha1, sha256, sha512 -from datetime import datetime +from __future__ import annotations + import logging -from typing import Optional, Union -from pathlib import Path + from base64 import b64encode +from datetime import datetime +from hashlib import md5, sha1, sha256, sha512 +from io import BytesIO +from pathlib import Path +from typing import Any from . import FileObject +from .abstractgenerator import AbstractMISPObjectGenerator +from ..exceptions import InvalidMISPObject -import lief # type: ignore +import lief +import lief.PE try: import pydeep # type: ignore @@ -24,7 +27,10 @@ except ImportError: logger = logging.getLogger('pymisp') -def make_pe_objects(lief_parsed: lief.Binary, misp_file: FileObject, standalone: bool = True, default_attributes_parameters: dict = {}): +def make_pe_objects(lief_parsed: lief.PE.Binary, + misp_file: FileObject, + standalone: bool = True, + default_attributes_parameters: dict[str, Any] = {}) -> tuple[FileObject, PEObject, list[PESectionObject]]: pe_object = PEObject(parsed=lief_parsed, standalone=standalone, default_attributes_parameters=default_attributes_parameters) misp_file.add_reference(pe_object.uuid, 'includes', 'PE indicators') pe_sections = [] @@ -35,44 +41,57 @@ def make_pe_objects(lief_parsed: lief.Binary, misp_file: FileObject, standalone: class PEObject(AbstractMISPObjectGenerator): - def __init__(self, parsed: Optional[lief.PE.Binary] = None, filepath: Optional[Union[Path, str]] = None, pseudofile: Optional[BytesIO] = None, **kwargs): + __pe: lief.PE.Binary + + def __init__(self, parsed: lief.PE.Binary | None = None, # type: ignore[no-untyped-def] + filepath: Path | str | None = None, + pseudofile: BytesIO | list[int] | None = None, + **kwargs) -> None: """Creates an PE object, with lief""" super().__init__('pe', **kwargs) if not HAS_PYDEEP: - logger.warning("Please install pydeep: pip install git+https://github.com/kbandla/pydeep.git") + logger.warning("pydeep is missing, please install pymisp this way: pip install pymisp[fileobjects]") if pseudofile: if isinstance(pseudofile, BytesIO): - self.__pe = lief.PE.parse(raw=pseudofile.getvalue()) + p = lief.PE.parse(obj=pseudofile) elif isinstance(pseudofile, bytes): - self.__pe = lief.PE.parse(raw=pseudofile) + p = lief.PE.parse(raw=list(pseudofile)) + elif isinstance(pseudofile, list): + p = lief.PE.parse(raw=pseudofile) else: - raise InvalidMISPObject('Pseudo file can be BytesIO or bytes got {}'.format(type(pseudofile))) + raise InvalidMISPObject(f'Pseudo file can be BytesIO or bytes got {type(pseudofile)}') + if not p: + raise InvalidMISPObject('Unable to parse pseudofile') + self.__pe = p elif filepath: - self.__pe = lief.PE.parse(filepath) + if p := lief.PE.parse(filepath): + self.__pe = p + else: + raise InvalidMISPObject(f'Unable to parse {filepath}') elif parsed: # Got an already parsed blob if isinstance(parsed, lief.PE.Binary): self.__pe = parsed else: - raise InvalidMISPObject('Not a lief.PE.Binary: {}'.format(type(parsed))) + raise InvalidMISPObject(f'Not a lief.PE.Binary: {type(parsed)}') self.generate_attributes() - def _is_exe(self): + def _is_exe(self) -> bool: if not self._is_dll() and not self._is_driver(): - return self.__pe.header.has_characteristic(lief.PE.HEADER_CHARACTERISTICS.EXECUTABLE_IMAGE) + return self.__pe.header.has_characteristic(lief.PE.Header.CHARACTERISTICS.EXECUTABLE_IMAGE) return False - def _is_dll(self): - return self.__pe.header.has_characteristic(lief.PE.HEADER_CHARACTERISTICS.DLL) + def _is_dll(self) -> bool: + return self.__pe.header.has_characteristic(lief.PE.Header.CHARACTERISTICS.DLL) - def _is_driver(self): + def _is_driver(self) -> bool: # List from pefile - system_DLLs = set(('ntoskrnl.exe', 'hal.dll', 'ndis.sys', 'bootvid.dll', 'kdcom.dll')) + system_DLLs = {'ntoskrnl.exe', 'hal.dll', 'ndis.sys', 'bootvid.dll', 'kdcom.dll'} if system_DLLs.intersection([imp.lower() for imp in self.__pe.libraries]): return True return False - def _get_pe_type(self): + def _get_pe_type(self) -> str: if self._is_dll(): return 'dll' elif self._is_driver(): @@ -82,31 +101,27 @@ class PEObject(AbstractMISPObjectGenerator): else: return 'unknown' - def generate_attributes(self): + def generate_attributes(self) -> None: self.add_attribute('type', value=self._get_pe_type()) # General information self.add_attribute('entrypoint-address', value=self.__pe.entrypoint) self.add_attribute('compilation-timestamp', value=datetime.utcfromtimestamp(self.__pe.header.time_date_stamps).isoformat()) self.add_attribute('imphash', value=lief.PE.get_imphash(self.__pe, lief.PE.IMPHASH_MODE.PEFILE)) self.add_attribute('authentihash', value=self.__pe.authentihash_sha256.hex()) - try: - if (self.__pe.has_resources - and self.__pe.resources_manager.has_version - and self.__pe.resources_manager.version.has_string_file_info - and self.__pe.resources_manager.version.string_file_info.langcode_items): - fileinfo = dict(self.__pe.resources_manager.version.string_file_info.langcode_items[0].items.items()) + r_manager = self.__pe.resources_manager + if isinstance(r_manager, lief.PE.ResourcesManager): + version = r_manager.version + if isinstance(version, lief.PE.ResourceVersion) and version.string_file_info is not None: + fileinfo = dict(version.string_file_info.langcode_items[0].items.items()) self.add_attribute('original-filename', value=fileinfo.get('OriginalFilename')) self.add_attribute('internal-filename', value=fileinfo.get('InternalName')) self.add_attribute('file-description', value=fileinfo.get('FileDescription')) self.add_attribute('file-version', value=fileinfo.get('FileVersion')) - self.add_attribute('lang-id', value=self.__pe.resources_manager.version.string_file_info.langcode_items[0].key) self.add_attribute('product-name', value=fileinfo.get('ProductName')) self.add_attribute('product-version', value=fileinfo.get('ProductVersion')) self.add_attribute('company-name', value=fileinfo.get('CompanyName')) self.add_attribute('legal-copyright', value=fileinfo.get('LegalCopyright')) - except lief.read_out_of_bound: - # The file is corrupted - pass + self.add_attribute('lang-id', value=version.string_file_info.langcode_items[0].key) # Sections self.sections = [] if self.__pe.sections: @@ -116,10 +131,14 @@ class PEObject(AbstractMISPObjectGenerator): # Skip section if name is none AND size is 0. continue s = PESectionObject(section, standalone=self._standalone, default_attributes_parameters=self._default_attributes_parameters) - self.add_reference(s.uuid, 'includes', 'Section {} of PE'.format(pos)) + self.add_reference(s.uuid, 'includes', f'Section {pos} of PE') if ((self.__pe.entrypoint >= section.virtual_address) and (self.__pe.entrypoint < (section.virtual_address + section.virtual_size))): - self.add_attribute('entrypoint-section-at-position', value='{}|{}'.format(section.name, pos)) + if isinstance(section.name, bytes): + section_name = section.name.decode() + else: + section_name = section.name + self.add_attribute('entrypoint-section-at-position', value=f'{section_name}|{pos}') pos += 1 self.sections.append(s) self.add_attribute('number-sections', value=len(self.sections)) @@ -139,16 +158,30 @@ class PEObject(AbstractMISPObjectGenerator): class PECertificate(AbstractMISPObjectGenerator): - def __init__(self, certificate: lief.PE.x509, **kwargs): + def __init__(self, certificate: lief.PE.x509, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__('x509') self.__certificate = certificate self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: self.add_attribute('issuer', value=self.__certificate.issuer) self.add_attribute('serial-number', value=self.__certificate.serial_number) - self.add_attribute('validity-not-before', value=datetime(*self.__certificate.valid_from)) - self.add_attribute('validity-not-after', value=datetime(*self.__certificate.valid_to)) + if len(self.__certificate.valid_from) == 6: + self.add_attribute('validity-not-before', + value=datetime(year=self.__certificate.valid_from[0], + month=self.__certificate.valid_from[1], + day=self.__certificate.valid_from[2], + hour=self.__certificate.valid_from[3], + minute=self.__certificate.valid_from[4], + second=self.__certificate.valid_from[5])) + if len(self.__certificate.valid_to) == 6: + self.add_attribute('validity-not-after', + value=datetime(year=self.__certificate.valid_to[0], + month=self.__certificate.valid_to[1], + day=self.__certificate.valid_to[2], + hour=self.__certificate.valid_to[3], + minute=self.__certificate.valid_to[4], + second=self.__certificate.valid_to[5])) self.add_attribute('version', value=self.__certificate.version) self.add_attribute('subject', value=self.__certificate.subject) self.add_attribute('signature_algorithm', value=self.__certificate.signature_algorithm) @@ -157,19 +190,19 @@ class PECertificate(AbstractMISPObjectGenerator): class PESigners(AbstractMISPObjectGenerator): - def __init__(self, signer: lief.PE.SignerInfo, **kwargs): + def __init__(self, signer: lief.PE.SignerInfo, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__('authenticode-signerinfo') self.__signer = signer self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: self.add_attribute('issuer', value=self.__signer.issuer) self.add_attribute('serial-number', value=self.__signer.serial_number) self.add_attribute('version', value=self.__signer.version) - self.add_attribute('digest_algorithm', value=self.__signer.digest_algorithm.name) - self.add_attribute('encryption_algorithm', value=self.__signer.encryption_algorithm.name) + self.add_attribute('digest_algorithm', value=str(self.__signer.digest_algorithm)) + self.add_attribute('encryption_algorithm', value=str(self.__signer.encryption_algorithm)) self.add_attribute('digest-base64', value=b64encode(self.__signer.encrypted_digest)) - info = self.__signer.get_attribute(lief.PE.SIG_ATTRIBUTE_TYPES.SPC_SP_OPUS_INFO) + info: lief.PE.SpcSpOpusInfo = self.__signer.get_attribute(lief.PE.Attribute.TYPE.SPC_SP_OPUS_INFO) # type: ignore[assignment] if info: self.add_attribute('program-name', value=info.program_name) self.add_attribute('url', value=info.more_info) @@ -177,17 +210,17 @@ class PESigners(AbstractMISPObjectGenerator): class PESectionObject(AbstractMISPObjectGenerator): - def __init__(self, section: lief.PE.Section, **kwargs): + def __init__(self, section: lief.PE.Section, **kwargs) -> None: # type: ignore[no-untyped-def] """Creates an PE Section object. Object generated by PEObject.""" super().__init__('pe-section') self.__section = section self.__data = bytes(self.__section.content) self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: self.add_attribute('name', value=self.__section.name) - size = self.add_attribute('size-in-bytes', value=self.__section.size) - if int(size.value) > 0: + self.add_attribute('size-in-bytes', value=self.__section.size) + if int(self.__section.size) > 0: # zero-filled sections can create too many correlations to_ids = float(self.__section.entropy) > 0 disable_correlation = not to_ids diff --git a/pymisp/tools/reportlab_generator.py b/pymisp/tools/reportlab_generator.py index b8f3369..dc4f507 100644 --- a/pymisp/tools/reportlab_generator.py +++ b/pymisp/tools/reportlab_generator.py @@ -1,5 +1,6 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations # Standard imports import base64 @@ -49,7 +50,7 @@ def create_flowable_tag(misp_tag): return [Flowable_Tag(text=misp_tag.name, color=misp_tag.colour, custom_style=col1_style)] -class Flowable_Tag(Flowable): +class Flowable_Tag(Flowable): # type: ignore[misc] """ Custom flowable to handle tags. Draw one Tag with the webview formatting Modified from : http://two.pairlist.net/pipermail/reportlab-users/2005-February/003695.html @@ -108,7 +109,7 @@ class Flowable_Tag(Flowable): LEFT_INTERNAL_PADDING = 2 ELONGATION = LEFT_INTERNAL_PADDING * 2 - p = Paragraph("{}".format(self.choose_good_text_color(), self.text), style=self.custom_style) + p = Paragraph(f"{self.text}", style=self.custom_style) string_width = stringWidth(self.text, self.custom_style.fontName, self.custom_style.fontSize) self.width = string_width + ELONGATION @@ -615,7 +616,7 @@ class Value_Formatter(): curr_uuid = str(is_safe_value(uuid)) curr_baseurl = self.config[moduleconfig[0]] curr_url = uuid_to_url(curr_baseurl, curr_uuid) - html_url = "{}".format(curr_url, safe_string(text)) + html_url = f"{safe_string(text)}" if color: # They want fancy colors @@ -744,7 +745,7 @@ class Value_Formatter(): answer = YES_ANSWER if is_safe_value(published_timestamp): # Published and have published date - answer += '({})'.format(published_timestamp.strftime(EXPORT_DATE_FORMAT)) + answer += f'({published_timestamp.strftime(EXPORT_DATE_FORMAT)})' else: # Published without published date answer += "(no date)" @@ -1091,7 +1092,7 @@ class Event_Metadata(): Paragraph("Related Event #" + str(i + OFFSET), self.sample_style_sheet['Heading4'])) flowable_table.append(Indenter(left=-INDENT_SIZE_HEADING)) - flowable_table += self.create_reduced_flowable_table_from_event(evt) + flowable_table += self.create_reduced_flowable_table_from_event(evt['Event']) i += 1 else: return flowable_table.append(self.value_formatter.get_unoverflowable_paragraph(DEFAULT_VALUE)) diff --git a/pymisp/tools/sbsignatureobject.py b/pymisp/tools/sbsignatureobject.py index ca7ad6e..a7356a3 100644 --- a/pymisp/tools/sbsignatureobject.py +++ b/pymisp/tools/sbsignatureobject.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- + +from __future__ import annotations from .abstractgenerator import AbstractMISPObjectGenerator @@ -8,13 +9,13 @@ class SBSignatureObject(AbstractMISPObjectGenerator): ''' Sandbox Analyzer ''' - def __init__(self, software: str, report: list, **kwargs): + def __init__(self, software: str, report: list[tuple[str, str]], **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__('sb-signature', **kwargs) self._software = software self._report = report self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: ''' Parse the report for relevant attributes ''' self.add_attribute("software", value=self._software) for (signature_name, description) in self._report: diff --git a/pymisp/tools/sshauthkeyobject.py b/pymisp/tools/sshauthkeyobject.py index 8d7d74c..ce6c2fb 100644 --- a/pymisp/tools/sshauthkeyobject.py +++ b/pymisp/tools/sshauthkeyobject.py @@ -1,23 +1,25 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- + +from __future__ import annotations + +import logging + +from io import StringIO +from pathlib import Path from ..exceptions import InvalidMISPObject from .abstractgenerator import AbstractMISPObjectGenerator -from io import StringIO -import logging -from typing import Optional, Union -from pathlib import Path logger = logging.getLogger('pymisp') class SSHAuthorizedKeysObject(AbstractMISPObjectGenerator): - def __init__(self, authorized_keys_path: Optional[Union[Path, str]] = None, authorized_keys_pseudofile: Optional[StringIO] = None, **kwargs): - # PY3 way: + def __init__(self, authorized_keys_path: Path | str | None = None, # type: ignore[no-untyped-def] + authorized_keys_pseudofile: StringIO | None = None, **kwargs): super().__init__('ssh-authorized-keys', **kwargs) if authorized_keys_path: - with open(authorized_keys_path, 'r') as f: + with open(authorized_keys_path) as f: self.__pseudofile = StringIO(f.read()) elif authorized_keys_pseudofile and isinstance(authorized_keys_pseudofile, StringIO): self.__pseudofile = authorized_keys_pseudofile @@ -26,7 +28,7 @@ class SSHAuthorizedKeysObject(AbstractMISPObjectGenerator): self.__data = self.__pseudofile.getvalue() self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: for line in self.__pseudofile: if line.startswith('ssh') or line.startswith('ecdsa'): key = line.split(' ')[1] diff --git a/pymisp/tools/stix.py b/pymisp/tools/stix.py deleted file mode 100644 index 0c0f605..0000000 --- a/pymisp/tools/stix.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- - -try: - from misp_stix_converter.converters.buildMISPAttribute import buildEvent # type: ignore - from misp_stix_converter.converters import convert # type: ignore - from misp_stix_converter.converters.convert import MISPtoSTIX # type: ignore - has_misp_stix_converter = True -except ImportError: - has_misp_stix_converter = False - - -def load_stix(stix, distribution: int = 3, threat_level_id: int = 2, analysis: int = 0): - '''Returns a MISPEvent object from a STIX package''' - if not has_misp_stix_converter: - raise Exception('You need to install misp_stix_converter: pip install git+https://github.com/MISP/MISP-STIX-Converter.git') - stix = convert.load_stix(stix) - return buildEvent(stix, distribution=distribution, - threat_level_id=threat_level_id, analysis=analysis) - - -def make_stix_package(misp_event, to_json: bool = False, to_xml: bool = False): - '''Returns a STIXPackage from a MISPEvent. - - Optionally can return the package in json or xml. - - ''' - if not has_misp_stix_converter: - raise Exception('You need to install misp_stix_converter: pip install git+https://github.com/MISP/MISP-STIX-Converter.git') - package = MISPtoSTIX(misp_event) - if to_json: - return package.to_json() - elif to_xml: - return package.to_xml() - else: - return package diff --git a/pymisp/tools/update_objects.py b/pymisp/tools/update_objects.py index 2bcb6c7..7f1dd25 100644 --- a/pymisp/tools/update_objects.py +++ b/pymisp/tools/update_objects.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- + +from __future__ import annotations import zipfile from io import BytesIO @@ -12,7 +13,7 @@ from ..abstract import resources_path static_repo = "https://github.com/MISP/misp-objects/archive/main.zip" -def update_objects(): +def update_objects() -> None: r = requests.get(static_repo) zipped_repo = BytesIO(r.content) diff --git a/pymisp/tools/urlobject.py b/pymisp/tools/urlobject.py index 485dfb7..3465541 100644 --- a/pymisp/tools/urlobject.py +++ b/pymisp/tools/urlobject.py @@ -1,9 +1,12 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations + +import logging + +from urllib.parse import unquote_plus from .abstractgenerator import AbstractMISPObjectGenerator -import logging -from urllib.parse import unquote_plus try: from pyfaup.faup import Faup # type: ignore @@ -17,13 +20,13 @@ faup = Faup() class URLObject(AbstractMISPObjectGenerator): - def __init__(self, url: str, generate_all=False, **kwargs): + def __init__(self, url: str, generate_all=False, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__('url', **kwargs) self._generate_all = True if generate_all is True else False faup.decode(unquote_plus(url)) self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: self.add_attribute('url', value=faup.url.decode()) if faup.get_host(): self.add_attribute('host', value=faup.get_host()) diff --git a/pymisp/tools/vehicleobject.py b/pymisp/tools/vehicleobject.py index 7d5bc95..75c9a4a 100644 --- a/pymisp/tools/vehicleobject.py +++ b/pymisp/tools/vehicleobject.py @@ -1,8 +1,12 @@ #!/usr/bin/python3 +from __future__ import annotations + import requests import json +from typing import Any + from .abstractgenerator import AbstractMISPObjectGenerator # Original sourcecode: https://github.com/hayk57/MISP_registration_check @@ -11,14 +15,16 @@ from .abstractgenerator import AbstractMISPObjectGenerator class VehicleObject(AbstractMISPObjectGenerator): '''Vehicle object generator out of regcheck.org.uk''' - country_urls = { + country_urls: dict[str, str] = { 'fr': "http://www.regcheck.org.uk/api/reg.asmx/CheckFrance", 'es': "http://www.regcheck.org.uk/api/reg.asmx/CheckSpain", 'uk': "http://www.regcheck.org.uk/api/reg.asmx/Check" } - def __init__(self, country: str, registration: str, username: str, **kwargs): + def __init__(self, country: str, registration: str, username: str, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__('vehicle', **kwargs) + if country not in self.country_urls: + raise ValueError(f"Country {country} not supportet, must be one of {self.country_urls.keys()}") self._country = country self._registration = registration self._username = username @@ -26,10 +32,10 @@ class VehicleObject(AbstractMISPObjectGenerator): self.generate_attributes() @property - def report(self): + def report(self) -> dict[str, Any]: return self._report - def generate_attributes(self): + def generate_attributes(self) -> None: carDescription = self._report["Description"] carMake = self._report["CarMake"]["CurrentTextValue"] carModel = self._report["CarModel"]["CurrentTextValue"] @@ -65,14 +71,14 @@ class VehicleObject(AbstractMISPObjectGenerator): self.add_attribute('date-first-registration', type='text', value=firstRegistration) self.add_attribute('image-url', type='text', value=ImageUrl) - def _query(self): - payload = "RegistrationNumber={}&username={}".format(self._registration, self._username) + def _query(self) -> dict[str, Any]: + payload = f"RegistrationNumber={self._registration}&username={self._username}" headers = { 'Content-Type': "application/x-www-form-urlencoded", 'cache-control': "no-cache", } - response = requests.request("POST", self.country_urls.get(self._country), data=payload, headers=headers) + response = requests.request("POST", self.country_urls[self._country], data=payload, headers=headers) # FIXME: Clean that up. for item in response.text.split(""): if "" in item: diff --git a/pymisp/tools/vtreportobject.py b/pymisp/tools/vtreportobject.py index 4974fdb..5374f5d 100644 --- a/pymisp/tools/vtreportobject.py +++ b/pymisp/tools/vtreportobject.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- + +from __future__ import annotations import re -from typing import Optional +from typing import Any import requests try: - import validators # type: ignore + import validators has_validators = True except ImportError: has_validators = False @@ -24,7 +25,7 @@ class VTReportObject(AbstractMISPObjectGenerator): :indicator: IOC to search VirusTotal for ''' - def __init__(self, apikey: str, indicator: str, vt_proxies: Optional[dict] = None, **kwargs): + def __init__(self, apikey: str, indicator: str, vt_proxies: dict[str, str] | None = None, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__('virustotal-report', **kwargs) indicator = indicator.strip() self._resource_type = self.__validate_resource(indicator) @@ -33,20 +34,20 @@ class VTReportObject(AbstractMISPObjectGenerator): self._report = self.__query_virustotal(apikey, indicator) self.generate_attributes() else: - error_msg = "A valid indicator is required. (One of type url, md5, sha1, sha256). Received '{}' instead".format(indicator) + error_msg = f"A valid indicator is required. (One of type url, md5, sha1, sha256). Received '{indicator}' instead" raise InvalidMISPObject(error_msg) - def get_report(self): + def get_report(self) -> dict[str, Any]: return self._report - def generate_attributes(self): + def generate_attributes(self) -> None: ''' Parse the VirusTotal report for relevant attributes ''' self.add_attribute("last-submission", value=self._report["scan_date"]) self.add_attribute("permalink", value=self._report["permalink"]) ratio = "{}/{}".format(self._report["positives"], self._report["total"]) self.add_attribute("detection-ratio", value=ratio) - def __validate_resource(self, ioc: str): + def __validate_resource(self, ioc: str) -> str | bool: ''' Validate the data type of an indicator. Domains and IP addresses aren't supported because @@ -62,7 +63,7 @@ class VTReportObject(AbstractMISPObjectGenerator): return "file" return False - def __query_virustotal(self, apikey: str, resource: str): + def __query_virustotal(self, apikey: str, resource: str) -> dict[str, Any]: ''' Query VirusTotal for information about an indicator @@ -70,7 +71,7 @@ class VTReportObject(AbstractMISPObjectGenerator): :resource: Indicator to search in VirusTotal ''' - url = "https://www.virustotal.com/vtapi/v2/{}/report".format(self._resource_type) + url = f"https://www.virustotal.com/vtapi/v2/{self._resource_type}/report" params = {"apikey": apikey, "resource": resource} # for now assume we're using a public API key - we'll figure out private keys later if self._proxies: diff --git a/pyproject.toml b/pyproject.toml index 2b90216..98e2acc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,12 @@ [tool.poetry] name = "pymisp" -version = "2.4.166" +version = "2.4.194" description = "Python API for MISP." authors = ["Raphaël Vinot "] license = "BSD-2-Clause" repository = "https://github.com/MISP/PyMISP" documentation = "https://pymisp.readthedocs.io" - readme = "README.md" classifiers=[ @@ -18,16 +17,17 @@ classifiers=[ 'Intended Audience :: Science/Research', 'Intended Audience :: Telecommunications Industry', 'Intended Audience :: Information Technology', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Topic :: Security', 'Topic :: Internet' ] include = [ "CHANGELOG.txt", - "README.md", "pymisp/data/*.json", "pymisp/data/misp-objects/schema_objects.json", "pymisp/data/misp-objects/schema_relationships.json", @@ -41,47 +41,51 @@ include = [ "Source" = "https://github.com/MISP/PyMISP" [tool.poetry.dependencies] -python = "^3.7" -requests = "^2.28.1" -python-dateutil = "^2.8.2" -jsonschema = "^4.17.1" -deprecated = "^1.2.13" -extract_msg = {version = "^0.37.1", optional = true} -RTFDE = {version = "^0.0.2", optional = true} +python = "^3.8" +requests = "^2.32.3" +python-dateutil = "^2.9.0.post0" +deprecated = "^1.2.14" +extract_msg = {version = "^0.48.5", optional = true} +RTFDE = {version = "^0.1.1", optional = true} oletools = {version = "^0.60.1", optional = true} python-magic = {version = "^0.4.27", optional = true} pydeep2 = {version = "^0.5.1", optional = true} -lief = {version = "^0.12.3", optional = true} -beautifulsoup4 = {version = "^4.11.1", optional = true} -validators = {version = "^0.20.0", optional = true} -sphinx-autodoc-typehints = {version = "^1.19.5", optional = true} -recommonmark = {version = "^0.7.1", optional = true} -reportlab = {version = "^3.6.12", optional = true} +lief = {version = "^0.14.1", optional = true} +beautifulsoup4 = {version = "^4.12.3", optional = true} +validators = {version = "^0.30.0", optional = true} +sphinx-autodoc-typehints = {version = "^2.2.2", optional = true, python = ">=3.9"} +docutils = {version = "^0.21.1", optional = true, python = ">=3.9"} +recommonmark = {version = "^0.7.1", optional = true, python = ">=3.9"} +reportlab = {version = "^4.2.2", optional = true} pyfaup = {version = "^1.2", optional = true} -publicsuffixlist = {version = "^0.9.1", optional = true} -chardet = {version = "^5.0.0", optional = true} -urllib3 = {extras = ["brotli"], version = "^1.26.13", optional = true} +publicsuffixlist = {version = "^1.0.1.20240702", optional = true} +urllib3 = {extras = ["brotli"], version = "*", optional = true} +Sphinx = {version = "^7.3.7", python = ">=3.9", optional = true} [tool.poetry.extras] fileobjects = ['python-magic', 'pydeep2', 'lief'] openioc = ['beautifulsoup4'] virustotal = ['validators'] -docs = ['sphinx-autodoc-typehints', 'recommonmark'] +docs = ['sphinx-autodoc-typehints', 'recommonmark', 'sphinx', 'docutils'] pdfexport = ['reportlab'] -url = ['pyfaup', 'chardet'] +url = ['pyfaup'] email = ['extract_msg', "RTFDE", "oletools"] brotli = ['urllib3'] [tool.poetry.group.dev.dependencies] -requests-mock = "^1.10.0" -mypy = "^0.991" -ipython = "^7.34.0" -jupyterlab = "^3.5.0" -types-requests = "^2.28.11.5" -types-python-dateutil = "^2.8.19.4" -types-redis = "^4.3.21.6" +requests-mock = "^1.12.1" +mypy = "^1.10.1" +ipython = [ + {version = "<8.13.0", python = "<3.9"}, + {version = "^8.18.0", python = ">=3.9"}, + {version = "^8.19.0", python = ">=3.10"} +] +jupyterlab = "^4.2.3" +types-requests = "^2.32.0.20240622" +types-python-dateutil = "^2.9.0.20240316" +types-redis = "^4.6.0.20240425" types-Flask = "^1.1.6" -pytest-cov = "^4.0.0" +pytest-cov = "^5.0.0" [build-system] requires = ["poetry_core>=1.1", "setuptools"] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index b88034e..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[metadata] -description-file = README.md diff --git a/setup.py b/setup.py deleted file mode 100644 index d97ddc2..0000000 --- a/setup.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -from os import path - -from setuptools import setup # type: ignore - -import pymisp - -this_directory = path.abspath(path.dirname(__file__)) -with open(path.join(this_directory, 'README.md'), 'r') as f: - long_description = f.read() - -setup( - name='pymisp', - version=pymisp.__version__, - author='Raphaël Vinot', - author_email='raphael.vinot@circl.lu', - maintainer='Raphaël Vinot', - url='https://github.com/MISP/PyMISP', - project_urls={ - 'Documentation': 'https://pymisp.readthedocs.io', - 'Source': 'https://github.com/MISP/PyMISP', - 'Tracker': 'https://github.com/MISP/PyMISP/issues', - }, - description='Python API for MISP.', - long_description=long_description, - long_description_content_type='text/markdown', - packages=['pymisp', 'pymisp.tools'], - classifiers=[ - 'License :: OSI Approved :: BSD License', - 'Development Status :: 5 - Production/Stable', - 'Environment :: Console', - 'Operating System :: POSIX :: Linux', - 'Intended Audience :: Science/Research', - 'Intended Audience :: Telecommunications Industry', - 'Intended Audience :: Information Technology', - 'Programming Language :: Python :: 3.6', - 'Topic :: Security', - 'Topic :: Internet', - ], - install_requires=['requests', - 'python-dateutil', - 'jsonschema', - 'deprecated'], - extras_require={'fileobjects': ['python-magic', 'pydeep2', 'lief>=0.11.0'], - 'neo': ['py2neo'], - 'openioc': ['beautifulsoup4'], - 'virustotal': ['validators'], - 'docs': ['sphinx-autodoc-typehints', 'recommonmark'], - 'pdfexport': ['reportlab']}, - tests_require=[ - 'jsonschema', - 'python-magic', - 'requests-mock' - ], - test_suite="tests.test_mispevent", - include_package_data=True, - package_data={'pymisp': ['data/*.json', - 'data/misp-objects/schema_objects.json', - 'data/misp-objects/schema_relationships.json', - 'data/misp-objects/objects/*/definition.json', - 'data/misp-objects/relationships/definition.json', - 'tools/pdf_fonts/Noto_TTF/*']}, -) diff --git a/tests/csv_testfiles/valid_fieldnames.csv b/tests/csv_testfiles/valid_fieldnames.csv index 6286be0..45ac6bf 100644 --- a/tests/csv_testfiles/valid_fieldnames.csv +++ b/tests/csv_testfiles/valid_fieldnames.csv @@ -1,4 +1,4 @@ -MD5, SHA1, SHA256 +md5, sha1, sha256 644087ccca16d2a728ef7685a4106f09, eabd6974ac71efd72d9e0688d5a6131f336d169c, 385e31c97e3a07bbb81513f0cd0979e64e6b014943902efd002f57b21eadd41e 34187a34d0a3c5d63016c26346371b54, ce8209ff9828aa8cb095bd7d1589fc4d394c298c, 5f815b8a8e77731c9ca2b3a07a27f880ef24d54e458d77bdabbbaf2269fe96c3 871aa15f4d61c85e1284e1be3f99f705, 236eac0b19f91117b27f1b198a4d8490d99ec2e5, b434bccf0a5ff75b27184e661df751466aef69f35fbd7b8b8692302b8b886262 diff --git a/tests/email_testfiles/mail_1.eml b/tests/email_testfiles/mail_1.eml deleted file mode 100644 index d2ae7e9..0000000 --- a/tests/email_testfiles/mail_1.eml +++ /dev/null @@ -1,858 +0,0 @@ -Return-Path: -Delivered-To: kinney@noth.com -Received: (qmail 11769 invoked from network); 22 Aug 2016 14:23:01 -0000 -Received: from smtprelay0207.b.hostedemail.com (HELO smtprelay.b.hostedemail.com) (64.98.42.207) - by smtp.server.net with SMTP; 22 Aug 2016 14:23:01 -0000 -Received: from filter.hostedemail.com (10.5.19.248.rfc1918.com [10.5.19.248]) - by smtprelay06.b.hostedemail.com (Postfix) with ESMTP id 2CC378D014 - for ; Mon, 22 Aug 2016 14:22:58 +0000 (UTC) -Received: from DM6PR06MB4475.namprd06.prod.outlook.com (2603:10b6:207:3d::31) - by BL0PR06MB4465.namprd06.prod.outlook.com with HTTPS id 12345 via - BL0PR02CA0054.NAMPRD02.PROD.OUTLOOK.COM; Mon, 1 Oct 2018 09:49:22 +0000 -Received: from DM3NAM03FT035.eop-NAM03.prod.protection.outlook.com - (2a01:111:f400:7e49::205) by CY4PR0601CA0051.outlook.office365.com - (2603:10b6:910:89::28) with Microsoft SMTP Server (version=TLS1_2, - cipher=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384) id 15.20.1185.23 via Frontend - Transport; Mon, 1 Oct 2018 09:49:21 +0000 -X-Session-Marker: 6A64617A657940616C6578616E646572736D6974682E636F6D -X-Spam-Summary: 69,4.5,0,,d41d8cd98f00b204,suvorov.s@nalg.ru,:,RULES_HIT:46:150:152:379:553:871:967:989:1000:1254:1260:1263:1313:1381:1516:1517:1520:1575:1594:1605:1676:1699:1730:1747:1764:1777:1792:1823:2044:2197:2199:2393:2525:2560:2563:2682:2685:2827:2859:2911:2933:2937:2939:2942:2945:2947:2951:2954:3022:3867:3872:3890:3934:3936:3938:3941:3944:3947:3950:3953:3956:3959:4425:5007:6001:6261:6506:6678:6747:6748:7281:7398:7688:8599:8824:8957:9009:9025:9388:10004:10848:11604:11638:11639:11783:11914:12043:12185:12445:12517:12519:12740:13026:14149:14381:14658:14659:14687:21080:21221:30054:30055:30065:30066,0,RBL:none,CacheIP:none,Bayesian:0.5,0.5,0.5,Netcheck:none,DomainCache:0,MSF:not bulk,SPF:fn,MSBL:0,DNSBL:none,Custom_rules:0:0:0,LFtime:5,LUA_SUMMARY:none -X-HE-Tag: print38_7083d7fd63e24 -X-Filterd-Recvd-Size: 64695 -Received: from computer_3436 (unknown [43.230.105.145]) - (Authenticated sender: jdazey@alexandersmith.com) - by omf06.b.hostedemail.com (Postfix) with ESMTPA - for ; Mon, 22 Aug 2016 14:22:52 +0000 (UTC) -From: =?UTF-8?B?0YHQu9GD0LbQsdCwINCk0J3QoSDQlNCw0L3QuNC40Lsg0KHRg9Cy0L7RgNC+0LI=?= -To: kinney@noth.com -Subject: =?UTF-8?B?0L/QuNGB0YzQvNC+INGD0LLQtdC00L7QvC3QtQ==?= -Content-Type: multipart/mixed; boundary="2NqJR3m2cLnhEraiqXA4Q9hqnmihx7b7" - ---2NqJR3m2cLnhEraiqXA4Q9hqnmihx7b7 -Content-Type: text/html; charset=UTF-8 -Content-Transfer-Encoding: base64 - -0J3QsNC70L7Qs9C+0L/Qu9Cw0YLQtdC70YzRidC40LohPGJyPg0K0JjQvdGE0L7RgNC80LjRgNGD -0LXQvCDQktCw0YEg0L7QsSDQuNC80LXRjtGJ0LXQudGB0Y8g0LfQsNC00L7Qu9C20LXQvdC90L7R -gdGC0LguPHA+DQrQn9GA0L7RgdGM0LHQsCDQvtC30L3QsNC60L7QvNC40YLRjNGB0Y8g0LIg0L/R -gNC40LvQvtC20LXQvdC40LguPHA+DQo8YnI+DQo8YnI+DQrQoSDRg9Cy0LDQttC10L3QuNC10Lws -PHA+DQrQuNC90YHQv9C10LrRgtC+0YAg0KTQndChINCg0KQg0JXQs9C+0YAg0KHRg9Cy0L7RgNC+ -0LIuPGJyPg0KPHA+DQo8YnI+DQo8cD4NCjxwPg0K0JjQvdGE0L7RgNC80LDRhtC40L7QvdC90YvQ -uSDRgNCw0LfQtNC10Ls8YnI+DQo8YnI+DQrQn9GA0LXQt9C40LTQtdC90YIg0L/QvtGA0YPRh9C4 -0Lsg0L/RgNCw0LLQuNGC0LXQu9GM0YHRgtCy0YMg0LfQsNC60YDQtdC/0LjRgtGMINCyINC30LDQ -utC+0L3QtSDRgNC40YHQui3QvtGA0LjQtdC90YLQuNGA0L7QstCw0L3QvdGL0Lkg0L/QvtC00YXQ -vtC0INC6INC/0YDQvtCy0LXRgNC60LDQvDxwPg0KPHA+DQoxNSDQsNCy0LPRg9GB0YLQsCAyMDE2 -PGJyPg0K0JLQsNC70LXRgNC40Y8g0JfQtdC90L7QstC40L3QsDxicj4NCjxwPg0K0J/RgNC10LfQ -uNC00LXQvdGCINC/0L7RgNGD0YfQuNC7INC/0YDQsNCy0LjRgtC10LvRjNGB0YLQstGDINC30LDQ -utGA0LXQv9C40YLRjCDQsiDQt9Cw0LrQvtC90LUg0YDQuNGB0Lot0L7RgNC40LXQvdGC0LjRgNC+ -0LLQsNC90L3Ri9C5INC/0L7QtNGF0L7QtCDQuiDQv9GA0L7QstC10YDQutCw0LzQktCy0LXRgdGC -0Lgg0YLQsNC60L7QuSDQv9C+0LTRhdC+0LQg0L/RgNC4INC+0YDQs9Cw0L3QuNC30LDRhtC40Lgg -0LrQvtC90YLRgNC+0LvRjNC90L4t0L3QsNC00LfQvtGA0L3Ri9GFINC80LXRgNC+0L/RgNC40Y/R -gtC40Lkg0Lgg0LIg0YbQtdC70L7QvCDQvtC/0YLQuNC80LjQt9C40YDQvtCy0LDRgtGMINC/0L7Q -tNC+0LHQvdGL0LUg0LzQtdGA0L7Qv9GA0LjRj9GC0LjRjyDQvdCwINGA0LXQs9C40L7QvdCw0LvR -jNC90L7QvCDQuCDQvNGD0L3QuNGG0LjQv9Cw0LvRjNC90L7QvCDRg9GA0L7QstC90Y/RhSDQn9GA -0LXQt9C40LTQtdC90YIg0KDQpCDQktC70LDQtNC40LzQuNGAINCf0YPRgtC40L0g0L/QvtGA0YPR -h9C40Lsg0LrQsNCx0LzQuNC90YMg0LTQviAzMSDQtNC10LrQsNCx0YDRjyDRgtC10LrRg9GJ0LXQ -s9C+INCz0L7QtNCwLiDQmNC30LzQtdC90LXQvdC40Y8g0LzQvtCz0YPRgiDQutC+0YHQvdGD0YLR -jNGB0Y8g0Lgg0LLQvdC10L/Qu9Cw0L3QvtCy0YvRhSDQv9GA0L7QstC10YDQvtC6LiDQkiDRgdC+ -0L7RgtCy0LXRgtGB0YLQstC40Lgg0YEg0YPQutCw0LfQsNC90LjRj9C80Lgg0L/RgNC10LfQuNC0 -0LXQvdGC0LAsINCT0LXQvdC10YDQsNC70YzQvdCw0Y8g0L/RgNC+0LrRg9GA0LDRgtGD0YDQsCDQ -oNCkINC00L7Qu9C20L3QsCDQstC90LXRgdGC0Lgg0LIg0LfQsNC60L7QvdC+0LTQsNGC0LXQu9GM -0YHRgtCy0L4g0L/QvtC/0YDQsNCy0LrQuCwg0L/RgNC10LTRg9GB0LzQsNGC0YDQuNCy0LDRjtGJ -0LjQtSDRgdC+0LPQu9Cw0YHQvtCy0LDQvdC40LUg0YLQsNC60LjRhSDQv9GA0L7QstC10YDQvtC6 -INGBINC+0YDQs9Cw0L3QsNC80Lgg0L/RgNC+0LrRg9GA0LDRgtGD0YDRiy4g0K3RgtC+INC30LDR -gtGA0L7QvdC10YIg0L/RgNC+0LLQtdGA0LrQuCwg0LjQvdC40YbQuNC40YDRg9C10LzRi9C1INCy -INGB0LLRj9C30Lgg0YEg0L3QsNGA0YPRiNC10L3QuNC10Lwg0L/RgNCw0LIg0L/QvtGC0YDQtdCx -0LjRgtC10LvQtdC5LCDQuCDQvdC10LrQvtGC0L7RgNGL0LUg0LTRgNGD0LPQuNC1LjxwPg0KPGJy -Pg0K0J3QsNGA0Y/QtNGDINGBINGN0YLQuNC8LCDRgNGP0LQg0LfQsNC60L7QvdC+0LTQsNGC0LXQ -u9GM0L3Ri9GFINC40LfQvNC10L3QtdC90LjQuSDQvNC+0LbQtdGCINC60L7RgdC90YPRgtGM0YHR -jyDQtdC00LjQvdC+0LPQviDRgNC10LXRgdGC0YDQsCDQv9GA0L7QstC10YDQvtC6IChwcm92ZXJr -aS5nb3YucnUpLjxwPg0KPGJyPg0K0KLQsNC6LCDQvtGA0LPQsNC90YssINGA0LXQsNC70LjQt9GD -0Y7RidC40LUg0LrQvtC90YLRgNC+0LvRjNC90L4t0L3QsNC00LfQvtGA0L3Ri9C1INC/0L7Qu9C9 -0L7QvNC+0YfQuNGPLCDQtNC+0LvQttC90Ysg0LHRg9C00YPRgiDQv9C+INGB0L7Qs9C70LDRgdC+ -0LLQsNC90LjRjiDRgSDQk9C10L3QtdGA0LDQu9GM0L3QvtC5INC/0YDQvtC60YPRgNCw0YLRg9GA -0L7QuSDQoNCkINGA0LDQt9GA0LDQsdC+0YLQsNGC0Ywg0Lgg0LjQt9C00LDRgtGMINCw0LrRgtGL -LCDRgNC10LPQu9Cw0LzQtdC90YLQuNGA0YPRjtGJ0LjQtSDQv9C+0YDRj9C00L7QuiDQstC90LXR -gdC10L3QuNGPINC40L3RhNC+0YDQvNCw0YbQuNC4INC+INC/0YDQvtCy0LXRgNC60LDRhSDQsiDQ -tdC00LjQvdGL0Lkg0YDQtdC10YHRgtGAINC/0YDQvtCy0LXRgNC+0LouINCQINC30LAg0L3QtdCy -0L3QtdGB0LXQvdC40LUg0YLQsNC60LjRhSDRgdCy0LXQtNC10L3QuNC5INC4INC30LAg0L3QsNGA -0YPRiNC10L3QuNC1INC/0L7RgNGP0LTQutCwINC4INGB0YDQvtC60L7QsiDQuNGFINCy0L3QtdGB -0LXQvdC40Y8g0L/RgNC10LTQu9Cw0LPQsNC10YLRgdGPINGD0YHRgtCw0L3QvtCy0LjRgtGMINCw -0LTQvNC40L3QuNGB0YLRgNCw0YLQuNCy0L3Rg9GOINC+0YLQstC10YLRgdGC0LLQtdC90L3QvtGB -0YLRjC4g0JrQsNC60LjQtSDQuNC80LXQvdC90L4g0L3QsNC60LDQt9Cw0L3QuNGPINC80L7Qs9GD -0YIg0LHRi9GC0Ywg0LLQstC10LTQtdC90Ysg0LIg0JrQvtCQ0J8g0KDQpCDigJMg0L3QtSDRg9GC -0L7Rh9C90Y/QtdGC0YHRjy4g0J7QttC40LTQsNC10YLRgdGPLCDRh9GC0L4g0YPQutCw0LfQsNC9 -0L3Ri9C1INC/0L7RgNGD0YfQtdC90LjRjyDQsdGD0LTRg9GCINC40YHQv9C+0LvQvdC10L3RiyDQ -uiAxINC00LXQutCw0LHRgNGPLjxicj4NCjxwPg0K0JrRgNC+0LzQtSDRgtC+0LPQviwg0LTQviAx -NSDQtNC10LrQsNCx0YDRjyDQk9C10L3QtdGA0LDQu9GM0L3QvtC5INC/0YDQvtC60YPRgNCw0YLR -g9GA0LUg0KDQpCDQvdC10L7QsdGF0L7QtNC40LzQviDQsdGD0LTQtdGCINC00L7RgNCw0LHQvtGC -0LDRgtGMINC10LTQuNC90YvQuSDRgNC10LXRgdGC0YAg0L/RgNC+0LLQtdGA0L7QuiDRgtCw0Los -INGH0YLQvtCx0Ysg0L/RgNC4INCy0L3QtdGB0LXQvdC40Lgg0LIg0L3QtdCz0L4g0LjQvdGE0L7R -gNC80LDRhtC40Lgg0YHRgtCw0LvQviDQstC+0LfQvNC+0LbQvdGL0Lwg0LjRgdC/0L7Qu9GM0LfQ -vtCy0LDQvdC40LUg0YHQstC10LTQtdC90LjQuSDQuNC3INC00YDRg9Cz0LjRhSDQs9C+0YHRg9C0 -0LDRgNGB0YLQstC10L3QvdGL0YUg0LjQvdGE0L7RgNC80LDRhtC40L7QvdC90YvRhSDRgdC40YHR -gtC10LwsINGB0L/RgNCw0LLQvtGH0L3QuNC60L7QsiDQuCDQutC70LDRgdGB0LjRhNC40LrQsNGC -0L7RgNC+0LIuINCi0LDQutC20LUg0LPQu9Cw0LLQsCDQs9C+0YHRg9C00LDRgNGB0YLQstCwINGD -0LrQsNC30LDQuywg0YfRgtC+INC90LXQvtCx0YXQvtC00LjQvNCwINGE0L7RgNC80LDQu9C40LfQ -sNGG0LjRjyDQuCDQutC+0L3QutGA0LXRgtC40LfQsNGG0LjRjyDQstC90L7RgdC40LzQvtC5INCy -INGA0LXQtdGB0YLRgCDQuNC90YTQvtGA0LzQsNGG0LjQuCwg0LXRgdC70Lgg0L7QvdCwINC60LDR -gdCw0LXRgtGB0Y8g0L/RgNCw0LLQvtCy0YvRhSDQvtGB0L3QvtCy0LDQvdC40Lkg0LTQu9GPINC+ -0YDQs9Cw0L3QuNC30LDRhtC40Lgg0L/RgNC+0LLQtdGA0L7Qui4g0KDQtdGH0Ywg0LjQtNC10YIg -0Lgg0L4g0YTQvtGA0LzQsNC70LjQt9Cw0YbQuNC4INGC0YDQtdCx0L7QstCw0L3QuNC5LCDRgdC+ -0LHQu9GO0LTQtdC90LjQtSDQutC+0YLQvtGA0YvRhSDQvtGG0LXQvdC40LLQsNC10YLRgdGPINC/ -0YDQuCDQv9GA0L7QstC10LTQtdC90LjQuCDRgdC+0L7RgtCy0LXRgtGB0YLQstGD0Y7RidC40YUg -0LzQtdGA0L7Qv9GA0LjRj9GC0LjQuSwg0LAg0YLQsNC60LbQtSDRgNC10LfRg9C70YzRgtCw0YLQ -vtCyINC/0YDQvtCy0LXRgNC+0Log0Lgg0L/RgNC40L3Rj9GC0YvRhSDQvNC10YAuPHA+DQo8YnI+ -DQrQn9C+0LzQuNC80L4g0Y3RgtC+0LPQviDQv9GA0LXQt9C40LTQtdC90YIg0L/QvtGA0YPRh9C4 -0Lsg0L/RgNCw0LLQuNGC0LXQu9GM0YHRgtCy0YMg0YDQsNGB0YHQvNC+0YLRgNC10YLRjCDQstC+ -0L/RgNC+0YEg0L4g0YHRgtC40LzRg9C70LjRgNGD0Y7RidC40YUg0LLRi9C/0LvQsNGC0LDRhSDQ -tNC70Y8g0YHQvtGC0YDRg9C00L3QuNC60L7QsiDRhNC10LTQtdGA0LDQu9GM0L3Ri9GFINC40YHQ -v9C+0LvQvdC40YLQtdC70YzQvdGL0YUg0L7RgNCz0LDQvdC+0LIsINC60L7RgtC+0YDRi9C1INC+ -0YHRg9GJ0LXRgdGC0LLQu9GP0Y7RgiDQv9GA0L7QstC10YDQutC4LjxwPg0KPGJyPg0K0J/QviDR -gNC10LfRg9C70YzRgtCw0YLQsNC8INGN0LvQtdC60YLRgNC+0L3QvdC+0LPQviDQsNGD0LrRhtC4 -0L7QvdCwINC30LDQutC70Y7Rh9Cw0YLRjCDQutC+0L3RgtGA0LDQutGCINC90LAg0LHRg9C80LDQ -s9C1INC90LUg0L3Rg9C20L3Qvjxicj4NCjxwPg0KMTIg0LDQstCz0YPRgdGC0LAgMjAxNjxicj4N -CtCS0LDQu9C10YDQuNGPINCX0LXQvdC+0LLQuNC90LA8cD4NCjxicj4NCtCf0L4g0YDQtdC30YPQ -u9GM0YLQsNGC0LDQvCDRjdC70LXQutGC0YDQvtC90L3QvtCz0L4g0LDRg9C60YbQuNC+0L3QsCDQ -t9Cw0LrQu9GO0YfQsNGC0Ywg0LrQvtC90YLRgNCw0LrRgiDQvdCwINCx0YPQvNCw0LPQtSDQvdC1 -INC90YPQttC90L7QnNC40L3RjdC60L7QvdC+0LzRgNCw0LfQstC40YLQuNGPINGA0LDQt9GK0Y/R -gdC90LjQu9C+LCDRh9GC0L4g0L/QviDRgNC10LfRg9C70YzRgtCw0YLQsNC8INGN0LvQtdC60YLR -gNC+0L3QvdC+0LPQviDQsNGD0LrRhtC40L7QvdCwINC/0YDQuCDQvdCw0LvQuNGH0LjQuCDQt9Cw -0LrQu9GO0YfQtdC90L3QvtCz0L4g0LrQvtC90YLRgNCw0LrRgtCwINCyINGN0LvQtdC60YLRgNC+ -0L3QvdC+0Lkg0YTQvtGA0LzQtSDQt9Cw0LrQu9GO0YfQsNGC0Ywg0LrQvtC90YLRgNCw0LrRgiDQ -tdGJ0LUg0Lgg0LIg0L/QuNGB0YzQvNC10L3QvdC+0Lkg0YTQvtGA0LzQtSDQvdCwINCx0YPQvNCw -0LbQvdC+0Lwg0L3QvtGB0LjRgtC10LvQtSDQvdC1INC90YPQttC90L4uINCS0LXQtNC+0LzRgdGC -0LLQviDQv9C+0LTRh9C10YDQutC90YPQu9C+LCDRh9GC0L4g0YLQsNC60LDRjyDQvdC10L7QsdGF -0L7QtNC40LzQvtGB0YLRjCDQvdC1INC/0YDQtdC00YPRgdC80L7RgtGA0LXQvdCwINC00LXQudGB -0YLQstGD0Y7RidC40Lwg0LfQsNC60L7QvdC+0LTQsNGC0LXQu9GM0YHRgtCy0L7QvCAo0L/QuNGB -0YzQvNC+INCc0LjQvdGN0LrQvtC90L7QvNGA0LDQt9Cy0LjRgtC40Y8g0KDQvtGB0YHQuNC4INC+ -0YIgNSDQuNGO0LvRjyAyMDE2INCzLiDihJYg0JQyONC4LTE2ODcpLjxwPg0KPHA+DQrQodC10LPQ -vtC00L3RjyDQtNC70Y8g0YLQvtCz0L4sINGH0YLQvtCx0Ysg0LfQsNC60LvRjtGH0LjRgtGMINGN -0LvQtdC60YLRgNC+0L3QvdGL0Lkg0LrQvtC90YLRgNCw0LrRgiDQvdCwINGN0LvQtdC60YLRgNC+ -0L3QvdC+0Lwg0LDRg9C60YbQuNC+0L3QtSwg0LXQs9C+INC90LXQvtCx0YXQvtC00LjQvNC+INGA -0LDQt9C80LXRgdGC0LjRgtGMINCyINC10LTQuNC90L7QuSDQuNC90YTQvtGA0LzQsNGG0LjQvtC9 -0L3QvtC5INGB0LjRgdGC0LXQvNC1ICjQldCY0KEpLCDQv9GA0LjRh9C10Lwg0L7QvSDQtNC+0LvQ -ttC10L0g0LHRi9GC0Ywg0L/QvtC00L/QuNGB0LDQvSDRg9GB0LjQu9C10L3QvdC+0Lkg0Y3Qu9C1 -0LrRgtGA0L7QvdC90L7QuSDQv9C+0LTQv9C40YHRjNGOINC70LjRhtCwLCDQuNC80LXRjtGJ0LXQ -s9C+INC/0YDQsNCy0L4g0LTQtdC50YHRgtCy0L7QstCw0YLRjCDQvtGCINC40LzQtdC90Lgg0LfQ -sNC60LDQt9GH0LjQutCwLiDQodC00LXQu9Cw0YLRjCDRjdGC0L4g0L3QtdC+0LHRhdC+0LTQuNC8 -0L4g0LIg0YLQtdGH0LXQvdC40LUg0YLRgNC10YUg0YDQsNCx0L7Rh9C40YUg0LTQvdC10Lkg0YEg -0LTQsNGC0Ysg0YDQsNC30LzQtdGJ0LXQvdC40Y8g0L/RgNC+0LXQutGC0LAg0YLQvtCz0L4g0LbQ -tSDQutC+0L3RgtGA0LDQutGC0LAuPGJyPg0KPGJyPg0K0JIg0LrQsNC60LjRhSDRgdC70YPRh9Cw -0Y/RhSDQt9Cw0LrQsNC30YfQuNC6INC+0LHRj9C30LDQvSDQv9GA0L7QstC+0LTQuNGC0Ywg0Y3Q -u9C10LrRgtGA0L7QvdC90YvQuSDQsNGD0LrRhtC40L7QvT8g0KPQt9C90LDQudGC0LUg0LjQtyDQ -vNCw0YLQtdGA0LjQsNC70LAgItCj0YHQu9C+0LLQuNGPINC/0YDQuNC80LXQvdC10L3QuNGPINGN -0LvQtdC60YLRgNC+0L3QvdC+0LPQviDQsNGD0LrRhtC40L7QvdCwIiDQsiAi0K3QvdGG0LjQutC7 -0L7Qv9C10LTQuNC4INGA0LXRiNC10L3QuNC5LiDQk9C+0YHRg9C00LDRgNGB0YLQstC10L3QvdGL -0LUg0Lgg0LrQvtGA0L/QvtGA0LDRgtC40LLQvdGL0LUg0LfQsNC60YPQv9C60LgiINC40L3RgtC1 -0YDQvdC10YIt0LLQtdGA0YHQuNC4INGB0LjRgdGC0LXQvNGLINCT0JDQoNCQ0J3Qoi4g0J/QvtC7 -0YPRh9C40YLQtSDQv9C+0LvQvdGL0Lkg0LTQvtGB0YLRg9C/INC90LAgMyDQtNC90Y8g0LHQtdGB -0L/Qu9Cw0YLQvdC+ITxwPg0K0J/QvtC70YPRh9C40YLRjCDQtNC+0YHRgtGD0L88cD4NCtCj0LrQ -sNC30LDQvdC90YvQuSDQv9GA0L7QtdC60YIg0L/RgNC4INGN0YLQvtC8INGC0L7QttC1INC00L7Q -u9C20LXQvSDQsdGL0YLRjCDQv9C+0LTQv9C40YHQsNC9INGD0YHQuNC70LXQvdC90L7QuSDRjdC7 -0LXQutGC0YDQvtC90L3QvtC5INC/0L7QtNC/0LjRgdGM0Y4sINC90L4g0YPQttC1INC70LjRhtCw -LCDQutC+0YLQvtGA0L7QtSDQuNC80LXQtdGCINC/0YDQsNCy0L4g0LTQtdC50YHRgtCy0L7QstCw -0YLRjCDQvtGCINC40LzQtdC90Lgg0L/QvtCx0LXQtNC40YLQtdC70Y8g0Y3Qu9C10LrRgtGA0L7Q -vdC90L7Qs9C+INCw0YPQutGG0LjQvtC90LAuINCf0L7QvNC40LzQviDQv9GA0L7QtdC60YLQsCDQ -utC+0L3RgtGA0LDQutGC0LAg0YLQsNC60L7QuSDQv9C+0LHQtdC00LjRgtC10LvRjCDQtNC+0LvQ -ttC10L0g0L/RgNC10LTQvtGB0YLQsNCy0LjRgtGMINC+0LHQtdGB0L/QtdGH0LXQvdC40LUg0LjR -gdC/0L7Qu9C90LXQvdC40Y8g0LrQvtC90YLRgNCw0LrRgtCwICjRhy4gNyDRgdGCLiA3MCDQpNC1 -0LTQtdGA0LDQu9GM0L3QvtCz0L4g0LfQsNC60L7QvdCwINC+0YIgNSDQsNC/0YDQtdC70Y8gMjAx -MyDQsy4g4oSWIDQ0LdCk0JcgItCeINC60L7QvdGC0YDQsNC60YLQvdC+0Lkg0YHQuNGB0YLQtdC8 -0LUg0LIg0YHRhNC10YDQtSDQt9Cw0LrRg9C/0L7QuiDRgtC+0LLQsNGA0L7Qsiwg0YDQsNCx0L7R -giwg0YPRgdC70YPQsyDQtNC70Y8g0L7QsdC10YHQv9C10YfQtdC90LjRjyDQs9C+0YHRg9C00LDR -gNGB0YLQstC10L3QvdGL0YUg0Lgg0LzRg9C90LjRhtC40L/QsNC70YzQvdGL0YUg0L3Rg9C20LQi -OyDQtNCw0LvQtdC1IOKAkyDQl9Cw0LrQvtC9IOKEliA0NC3QpNCXKS48cD4NCjxicj4NCtChINC8 -0L7QvNC10L3RgtCwINGA0LDQt9C80LXRidC10L3QuNGPINCyINCV0JjQoSDQv9C+0LTQv9C40YHQ -sNC90L3QvtCz0L4g0LfQsNC60LDQt9GH0LjQutC+0Lwg0LrQvtC90YLRgNCw0LrRgtCwINC+0L0g -0YHRh9C40YLQsNC10YLRgdGPINC30LDQutC70Y7Rh9C10L3QvdGL0LwgKNGHLiA4INGB0YIuIDcw -INCX0LDQutC+0L3QsCDihJYgNDQt0KTQlykuPGJyPg0KPGJyPg0K0J3QsNC/0L7QvNC90LjQvCwg -0L/QviDQtNC10LnRgdGC0LLRg9GO0YnQtdC80YMg0LfQsNC60L7QvdC+0LTQsNGC0LXQu9GM0YHR -gtCy0YMsINGN0LvQtdC60YLRgNC+0L3QvdGL0Lkg0LDRg9C60YbQuNC+0L0g0L/RgNC+0LLQvtC0 -0LjRgtGB0Y8g0L/Rg9GC0LXQvCDRgdC90LjQttC10L3QuNGPINC90LDRh9Cw0LvRjNC90L7QuSAo -0LzQsNC60YHQuNC80LDQu9GM0L3QvtC5KSDRhtC10L3RiyDQutC+0L3RgtGA0LDQutGC0LAsINGD -0LrQsNC30LDQvdC90L7QuSDQsiDQuNC30LLQtdGJ0LXQvdC40Lgg0L4g0L/RgNC+0LLQtdC00LXQ -vdC40Lgg0YLQsNC60L7Qs9C+INCw0YPQutGG0LjQvtC90LAuINCSINC90LXQvCDQvNC+0LPRg9GC -INGD0YfQsNGB0YLQstC+0LLQsNGC0Ywg0YLQvtC70YzQutC+INCw0LrQutGA0LXQtNC40YLQvtCy -0LDQvdC90YvQtSDRg9GH0LDRgdGC0L3QuNC60LgsINCwINC80LXRgdGC0L7QvCDQtdCz0L4g0L/R -gNC+0LLQtdC00LXQvdC40Y8g0Y/QstC70Y/QtdGC0YHRjyDRjdC70LXQutGC0YDQvtC90L3QsNGP -INC/0LvQvtGJ0LDQtNC60LAgKNGB0YIuIDY4INCX0LDQutC+0L3QsCDihJYgNDQt0KTQlykuPGJy -Pg0KPHA+DQo1INC00LXQutCw0LHRgNGPIDIwMTQg0LPQvtC00LAg0YHQvtGB0YLQvtGP0LvQvtGB -0Ywg0LjQvdGC0LXRgNC90LXRgi3QuNC90YLQtdGA0LLRjNGOINGBINC90LDRh9Cw0LvRjNC90LjQ -utC+0Lwg0KPQv9GA0LDQstC70LXQvdC40Y8g0LrQsNC80LXRgNCw0LvRjNC90L7Qs9C+INC60L7Q -vdGC0YDQvtC70Y8g0KTQtdC00LXRgNCw0LvRjNC90L7QuSDQvdCw0LvQvtCz0L7QstC+0Lkg0YHQ -u9GD0LbQsdGLINCh0LDRgtC40L3Ri9C8INCU0LzQuNGC0YDQuNC10Lwg0KHRgtCw0L3QuNGB0LvQ -sNCy0L7QstC40YfQtdC8Ljxicj4NCjxicj4NCtCi0LXQvNCwINC40L3RgtC10YDQvdC10YIt0LjQ -vdGC0LXRgNCy0YzRjjogItCd0LDQu9C+0LMg0L3QsCDQtNC+0LHQsNCy0LvQtdC90L3Rg9GOINGB -0YLQvtC40LzQvtGB0YLRjDog0L3QvtCy0LDRhtC40LggMjAxNSDQs9C+0LTQsCIuPGJyPg0KPHA+ -DQrQktC10LTRg9GJ0LDRjzog0JTQvtCx0YDRi9C5INC00LXQvdGMLCDQlNC80LjRgtGA0LjQuSDQ -odGC0LDQvdC40YHQu9Cw0LLQvtCy0LjRhyEg0KDQsNGB0YHQutCw0LbQuNGC0LUsINC/0L7QttCw -0LvRg9C50YHRgtCwLCDQutCw0LrQuNC1INC90L7QstCw0YbQuNC4LCDRgdCy0Y/Qt9Cw0L3QvdGL -0LUg0YEg0L/RgNC10LTRgdGC0LDQstC70LXQvdC40LXQvCDQvtGC0YfQtdGC0L3QvtGB0YLQuCDQ -v9C+INCd0JTQoSwg0L7QttC40LTQsNGO0YIg0L3QsNC70L7Qs9C+0L/Qu9Cw0YLQtdC70YzRidC4 -0LrQvtCyINCyIDIwMTUg0LPQvtC00YM/PGJyPg0KPGJyPg0K0KHQsNGC0LjQvSDQlC7QoS46INCU -0L7QsdGA0YvQuSDQtNC10L3RjCEg0J3QsNGH0LjQvdCw0Y8g0YEg0L3QsNC70L7Qs9C+0LLQvtCz -0L4g0L/QtdGA0LjQvtC00LAg0LfQsCAxINC60LLQsNGA0YLQsNC7IDIwMTUg0LPQvtC00LAg0L3Q -sCDQvtGB0L3QvtCy0LDQvdC40Lgg0L/Rg9C90LrRgtCwIDUuMSDRgdGC0LDRgtGM0LggMTc0INCa -0L7QtNC10LrRgdCwICjQsiDRgNC10LTQsNC60YbQuNC4INCk0LXQtNC10YDQsNC70YzQvdC+0LPQ -viDQt9Cw0LrQvtC90LAg0L7RgiAyOC4wNi4yMDEzIOKEliAxMzQt0KTQlykg0LIg0L3QsNC70L7Q -s9C+0LLRg9GOINC00LXQutC70LDRgNCw0YbQuNGOINC/0L4g0J3QlNChINCy0LrQu9GO0YfQsNGO -0YLRgdGPINGB0LLQtdC00LXQvdC40Y8sINGD0LrQsNC30LDQvdC90YvQtSDQsiDQutC90LjQs9C1 -INC/0L7QutGD0L/QvtC6INC4INC60L3QuNCz0LUg0L/RgNC+0LTQsNC2LiDQn9GA0Lgg0L7RgdGD -0YnQtdGB0YLQstC70LXQvdC40Lgg0L/QvtGB0YDQtdC00L3QuNGH0LXRgdC60L7QuSDQtNC10Y/R -gtC10LvRjNC90L7RgdGC0Lgg0LIg0L3QsNC70L7Qs9C+0LLRg9GOINC00LXQutC70LDRgNCw0YbQ -uNGOINC/0L4g0J3QlNChINCy0LrQu9GO0YfQsNGO0YLRgdGPINGB0LLQtdC00LXQvdC40Y8sINGD -0LrQsNC30LDQvdC90YvQtSDQsiDQttGD0YDQvdCw0LvQtSDRg9GH0LXRgtCwINC/0L7Qu9GD0YfQ -tdC90L3Ri9GFINC4INCy0YvRgdGC0LDQstC70LXQvdC90YvRhSDRgdGH0LXRgtC+0LIt0YTQsNC6 -0YLRg9GALCDQsiDQvtGC0L3QvtGI0LXQvdC40Lgg0YPQutCw0LfQsNC90L3QvtC5INC00LXRj9GC -0LXQu9GM0L3QvtGB0YLQuC48YnI+DQo8YnI+DQrQn9GA0LjQutCw0LfQvtC8INCk0J3QoSDQoNC+ -0YHRgdC40Lgg0L7RgiAyOS4xMC4yMDE0IOKEliDQnNCc0JItNy0zLzU1OEAg0YPRgtCy0LXRgNC2 -0LTQtdC90LAg0YTQvtGA0LzQsCDQvdCw0LvQvtCz0L7QstC+0Lkg0LTQtdC60LvQsNGA0LDRhtC4 -0Lgg0L/QviDQndCU0KEsINC/0L7RgNGP0LTQvtC6INC10LUg0LfQsNC/0L7Qu9C90LXQvdC40Y8g -0Lgg0YTQvtGA0LzQsNGCINC/0YDQtdC00YHRgtCw0LLQu9C10L3QuNGPINCyINGN0LvQtdC60YLR -gNC+0L3QvdC+0Lkg0YTQvtGA0LzQtS4g0JIg0L3QsNGB0YLQvtGP0YnQtdC1INCy0YDQtdC80Y8g -0L/RgNC40LrQsNC3INC/0YDQvtGF0L7QtNC40YIg0LPQvtGB0YPQtNCw0YDRgdGC0LLQtdC90L3R -g9GOINGA0LXQs9C40YHRgtGA0LDRhtC40Y4g0LIg0JzQuNC90Y7RgdGC0LUg0KDQvtGB0YHQuNC4 -LjxwPg0KPHA+DQrQkiDQvdC+0LLQvtC5INGE0L7RgNC80LUg0L3QsNC70L7Qs9C+0LLQvtC5INC0 -0LXQutC70LDRgNCw0YbQuNC4INC/0L4g0J3QlNChINC/0YDQtdC00YPRgdC80L7RgtGA0LXQvdGL -INGA0LDQt9C00LXQu9GLLCDRgdC+0LTQtdGA0LbQsNGJ0LjQtSDRgdCy0LXQtNC10L3QuNGPINC4 -0Lcg0LrQvdC40LMg0L/QvtC60YPQv9C+0LosINC60L3QuNCzINC/0YDQvtC00LDQtiwg0LbRg9GA -0L3QsNC70L7QsiDRg9GH0LXRgtCwINC/0L7Qu9GD0YfQtdC90L3Ri9GFINC4INCy0YvRgdGC0LDQ -stC70LXQvdC90YvRhSDRgdGH0LXRgtC+0LIt0YTQsNC60YLRg9GALjxicj4NCjxicj4NCtCSINGB -0L7QvtGC0LLQtdGC0YHRgtCy0LjQuCDRgSDQv9GD0L3QutGC0L7QvCAzINGB0YLQsNGC0YzQuCA4 -MCDQuCDQv9GD0L3QutGC0L7QvCA1INGB0YLQsNGC0YzQuCAxNzQg0JrQvtC00LXQutGB0LAg0L3Q -sNC70L7Qs9C+0LLQsNGPINC00LXQutC70LDRgNCw0YbQuNGPINC/0L4g0J3QlNChINC00L7Qu9C2 -0L3QsCDQv9GA0LXQtNGB0YLQsNCy0LvRj9GC0YzRgdGPINCyINGN0LvQtdC60YLRgNC+0L3QvdC+ -0Lkg0YTQvtGA0LzQtSDQv9C+INGC0LXQu9C10LrQvtC80LzRg9C90LjQutCw0YbQuNC+0L3QvdGL -0Lwg0LrQsNC90LDQu9Cw0Lwg0YHQstGP0LfQuCDRh9C10YDQtdC3INC+0L/QtdGA0LDRgtC+0YDQ -sCDRjdC70LXQutGC0YDQvtC90L3QvtCz0L4g0LTQvtC60YPQvNC10L3RgtC+0L7QsdC+0YDQvtGC -0LAuINCi0LDQutC40Lwg0L7QsdGA0LDQt9C+0LwsINGE0L7RgNC80LjRgNC+0LLQsNC90LjQtSDQ -vdCw0LvQvtCz0L7QstC+0Lkg0LTQtdC60LvQsNGA0LDRhtC40Lgg0L/QviDQndCU0KEg0L3QsCDQ -sdGD0LzQsNC20L3Ri9GFINC90L7RgdC40YLQtdC70Y/RhSDQt9Cw0LrQvtC90L7QtNCw0YLQtdC7 -0YzRgdGC0LLQvtC8INC+INC90LDQu9C+0LPQsNGFINC4INGB0LHQvtGA0LDRhSDQvdC1INC/0YDQ -tdC00YPRgdC80L7RgtGA0LXQvdC+Ljxicj4NCjxicj4NCtCb0LjRhtCwLCDQvdC1INGP0LLQu9GP -0Y7RidC40LXRgdGPINC90LDQu9C+0LPQvtC/0LvQsNGC0LXQu9GM0YnQuNC60LDQvNC4INCd0JTQ -oSDQuNC70Lgg0L3QsNC70L7Qs9C+0LLRi9C80Lgg0LDQs9C10L3RgtCw0LzQuCDQv9C+INCd0JTQ -oSwg0L3QviDQvtGB0YPRidC10YHRgtCy0LvRj9GO0YnQuNC1INC/0L7RgdGA0LXQtNC90LjRh9C1 -0YHQutGD0Y4g0LTQtdGP0YLQtdC70YzQvdC+0YHRgtGMLCDQtNC+0LvQttC90Ysg0L3QsCDQvtGB -0L3QvtCy0LDQvdC40Lgg0L/Rg9C90LrRgtCwIDUuMiDRgdGC0LDRgtGM0LggMTc0INCa0L7QtNC1 -0LrRgdCwINC/0YDQtdC00YHRgtCw0LLQu9GP0YLRjCDQsiDQvdCw0LvQvtCz0L7QstGL0Lkg0L7R -gNCz0LDQvSDQsiDQvtGC0L3QvtGI0LXQvdC40Lgg0YPQutCw0LfQsNC90L3QvtC5INC00LXRj9GC -0LXQu9GM0L3QvtGB0YLQuCDQttGD0YDQvdCw0Lsg0YPRh9C10YLQsCDQv9C+0LvRg9GH0LXQvdC9 -0YvRhSDQuCDQstGL0YHRgtCw0LLQu9C10L3QvdGL0YUg0YHRh9C10YLQvtCyLdGE0LDQutGC0YPR -gCDQv9C+INCi0JrQoSDRh9C10YDQtdC3INC+0L/QtdGA0LDRgtC+0YDQsCDQrdCU0J4uPHA+DQo8 -cD4NCtCk0LXQtNC10YDQsNC70YzQvdC+0Lkg0L3QsNC70L7Qs9C+0LLQvtC5INGB0LvRg9C20LHQ -vtC5INGA0LDQt9GA0LDQsdC+0YLQsNC90LAg0LHQtdGB0L/Qu9Cw0YLQvdCw0Y8g0L/RgNC+0LPR -gNCw0LzQvNCwICLQndCw0LvQvtCz0L7Qv9C70LDRgtC10LvRjNGJ0LjQuiDQrtCbIiDQtNC70Y8g -0LLQtdC00LXQvdC40Y8g0LrQvdC40LMg0L/QvtC60YPQv9C+0Log0Lgg0L/RgNC+0LTQsNC2LCDQ -sCDRgtCw0LrQttC1INGE0L7RgNC80LjRgNC+0LLQsNC90LjRjyDQvdCw0LvQvtCz0L7QstC+0Lkg -0LTQtdC60LvQsNGA0LDRhtC40Lgg0L/QviDQndCU0KEuINCU0LDQvdC90YvQvCDQv9GA0L7Qs9GA -0LDQvNC80L3Ri9C8INC/0YDQvtC00YPQutGC0L7QvCDQvNC+0LbQtdGCINCy0L7RgdC/0L7Qu9GM -0LfQvtCy0LDRgtGM0YHRjyDQu9GO0LHQvtC5INC90LDQu9C+0LPQvtC/0LvQsNGC0LXQu9GM0YnQ -uNC6INCx0LXRgdC/0LvQsNGC0L3QviDQt9Cw0LPRgNGD0LfQuNCyINC10LPQviDRgSDQvtGE0LjR -htC40LDQu9GM0L3QvtCz0L4g0YHQsNC50YLQsCDQpNCd0KEg0KDQvtGB0YHQuNC4IChodHRwOi8v -d3d3Lm5hbG9nLnJ1L3JuNzcvL3Byb2dyYW0vYWxsL25hbF91bC8pLjxicj4NCjxicj4NCtCS0LXQ -tNGD0YnQsNGPOiDQlNC80LjRgtGA0LjQuSDQodGC0LDQvdC40YHQu9Cw0LLQvtCy0LjRhywg0LXR -gdGC0Ywg0YLQsNC60LDRjyDRgdC40YLRg9Cw0YbQuNGPOiDQtNC+0LHRgNC+0YHQvtCy0LXRgdGC -0L3QsNGPINC60L7QvNC/0LDQvdC40Y8g0L7RgtGA0LDQt9C40LvQsCDQsiDQtNC10LrQu9Cw0YDQ -sNGG0LjQuCDRgNGP0LQg0YHRh9C10YLQvtCyLdGE0LDQutGC0YPRgCwg0L/QviDQutC+0YLQvtGA -0YvQvCDQv9C+0YHRgtCw0LLRidC40Log0L7RgtGH0LXRgtC90L7RgdGC0Ywg0L3QtSDRgdC00LDQ -uyDQstC+0L7QsdGJ0LUuINCl0L7RgtGPINC90LAg0LzQvtC80LXQvdGCINC/0YDQvtCy0LXQtNC1 -0L3QuNGPINGB0LTQtdC70LrQuCDQutC+0L3RgtGA0LDQs9C10L3RgiDQv9C+INC00LDQvdC90YvQ -vCDQstGL0L/QuNGB0LrQuCDQuNC3INCV0JPQoNCu0Jsg0LHRi9C7INCy0L/QvtC70L3QtSDQtNC1 -0LnRgdGC0LLRg9GO0YnQuNC8LiDQmtCw0LrQuNC1INC/0L7RgdC70LXQtNGB0YLQstC40Y8g0L7Q -ttC40LTQsNGO0YIg0L3QsNC70L7Qs9C+0L/Qu9Cw0YLQtdC70YzRidC40LrQsCDQsiDRgtCw0LrQ -vtC8INGB0LvRg9GH0LDQtT8gPHA+DQo8YnI+DQrQodCw0YLQuNC9INCULtChLjog0JXRgdC70Lgg -0LrQvtC90YLRgNCw0LPQtdC90YIg0L3QsNC70L7Qs9C+0L/Qu9Cw0YLQtdC70YzRidC40LrQsCDQ -vdC1INC/0YDQtdC00YHRgtCw0LLQuNC7INC90LDQu9C+0LPQvtCy0YPRjiDQtNC10LrQu9Cw0YDQ -sNGG0LjRjiDQv9C+INCd0JTQoSDQuNC70Lgg0LIg0LXQs9C+INC/0YDQtdC00YHRgtCw0LLQu9C1 -0L3QvdC+0Lkg0LTQtdC60LvQsNGA0LDRhtC40Lgg0L3QtSDQvtGC0YDQsNC20LXQvdGLINC60LDQ -utC40LUt0LvQuNCx0L4g0YHRh9C10YLQsC3RhNCw0LrRgtGD0YDRiywg0YLQviDRgtCw0LrQvtC5 -INC60L7QvdGC0YDQsNCz0LXQvdGCINGB0YLQsNC90L7QstC40YLRgdGPINC+0LHRitC10LrRgtC+ -0Lwg0LLQvdC40LzQsNC90LjRjyDRgdC+INGB0YLQvtGA0L7QvdGLINC90LDQu9C+0LPQvtCy0L7Q -s9C+INC+0YDQs9Cw0L3QsC4g0J3QsCDQv9C10YDQstC+0Lwg0Y3RgtCw0L/QtSDQsiDRgdC70YPR -h9Cw0LUg0L7RgtGB0YPRgtGB0YLQstC40Y8g0L3QsNC70L7Qs9C+0LLQvtC5INC00LXQutC70LDR -gNCw0YbQuNC4INC90LDQu9C+0LPQvtCy0YvQuSDQvtGA0LPQsNC9INCx0YPQtNC10YIg0LjQvNC1 -0YLRjCDQstC+0LfQvNC+0LbQvdC+0YHRgtGMINC/0YDQuNC+0YHRgtCw0L3QvtCy0LjRgtGMINC0 -0LLQuNC20LXQvdC40LUg0LTQtdC90LXQttC90YvRhSDRgdGA0LXQtNGB0YLQsiDQv9C+INGA0LDR -gdGH0LXRgtC90YvQvCDRgdGH0LXRgtCw0LwuINCU0LDQu9C10LUg0L/RgNC+0LLQvtC00LjRgtGB -0Y8g0YPRgdGC0LDQvdC+0LLQu9C10L3QvdGL0Lkg0LPQu9Cw0LLQvtC5IDE0INCd0LDQu9C+0LPQ -vtCy0L7Qs9C+INC60L7QtNC10LrRgdCwINCg0L7RgdGB0LjQudGB0LrQvtC5INCk0LXQtNC10YDQ -sNGG0LjQuCDQutC+0LzQv9C70LXQutGBINC60L7QvdGC0YDQvtC70YzQvdGL0YUg0LzQtdGA0L7Q -v9GA0LjRj9GC0LjQuSDQv9C+INGE0L7RgNC80LjRgNC+0LLQsNC90LjRjiDQtNC+0LrQsNC30LDR -gtC10LvRjNC90L7QuSDQsdCw0LfRiyDQvtCxINGD0LrQu9C+0L3QtdC90LjQuCDQvtGCINC90LDQ -u9C+0LPQvtC+0LHQu9C+0LbQtdC90LjRjy4g0K3RgtC+INC40YHRgtGA0LXQsdC+0LLQsNC90LjQ -tSDQuNC90YTQvtGA0LzQsNGG0LjQuCDQuCDQtNC+0LrRg9C80LXQvdGC0L7Qsiwg0LAg0L/RgNC4 -INC90LXQvtCx0YXQvtC00LjQvNC+0YHRgtC4INC00YDRg9Cz0LjQtSDQvNC10YDQvtC/0YDQuNGP -0YLQuNGPINC90LDQu9C+0LPQvtCy0L7Qs9C+INC60L7QvdGC0YDQvtC70Y8uINCf0YDQuCDRjdGC -0L7QvCDQsiDRgdC70YPRh9Cw0LUg0YPRgdGC0LDQvdC+0LLQu9C10L3QuNGPINGE0LDQutGC0LAg -0YDQtdCw0LvRjNC90L7RgdGC0Lgg0L7Qv9C10YDQsNGG0LjQuCwg0LTQu9GPINGB0LDQvNC+0LPQ -viDQvdCw0LvQvtCz0L7Qv9C70LDRgtC10LvRjNGJ0LjQutCwINC90Lgg0LrQsNC60LjRhSDQv9C+ -0YHQu9C10LTRgdGC0LLQuNC5INC90LUg0LHRg9C00LXRgi4g0Jog0L7RgtCy0LXRgtGB0YLQstC1 -0L3QvdC+0YHRgtC4INCx0YPQtNC10YIg0L/RgNC40LLQu9C10YfQtdC90L4g0YLQviDQu9C40YbQ -viwg0LrQvtGC0L7RgNC+0LUg0L3QsNGA0YPRiNC40LvQviDQvdCw0LvQvtCz0L7QstC+0LUg0LfQ -sNC60L7QvdC+0LTQsNGC0LXQu9GM0YHRgtCy0L4uPGJyPg0KPHA+DQrQn9GA0Lgg0Y3RgtC+0Lwg -0LzRiyDRgNC10LrQvtC80LXQvdC00YPQtdC8INCw0LrQutGD0YDQsNGC0L3QviDQv9C+0LTRhdC+ -0LTQuNGC0Ywg0Log0LLRi9Cx0L7RgNGDINC/0L7RgtC10L3RhtC40LDQu9GM0L3Ri9GFINC60L7Q -vdGC0YDQsNCz0LXQvdGC0L7QsiDQuCDQv9GA0L7Rj9Cy0LvRj9GC0Ywg0LTQvtC70LbQvdGD0Y4g -0L7RgdC80L7RgtGA0LjRgtC10LvRjNC90L7RgdGC0YwsINGC0LDQuiDQutCw0Log0Y3RgtC+INC9 -0LXQvtCx0YXQvtC00LjQvNC+INC00LvRjyDRgdC+0YXRgNCw0L3QtdC90LjRjyDQtNC10LvQvtCy -0L7QuSDRgNC10L/Rg9GC0LDRhtC40Lgg0Lgg0YTQvtGA0LzQuNGA0L7QstCw0L3QuNGPINC/0L7Q -u9C+0LbQuNGC0LXQu9GM0L3QvtC5INC90LDQu9C+0LPQvtCy0L7QuSDQuNGB0YLQvtGA0LjQuC48 -cD4NCjxicj4NCtCS0LXQtNGD0YnQsNGPOiDQldGB0LvQuCDQsiDRgdCy0LXQtNC10L3QuNGP0YUg -0YMg0LrQvtC90YLRgNCw0LPQtdC90YLQvtCyINCx0YPQtNGD0YIg0YDQsNGB0YXQvtC20LTQtdC9 -0LjRjywg0L3QsNC/0YDQuNC80LXRgCwg0YDQsNC30L3Ri9C1INC90L7QvNC10YDQsCDRgdGH0LXR -gtC+0LIt0YTQsNC60YLRg9GAINC4INGA0LDQt9C90YvQtSDQtNCw0YLRiywg0LrQsNC6INGN0YLQ -viDQsdGD0LTRg9GCINC+0YbQtdC90LjQstCw0YLRjCDQuNC90YHQv9C10LrRgtC+0YDRiz8g0JAg -0LXRgdC70Lgg0L7RgtC70LjRh9Cw0Y7RgtGB0Y8g0YHRg9C80LzRiyDQv9C+INC30LDRgNC10LPQ -uNGB0YLRgNC40YDQvtCy0LDQvdC90YvQvCDRgdGH0LXRgtCw0Lwt0YTQsNC60YLRg9GA0LDQvD8g -PGJyPg0KPGJyPg0K0KHQsNGC0LjQvSDQlC7QoS46INCf0YDQtdC20LTQtSDQstGB0LXQs9C+INCy -0L4g0LjQt9Cx0LXQttCw0L3QuNC1INGC0LXRhdC90LjRh9C10YHQutC40YUg0L7RiNC40LHQvtC6 -INCyINC/0YDQtdC00YHRgtCw0LLQu9C10L3QvdC+0Lkg0LTQtdC60LvQsNGA0LDRhtC40Lgg0L/Q -viDQndCU0KEg0LzRiyDRgNC10LrQvtC80LXQvdC00YPQtdC8INC/0YDQvtCy0LXRgNC40YLRjCDQ -uNC90YTQvtGA0LzQsNGG0LjRjiDQviDQutC+0L3RgtGA0LDQs9C10L3RgtCw0YUsINC60L7RgtC+ -0YDQsNGPINGB0L7QtNC10YDQttC40YLRgdGPINCyINCy0LDRiNC10Lkg0YPRh9C10YLQvdC+0Lkg -KNCx0YPRhdCz0LDQu9GC0LXRgNGB0LrQvtC5KSDRgdC40YHRgtC10LzQtSwg0L3QsCDQv9GA0LXQ -tNC80LXRgiDQv9GA0LDQstC40LvRjNC90L7RgdGC0Lgg0LfQsNC90LXRgdC10L3QuNGPINCyINGB -0LjRgdGC0LXQvNGDINCY0J3QnSDQuCDQmtCf0J8g0LrQvtC90YLRgNCw0LPQtdC90YLQvtCyLiDQ -nNC+0LbQvdC+INCy0L7RgdC/0L7Qu9GM0LfQvtCy0LDRgtGM0YHRjywg0L3QsNC/0YDQuNC80LXR -gCwg0L7QvdC70LDQudC9LdGB0LXRgNCy0LjRgdC+0LwsINGA0LDQt9C80LXRidC10L3QvdGL0Lwg -0L3QsCDQvtGE0LjRhtC40LDQu9GM0L3QvtC8INGB0LDQudGC0LUg0KTQndChINCg0L7RgdGB0LjQ -uCAod3d3Lm5hbG9nLnJ1LCBodHRwOi8vbnBjaGsubmFsb2cucnUpLjxicj4NCjxicj4NCtCV0YHQ -u9C4INGA0LDRgdGF0L7QttC00LXQvdC40Y8g0LIg0YHQstC10LTQtdC90LjRj9GFINC+0LEg0L7Q -v9C10YDQsNGG0LjRj9GFINC60L7QvdGC0YDQsNCz0LXQvdGC0L7QsiDQsdGD0LTRg9GCINCy0YHQ -tS3RgtCw0LrQuCDQstGL0Y/QstC70LXQvdGLLCDRgtC+INC80Ysg0LjRhSDQtNC10LvQuNC8INC9 -0LAg0LTQstC1INC60LDRgtC10LPQvtGA0LjQuC4g0J/QtdGA0LLQsNGPINGN0YLQviDRgtC10YXQ -vdC40YfQtdGB0LrQuNC1INC+0YjQuNCx0LrQuCwg0YLQsNC6INC90LDQt9GL0LLQsNC10LzRi9C1 -ICLQvtGI0LjQsdC60Lgg0LLQstC+0LTQsCIsINC60L7RgtC+0YDRi9C1INC90LUg0LjQvNC10Y7R -giDQstGL0YHQvtC60L7Qs9C+INC/0YDQuNC+0YDQuNGC0LXRgtCwINCyINC+0YLRgNCw0LHQvtGC -0LrQtS4g0J7RiNC40LHQutC4INCyINC90L7QvNC10YDQtSwg0LTQsNGC0LUg0YHRh9C10YLQsC3R -hNCw0LrRgtGD0YDRiyDQuNC70Lgg0LIg0LTRgNGD0LPQvtC8INGA0LXQutCy0LjQt9C40YLQtSwg -0L3QtSDQstC70LjRj9GO0YnQtdC8INC90LAg0YHRg9C80LzRgyDQvdCw0LvQvtCz0LAsINC80Ysg -0YDQsNGB0YHRh9C40YLRi9Cy0LDQtdC8INGD0YHRgtGA0LDQvdGP0YLRjCDQvdCwINGN0YLQsNC/ -0LUg0LfQsNC/0YDQvtGB0LAg0L/QvtGP0YHQvdC10L3QuNC5LiDQn9GA0Lgg0Y3RgtC+0Lwg0L3Q -sNC70L7Qs9C+0L/Qu9Cw0YLQtdC70YzRidC40LrRgyDQv9GA0LXQtNGB0YLQsNCy0LjRgtGB0Y8g -0LLQvtC30LzQvtC20L3QvtGB0YLRjCDQvdCw0L/RgNCw0LLQuNGC0Ywg0LIg0L3QsNC70L7Qs9C+ -0LLRi9C5INC+0YDQs9Cw0L0g0YTQvtGA0LzQsNC70LjQt9C+0LLQsNC90L3Ri9C1INC/0L7Rj9GB -0L3QtdC90LjRjyDQsdC10Lcg0LrQsNC60LjRhS3Qu9C40LHQviDQvdCw0LvQvtCz0L7QstGL0YUg -0L/QvtGB0LvQtdC00YHRgtCy0LjQuS4g0JTQu9GPINGN0YLQvtCz0L4g0L3QsNC00L4g0LHRg9C0 -0LXRgiDRg9C60LDQt9Cw0YLRjCDQsiDRgdC/0LXRhtC40LDQu9GM0L3QvtC5INGA0LXQutC+0LzQ -tdC90LTQvtCy0LDQvdC90L7QuSDRhNC+0YDQvNC1INC60L7RgNGA0LXQutGC0L3Ri9C1INGB0LLQ -tdC00LXQvdC40Y8g0Lgg0L3QsNC/0YDQsNCy0LjRgtGMINC40YUg0L/QviDQotCa0KEg0YfQtdGA -0LXQtyDQvtC/0LXRgNCw0YLQvtGA0LAg0Y3Qu9C10LrRgtGA0L7QvdC90L7Qs9C+INC00L7QutGD -0LzQtdC90YLQvtC+0LHQvtGA0L7RgtCwINCyINC90LDQu9C+0LPQvtCy0YvQtSDQvtGA0LPQsNC9 -0YsuINCf0YDQtdC00YHRgtCw0LLQu9GP0YLRjCDRg9GC0L7Rh9C90LXQvdC90YPRjiDQvdCw0LvQ -vtCz0L7QstGD0Y4g0LTQtdC60LvQsNGA0LDRhtC40Y4g0L/QviDQndCU0KEg0LIg0YLQsNC60L7Q -vCDRgdC70YPRh9Cw0LUg0L3QtSDRgtGA0LXQsdGD0LXRgtGB0Y8uPGJyPg0KPHA+DQrQldGB0LvQ -uCDQttC1INGA0LDRgdGF0L7QttC00LXQvdC40Y8g0LLRi9C30LLQsNC90Ysg0L3QtdCy0LXRgNC9 -0YvQvCDRgNCw0YHRh9C10YLQvtC8INGB0YPQvNC80Ysg0L3QsNC70L7Qs9CwINC40LvQuCDRgdGH -0LXRgi3RhNCw0LrRgtGD0YDQsCDQvtGC0YDQsNC20LXQvSDQvtGI0LjQsdC+0YfQvdC+LCDRgtC+ -INCyINGB0L7QvtGC0LLQtdGC0YHRgtCy0LjQuCDRgdC+INGB0YLQsNGC0YzQtdC5IDgxINCd0LDQ -u9C+0LPQvtCy0L7Qs9C+INC60L7QtNC10LrRgdCwINCg0L7RgdGB0LjQudGB0LrQvtC5INCk0LXQ -tNC10YDQsNGG0LjQuCDQvdC10L7QsdGF0L7QtNC40LzQviDQsdGD0LTQtdGCINC/0YDQtdC00YHR -gtCw0LLQuNGC0Ywg0YPRgtC+0YfQvdC10L3QvdGD0Y4g0L3QsNC70L7Qs9C+0LLRg9GOINC00LXQ -utC70LDRgNCw0YbQuNGOINC/0L4g0J3QlNChLiDQmCDQt9C00LXRgdGMINC/0YDQtdC00YPRgdC8 -0LDRgtGA0LjQstCw0LXRgtGB0Y8g0L3QvtCy0YvQuSDQv9C+0LTRhdC+0LQuINCi0LXQv9C10YDR -jCDQtNC+0L/Rg9GB0LrQsNC10YLRgdGPINC/0YDQtdC00YHRgtCw0LLQu9C10L3QuNC1INGD0YLQ -vtGH0L3QtdC90L3QvtC5INC90LDQu9C+0LPQvtCy0L7QuSDQtNC10LrQu9Cw0YDQsNGG0LjQuCAi -0L3QsCDQtNC10LvRjNGC0YMiLiDQotC+INC10YHRgtGMINCyINGD0YLQvtGH0L3QtdC90L3QvtC5 -INC90LDQu9C+0LPQvtCy0L7QuSDQtNC10LrQu9Cw0YDQsNGG0LjQuCDQt9Cw0L/QvtC70L3Rj9GO -0YLRgdGPINGC0L7Qu9GM0LrQviDRgtC1INGA0LDQt9C00LXQu9GLLCDQsiDQutC+0YLQvtGA0YvQ -tSDQstC90LXRgdC10L3RiyDQuNGB0L/RgNCw0LLQu9C10L3QuNGPLiDQn9C+0LTRgNC+0LHQvdC1 -0LUg0L/QvtGA0Y/QtNC+0Log0LfQsNC/0L7Qu9C90LXQvdC40Y8g0L3QsNC70L7Qs9C+0LLQvtC5 -INC00LXQutC70LDRgNCw0YbQuNC4INC40LfQu9C+0LbQtdC9INCyINC/0YDQuNC60LDQt9C1INCk -0J3QoSDQoNC+0YHRgdC40Lgg0L7RgiAyOS4xMC4yMDE0IOKEliDQnNCc0JItNy0zLzU1OEAuPGJy -Pg0KPHA+DQo8cD4NCtCd0LDQu9C+0LPQvtC/0LvQsNGC0LXQu9GM0YnQuNC60LDQvNC4INC4INC/ -0LvQsNGC0LXQu9GM0YnQuNC60LDQvNC4INGB0LHQvtGA0L7QsiDQv9GA0LjQt9C90LDRjtGC0YHR -jyDQvtGA0LPQsNC90LjQt9Cw0YbQuNC4INC4INGE0LjQt9C40YfQtdGB0LrQuNC1INC70LjRhtCw -LCDQvdCwINC60L7RgtC+0YDRi9GFINCyINGB0L7QvtGC0LLQtdGC0YHRgtCy0LjQuCDRgSDQvdCw -0YHRgtC+0Y/RidC40Lwg0JrQvtC00LXQutGB0L7QvCDQstC+0LfQu9C+0LbQtdC90LAg0L7QsdGP -0LfQsNC90L3QvtGB0YLRjCDRg9C/0LvQsNGH0LjQstCw0YLRjCDRgdC+0L7RgtCy0LXRgtGB0YLQ -stC10L3QvdC+INC90LDQu9C+0LPQuCDQuCAo0LjQu9C4KSDRgdCx0L7RgNGLLjxicj4NCtCSINC/ -0L7RgNGP0LTQutC1LCDQv9GA0LXQtNGD0YHQvNC+0YLRgNC10L3QvdC+0Lwg0L3QsNGB0YLQvtGP -0YnQuNC8INCa0L7QtNC10LrRgdC+0LwsINGE0LjQu9C40LDQu9GLINC4INC40L3Ri9C1INC+0LHQ -vtGB0L7QsdC70LXQvdC90YvQtSDQv9C+0LTRgNCw0LfQtNC10LvQtdC90LjRjyDRgNC+0YHRgdC4 -0LnRgdC60LjRhSDQvtGA0LPQsNC90LjQt9Cw0YbQuNC5INC40YHQv9C+0LvQvdGP0Y7RgiDQvtCx -0Y/Qt9Cw0L3QvdC+0YHRgtC4INGN0YLQuNGFINC+0YDQs9Cw0L3QuNC30LDRhtC40Lkg0L/QviDR -g9C/0LvQsNGC0LUg0L3QsNC70L7Qs9C+0LIg0Lgg0YHQsdC+0YDQvtCyINC/0L4g0LzQtdGB0YLR -gyDQvdCw0YXQvtC20LTQtdC90LjRjyDRjdGC0LjRhSDRhNC40LvQuNCw0LvQvtCyINC4INC40L3R -i9GFINC+0LHQvtGB0L7QsdC70LXQvdC90YvRhSDQv9C+0LTRgNCw0LfQtNC10LvQtdC90LjQuS48 -YnI+DQrQkiDRgdC70YPRh9Cw0Y/RhSwg0L/RgNC10LTRg9GB0LzQvtGC0YDQtdC90L3Ri9GFINC9 -0LDRgdGC0L7Rj9GJ0LjQvCDQmtC+0LTQtdC60YHQvtC8LCDQvdCw0LvQvtCz0L7Qv9C70LDRgtC1 -0LvRjNGJ0LjQutCw0LzQuCDQv9GA0LjQt9C90LDRjtGC0YHRjyDQuNC90L7RgdGC0YDQsNC90L3R -i9C1INGB0YLRgNGD0LrRgtGD0YDRiyDQsdC10Lcg0L7QsdGA0LDQt9C+0LLQsNC90LjRjyDRjtGA -0LjQtNC40YfQtdGB0LrQvtCz0L4g0LvQuNGG0LAuPHA+DQo8cD4NCtCh0YLQsNGC0YzRjyAyMC4g -0JLQt9Cw0LjQvNC+0LfQsNCy0LjRgdC40LzRi9C1INC70LjRhtCwPHA+DQoxLiDQktC30LDQuNC8 -0L7Qt9Cw0LLQuNGB0LjQvNGL0LzQuCDQu9C40YbQsNC80Lgg0LTQu9GPINGG0LXQu9C10Lkg0L3Q -sNC70L7Qs9C+0L7QsdC70L7QttC10L3QuNGPINC/0YDQuNC30L3QsNGO0YLRgdGPINGE0LjQt9C4 -0YfQtdGB0LrQuNC1INC70LjRhtCwINC4ICjQuNC70LgpINC+0YDQs9Cw0L3QuNC30LDRhtC40Lgs -INC+0YLQvdC+0YjQtdC90LjRjyDQvNC10LbQtNGDINC60L7RgtC+0YDRi9C80Lgg0LzQvtCz0YPR -giDQvtC60LDQt9GL0LLQsNGC0Ywg0LLQu9C40Y/QvdC40LUg0L3QsCDRg9GB0LvQvtCy0LjRjyDQ -uNC70Lgg0Y3QutC+0L3QvtC80LjRh9C10YHQutC40LUg0YDQtdC30YPQu9GM0YLQsNGC0Ysg0LjR -hSDQtNC10Y/RgtC10LvRjNC90L7RgdGC0Lgg0LjQu9C4INC00LXRj9GC0LXQu9GM0L3QvtGB0YLQ -uCDQv9GA0LXQtNGB0YLQsNCy0LvRj9C10LzRi9GFINC40LzQuCDQu9C40YYsINCwINC40LzQtdC9 -0L3Qvjo8YnI+DQoxKSDQvtC00L3QsCDQvtGA0LPQsNC90LjQt9Cw0YbQuNGPINC90LXQv9C+0YHR -gNC10LTRgdGC0LLQtdC90L3QviDQuCAo0LjQu9C4KSDQutC+0YHQstC10L3QvdC+INGD0YfQsNGB -0YLQstGD0LXRgiDQsiDQtNGA0YPQs9C+0Lkg0L7RgNCz0LDQvdC40LfQsNGG0LjQuCwg0Lgg0YHR -g9C80LzQsNGA0L3QsNGPINC00L7Qu9GPINGC0LDQutC+0LPQviDRg9GH0LDRgdGC0LjRjyDRgdC+ -0YHRgtCw0LLQu9GP0LXRgiDQsdC+0LvQtdC1IDIwINC/0YDQvtGG0LXQvdGC0L7Qsi4g0JTQvtC7 -0Y8g0LrQvtGB0LLQtdC90L3QvtCz0L4g0YPRh9Cw0YHRgtC40Y8g0L7QtNC90L7QuSDQvtGA0LPQ -sNC90LjQt9Cw0YbQuNC4INCyINC00YDRg9Cz0L7QuSDRh9C10YDQtdC3INC/0L7RgdC70LXQtNC+ -0LLQsNGC0LXQu9GM0L3QvtGB0YLRjCDQuNC90YvRhSDQvtGA0LPQsNC90LjQt9Cw0YbQuNC5INC+ -0L/RgNC10LTQtdC70Y/QtdGC0YHRjyDQsiDQstC40LTQtSDQv9GA0L7QuNC30LLQtdC00LXQvdC4 -0Y8g0LTQvtC70LXQuSDQvdC10L/QvtGB0YDQtdC00YHRgtCy0LXQvdC90L7Qs9C+INGD0YfQsNGB -0YLQuNGPINC+0YDQs9Cw0L3QuNC30LDRhtC40Lkg0Y3RgtC+0Lkg0L/QvtGB0LvQtdC00L7QstCw -0YLQtdC70YzQvdC+0YHRgtC4INC+0LTQvdCwINCyINC00YDRg9Cz0L7QuTs8YnI+DQoyKSDQvtC0 -0L3QviDRhNC40LfQuNGH0LXRgdC60L7QtSDQu9C40YbQviDQv9C+0LTRh9C40L3Rj9C10YLRgdGP -INC00YDRg9Cz0L7QvNGDINGE0LjQt9C40YfQtdGB0LrQvtC80YMg0LvQuNGG0YMg0L/QviDQtNC+ -0LvQttC90L7RgdGC0L3QvtC80YMg0L/QvtC70L7QttC10L3QuNGOOzxicj4NCjMpINC70LjRhtCw -INGB0L7RgdGC0L7Rj9GCINCyINGB0L7QvtGC0LLQtdGC0YHRgtCy0LjQuCDRgSDRgdC10LzQtdC5 -0L3Ri9C8INC30LDQutC+0L3QvtC00LDRgtC10LvRjNGB0YLQstC+0Lwg0KDQvtGB0YHQuNC50YHQ -utC+0Lkg0KTQtdC00LXRgNCw0YbQuNC4INCyINCx0YDQsNGH0L3Ri9GFINC+0YLQvdC+0YjQtdC9 -0LjRj9GFLCDQvtGC0L3QvtGI0LXQvdC40Y/RhSDRgNC+0LTRgdGC0LLQsCDQuNC70Lgg0YHQstC+ -0LnRgdGC0LLQsCwg0YPRgdGL0L3QvtCy0LjRgtC10LvRjyDQuCDRg9GB0YvQvdC+0LLQu9C10L3Q -vdC+0LPQviwg0LAg0YLQsNC60LbQtSDQv9C+0L/QtdGH0LjRgtC10LvRjyDQuCDQvtC/0LXQutCw -0LXQvNC+0LPQvi48cD4NCjIuINCh0YPQtCDQvNC+0LbQtdGCINC/0YDQuNC30L3QsNGC0Ywg0LvQ -uNGG0LAg0LLQt9Cw0LjQvNC+0LfQsNCy0LjRgdC40LzRi9C80Lgg0L/QviDQuNC90YvQvCDQvtGB -0L3QvtCy0LDQvdC40Y/QvCwg0L3QtSDQv9GA0LXQtNGD0YHQvNC+0YLRgNC10L3QvdGL0Lwg0L/R -g9C90LrRgtC+0LwgMSDQvdCw0YHRgtC+0Y/RidC10Lkg0YHRgtCw0YLRjNC4LCDQtdGB0LvQuCDQ -vtGC0L3QvtGI0LXQvdC40Y8g0LzQtdC20LTRgyDRjdGC0LjQvNC4INC70LjRhtCw0LzQuCDQvNC+ -0LPRg9GCINC/0L7QstC70LjRj9GC0Ywg0L3QsCDRgNC10LfRg9C70YzRgtCw0YLRiyDRgdC00LXQ -u9C+0Log0L/QviDRgNC10LDQu9C40LfQsNGG0LjQuCDRgtC+0LLQsNGA0L7QsiAo0YDQsNCx0L7R -giwg0YPRgdC70YPQsykuPGJyPg0KPGJyPg0KPGJyPg0K0J3QtdC00LDQstC90L4gwqvRhtC40YTR -gNC+0LLQsNGPINGN0LrQvtC90L7QvNC40LrQsMK7INC+0LTQtdGA0LbQsNC70LAg0LLQsNC20L3R -g9GOINC/0L7QsdC10LTRgywg0LLRi9C60LjQvdGD0LIg0LjQtyDQv9GP0YLQtdGA0LrQuCDRgdCw -0LzRi9GFINC00L7RgNC+0LPQuNGFINC60L7QvNC/0LDQvdC40Lkg0LzQuNGA0LAg0L/QvtGB0LvQ -tdC00L3QtdCz0L4g0L/RgNC10LTRgdGC0LDQstC40YLQtdC70Y8gwqvRgNC10LDQu9GM0L3QvtCz -0L4g0YHQtdC60YLQvtGA0LDCuyBFeHhvbk1vYmlsLiDQndC+INC/0L7QutCwINCx0LjRgtCy0LAg -wqvRgdC10YDQstC10YDQsCDRgdC+INGB0YLQsNC90LrQvtC8wrsg0L/RgNC40LLQvtC00LjRgiDQ -uiDRgtC+0YDQvNC+0LbQtdC90LjRjiDRjdC60L7QvdC+0LzQuNGH0LXRgdC60L7Qs9C+INGA0L7R -gdGC0LAuINCe0YfQtdCy0LjQtNC90L4sINGH0YLQviDQtNC70Y8g0L7QutC+0L3Rh9Cw0YLQtdC7 -0YzQvdC+0LPQviDRhNC+0YDQvNC40YDQvtCy0LDQvdC40Y8g0L3QvtCy0L7Qs9C+INGN0LrQvtC9 -0L7QvNC40YfQtdGB0LrQvtCz0L4g0YPQutC70LDQtNCwINC80LjRgNGDINC/0YDQuNC00LXRgtGB -0Y8g0L/QtdGA0LXQttC40YLRjCDQtdGJ0LUg0L3QtSDQvtC00L3QviDQv9C+0YLRgNGP0YHQtdC9 -0LjQtS48cD4NCiA8YnI+DQo8cD4NCjxwPg0KPHA+DQog0J/Rj9GC0LXRgNC60YMg0LrRgNGD0L/Q -vdC10LnRiNC40YUg0L/QviDRgNGL0L3QvtGH0L3QvtC5INGB0YLQvtC40LzQvtGB0YLQuCDQutC+ -0LzQv9Cw0L3QuNC5INC+0LrQutGD0L/QuNGA0L7QstCw0LvQuCBJVC3Qs9C40LPQsNC90YLRizxi -cj4NCtCd0LXRhNGC0Y/QvdC40LrQvtCyINGB0LzQtdC90LjQu9C4INGC0LXRhdC90L7Qu9C+0LPQ -uNC4PGJyPg0K0J3QsCDRgdC80LXQvdGDINGA0LXRgdGD0YDRgdC90L7QuSDRjdC60L7QvdC+0LzQ -uNC60LUg0L/RgNC40YXQvtC00LjRgiDRgtC10YXQvdC+0LvQvtCz0LjRh9C10YHQutCw0Y8uINCV -0YHQu9C4INGA0LDQvdGM0YjQtSDQsiDRgtC+0L8tNSDQu9C40LTQtdGA0L7QsiDQv9C+INC60LDQ -v9C40YLQsNC70LjQt9Cw0YbQuNC4IElULdCz0LjQs9Cw0L3RgtGLINC/0L7Qv9Cw0LTQsNC70Lgg -0LvQuNGI0Ywg0LjQt9GA0LXQtNC60LAsINGC0L4g0YLQtdC/0LXRgNGMINCy0YHRji4uLiDihpI8 -YnI+DQrQndC+INC90Lgg0LIg0L7QtNC90L7QvCDQsdCw0L3QutC+0LzQsNGC0LUg0YHQvdGP0YLR -jCDQtNC10L3QtdCzINC90LUg0L/QvtC70YPRh9C40LvQvtGB0YwuINCi0LXQu9C10YTQvtC9INCx -0LDQvdC60LAg0L3QtSDQvtGC0LLQtdGH0LDQuywg0YHQsNC50YIg0LLQuNGB0LXQuy4g0KfQtdGA -0LXQtyDQv9C+0LvRh9Cw0YHQsCDQstGL0Y/RgdC90LjQu9C+0YHRjCwg0YfRgtC+INCx0LDQvdC6 -0L7QstGB0LrQuNC1INGB0LjRgdGC0LXQvNGLINGA0YPRhdC90YPQu9C4LCDQsCDRgdGH0LXRgtCw -INC00LXRgdGP0YLQutC+0LIg0LzQuNC70LvQuNC+0L3QvtCyINCy0LrQu9Cw0LTRh9C40LrQvtCy -INC+0LHQvdGD0LvQtdC90YsuINCU0L7Qu9C70LDRgCwg0YTRg9C90YIg0Lgg0LXQstGA0L4g0L/R -gNC10LLRgNCw0YLQuNC70LjRgdGMINCyINC/0YvQu9GMLCDQt9Cw0YLQviDRgtC1LCDQutGC0L4g -0LfQsNC/0LDRgdCw0LvRgdGPINC30L7Qu9C+0YLQvtC8INC4INC/0L7QutGD0L/QsNC7INC60YDQ -uNC/0YLQvtCy0LDQu9GO0YLRiywg0L/QvtGC0LjRgNCw0LvQuCDRgNGD0LrQuC4g0KHRgtGA0LDR -hSDQuCDRgNCw0YHRgtC10YDRj9C90L3QvtGB0YLRjCDigJQg0LLQvtGCINC00LLQsCDQs9C70LDQ -stC90YvRhSDRgdC70L7QstCwLCDQutC+0YLQvtGA0YvQtSDQsdGL0LvQuCDQsiDQt9Cw0LPQvtC7 -0L7QstC60LDRhSDQvdC+0LLQvtGB0YLQvdGL0YUg0YHQvtC+0LHRidC10L3QuNC5INCy0YHQtdGF -INC40L3RhNC+0YDQvNCw0LPQtdC90YLRgdGC0LIuINCd0L7QstGL0Lkg0LTQuNCy0L3Ri9C5INC8 -0LjRgCDQstGB0YLRgNC10YfQsNC7INGB0LLQvtC40YUg0LjRgdC/0YPQs9Cw0L3QvdGL0YUg0L7Q -sdC40YLQsNGC0LXQu9C10LnigKY8cD4NCjxicj4NCtCa0L7QvdC10YfQvdC+LCDQstC10YHRjNC8 -0LAg0LLQtdGA0L7Rj9GC0L3Qviwg0YfRgtC+INGB0YbQtdC90LDRgNC40LkgwqvRhNC40LvRjNC8 -0LAt0LrQsNGC0LDRgdGC0YDQvtGE0YvCuyDQsiDQttC40LfQvdC4INC90LjQutC+0LPQtNCwINC9 -0LUg0LHRg9C00LXRgiDRgNC10LDQu9C40LfQvtCy0LDQvS4g0J3QviDQstC+0YIg0YfRgtC+INC9 -0LDQtNC+INGH0LXRgtC60L4g0L/QvtC90LjQvNCw0YLRjDog0L3QvtCy0YvQuSDQvNC40YAsINC9 -0L7QstCw0Y8g0YbQuNGE0YDQvtCy0LDRjyDRgNC10LDQu9GM0L3QvtGB0YLRjCDRg9C20LUg0L/R -gNC40LHQuNGA0LDQtdGCINC6INGA0YPQutCw0Lwg0L7QutGA0YPQttCw0Y7RidC40Lkg0LzQuNGA -Ljxicj4NCjxwPg0K0J3QtdC00LDQstC90L4g0LLQv9C10YDQstGL0LUg0LIg0LjRgdGC0L7RgNC4 -0Lgg0L/Rj9GC0LXRgNC60LAg0YHQsNC80YvRhSDQtNC+0YDQvtCz0LjRhSDQutC+0LzQv9Cw0L3Q -uNC5INC80LjRgNCwINGB0YLQsNC70LAg0LjRgdC60LvRjtGH0LjRgtC10LvRjNC90L4gwqvRhtC4 -0YTRgNC+0LLQvtC5wrsuINCf0L7RgdC70LXQtNC90LjQuSDQv9GA0LXQtNGB0YLQsNCy0LjRgtC1 -0LvRjCDCq9GB0YLQsNGA0L7Qs9C+INC80LjRgNCwwrsg4oCUINC90LXRhNGC0Y/QvdC+0Lkg0LPQ -uNCz0LDQvdGCIEV4eG9uTW9iaWwg0LHRi9C7INCy0YvRgtC10YHQvdC10L0g0YEg0L/Rj9GC0L7Q -s9C+INC80LXRgdGC0LAg0LjQvdGC0LXRgNC90LXRgi3QvNCw0LPQsNC30LjQvdC+0LwgQW1hem9u -INC4INGB0L7RhtGB0LXRgtGM0Y4gRmFjZWJvb2suINCf0LXRgNCy0YvQtSDRgtGA0Lgg0L/QvtC3 -0LjRhtC40Lgg0L/RgNC40L3QsNC00LvQtdC20LDRgiBBcHBsZSwgQWxwaGFiZXQgKNC80LDRgtC1 -0YDQuNC90YHQutCw0Y8g0LrQvtC80L/QsNC90LjRjyBHb29nbGUpINC4IE1pY3Jvc29mdC4gwqvQ -r9Cx0LvQvtGH0L3Ri9C5wrsg0LvQuNC00LXRgCDRgdGC0L7QuNGCINC+0LrQvtC70L4gJDU3MCDQ -vNC70YDQtC48YnI+DQo8cD4NCtCf0YDQtdC40LzRg9GJ0LXRgdGC0LLQviBJVC3RgdC10LrRgtC+ -0YDQsCDQvdCw0YDQsNGB0YLQsNC10YIg0YEg0LrQvtC90YbQsCDQtNC10LLRj9C90L7RgdGC0YvR -hSDQs9C+0LTQvtCyINC/0YDQvtGI0LvQvtCz0L4g0LLQtdC60LAuINCi0L7Qs9C00LAg0L/RgNC+ -0LjQt9C+0YjQtdC7INGE0LDQu9GM0YHRgtCw0YDRgiwg0LLRi9C70LjQstGI0LjQudGB0Y8g0LIg -0LrRgNC40LfQuNGBINC00L7RgtC60L7QvNC+0LIsINGH0YPRgtGMINCx0YvQu9C+INC90LUg0L/Q -vtCz0YDRg9C30LjQstGI0LjQuSDQsNC80LXRgNC40LrQsNC90YHQutGD0Y4g0Y3QutC+0L3QvtC8 -0LjQutGDINCyINGA0LXRhtC10YHRgdC40Y4g0LIg0L3QsNGH0LDQu9C1IDIwMDAt0YUg0LPQvtC0 -0L7Qsi4g0J3QviDQuCDRgdC10LnRh9Cw0YEg0L3QvtCy0LDRjyDRhtC40YTRgNC+0LLQsNGPINGN -0YDQsCDQvdC40LrQsNC6INC90LUg0LLRi9Cy0LXQtNC10YIg0Y3QutC+0L3QvtC80LjQutGDINC9 -0LAg0YLRgNCw0LXQutGC0L7RgNC40Y4g0YPRgdGC0L7QudGH0LjQstC+0LPQviDRgNC+0YHRgtCw -LjxwPg0KPHA+DQrQkdC+0LvQtdC1INGC0L7Qs9C+LCDRgdGA0LXQtNC90LjQtSDRgtC10LzQv9GL -INGA0L7RgdGC0LAg0JLQktCfINCh0L7QtdC00LjQvdC10L3QvdGL0YUg0KjRgtCw0YLQvtCyICjQ -uNC80LXQvdC90L4g0LIg0Y3RgtC+0Lkg0YHRgtGA0LDQvdC1INCx0LDQt9C40YDRg9C10YLRgdGP -INCx0L7Qu9GM0YjQuNC90YHRgtCy0L4g0LrRgNGD0L/QvdC10LnRiNC40YUg0LLRi9GB0L7QutC+ -0YLQtdGF0L3QvtC70L7Qs9C40YfQvdGL0YUg0LrQvtGA0L/QvtGA0LDRhtC40LkpINCyIFhYSSDQ -stC10LrQtSDQvdCw0YXQvtC00Y/RgtGB0Y8g0L3QsCDQvNC40L3QuNC80LDQu9GM0L3QvtC8INGD -0YDQvtCy0L3QtSDQv9C+INGB0YDQsNCy0L3QtdC90LjRjiDRgdC+INCy0YLQvtGA0L7QuSDQv9C+ -0LvQvtCy0LjQvdC+0LkgWFgg0LLQtdC60LAuPHA+DQo8cD4NCtCSIDIwMDHigJMyMDE1INCz0L7Q -tNCw0YUg0LDQvNC10YDQuNC60LDQvdGB0LrQsNGPINGN0LrQvtC90L7QvNC40LrQsCDRgNC+0YHQ -u9CwINC90LAgMSw4JSDQsiDRgdGA0LXQtNC90LXQvCDQt9CwINCz0L7QtC4g0KDQvtGB0YIg0LIg -0L/RgNC10LTRi9C00YPRidC40LUg0LTQstC1INC/0Y/RgtC90LDQtNGG0LDRgtC40LvQtdGC0LrQ -uCDRgdC+0YHRgtCw0LLQu9GP0LsgMyw0INC4IDMsMyUsINCwINC00L4g0Y3RgtC+0LPQviDQsdGL -0Lsg0LXRidC1INCy0YvRiNC1ICjQt9C00LXRgdGMINC4INC00LDQu9C10LUg4oCUINC00LDQvdC9 -0YvQtSDQktGB0LXQvNC40YDQvdC+0LPQviDQsdCw0L3QutCwKS48YnI+DQo8cD4NCtCYINGN0YLQ -viDQvdC1INCy0YHQtSDQsNC90YLQuNGA0LXQutC+0YDQtNGLLiDQotC10LzQv9GLINGA0L7RgdGC -0LAg0LfQsCDQv9C+0YHQu9C10LTQvdC40LUg0L/Rj9GC0L3QsNC00YbQsNGC0Ywg0LvQtdGCINC9 -0Lgg0YDQsNC30YMg0L3QtSDQv9GA0LXQstGL0YjQsNC70LggNCUgKNC80LDQutGB0LjQvNGD0Lwg -4oCUIDMsOCUg4oCUINCx0YvQuyDQv9C+0LrQsNC30LDQvSDQsiAyMDA0INCz0L7QtNGDKSwg0YLQ -vtCz0LTQsCDQutCw0Log0YDQsNC90LXQtSDRjdGC0LggNCUg0LHRi9C70Lgg0L7QsdGL0YfQvdGL -0Lwg0LTQtdC70L7QvCAo0L/QuNC6INCyIDcsMjYlINCx0YvQuyDQv9C+0LrQsNC30LDQvSDQsiAx -OTg0INCz0L7QtNGDKS48cD4NCjxwPg0KPHA+DQrQk9C70YPQsdC40L3QsCDQv9Cw0LTQtdC90LjR -jyDRjdC60L7QvdC+0LzQuNC60Lgg0LIgMjAwOSDQs9C+0LTRgyAo4oCTMiw3OCUpINGB0YLQsNC7 -0LAg0LzQsNC60YHQuNC80LDQu9GM0L3QvtC5INGBIDE5NjEg0LPQvtC00LAuINCf0YDQuCDRjdGC -0L7QvCDQvNCw0YHRiNGC0LDQsdGLIMKr0L7RgtGB0LrQvtC60LDCuyDQv9C+0YHQu9C1INGB0L/Q -sNC00LAgKCsyLDUzJSDQsiAyMDEwINCz0L7QtNGDKSDQsdGL0LvQuCDQvNC40L3QuNC80LDQu9GM -0L3Ri9C80Lgg0LfQsCDQstC10YHRjCDQvdCw0LHQu9GO0LTQsNC10LzRi9C5INC/0LXRgNC40L7Q -tC4g0JIg0L7QsdGJ0LXQvCwg0L3QtSDQt9GA0Y8g0L/QvtGB0LvQtdC00L3QuNC5INC60YDQuNC3 -0LjRgSDQv9C+0LvRg9GH0LjQuyDQsiDQqNGC0LDRgtCw0YUg0L3QsNC30LLQsNC90LjQtSDCq9CS -0LXQu9C40LrQsNGPINGA0LXRhtC10YHRgdC40Y/CuyAoR3JlYXQgUmVjZXNzaW9uKS48cD4NCjxi -cj4NCiDQm9Cw0LfQtdGA0Ysg0Lgg0LTRgNC+0L3RiyDQvtGCIEZhY2Vib29rINC4INC00YDRg9Cz -0LjQtSDRgdC/0L7RgdC+0LHRiyDQvtCx0LXRgdC/0LXRh9C40YLRjCDQsdC10LTQvdGL0LUg0YHR -gtGA0LDQvdGLINC40L3RgtC10YDQvdC10YLQvtC8PGJyPg0K0JvQsNC30LXRgNGLINC90LAg0LHQ -tdC00L3QvtGB0YLRjDxicj4NCtCW0LXQu9Cw0L3QuNC1INCc0LDRgNC60LAg0KbRg9C60LXRgNCx -0LXRgNCz0LAg0L/QvtC60YDRi9GC0Ywg0LjQvdGC0LXRgNC90LXRgtC+0Lwg0LLQtdGB0Ywg0LzQ -uNGAINC90LDQsdC40YDQsNC10YIg0L3QvtCy0YvQtSDQvtCx0L7RgNC+0YLRiyDigJQg0Log0LTR -gNC+0L3QsNC8LCDQutC+0YLQvtGA0YvQtSDQsdGD0LTRg9GCINC+0LHQtdGB0L/QtdGH0LjQstCw -0YLRjCDRgdC10YLRjCDQsiDRgtGA0YPQtNC90L7QtNC+0YHRgtGD0L/QvdGL0YUuLi4g4oaSPHA+ -DQrQn9GA0LXQt9C40LTQtdC90YLRgdGC0LLQviDQkdCw0YDQsNC60LAg0J7QsdCw0LzRiyDQvNC+ -0LbQvdC+INCy0L7QvtCx0YnQtSDRgdGH0LjRgtCw0YLRjCDRgdCw0LzRi9C8INC90LXRg9C00LDR -h9C90YvQvCDRgSDRgtC+0YfQutC4INC30YDQtdC90LjRjyDRjdC60L7QvdC+0LzQuNGH0LXRgdC6 -0LjRhSDRg9GB0L/QtdGF0L7Qsi4g0JfQsCDQv9GA0LXQtNGL0LTRg9GJ0LjQtSDRgdC10LzRjCDQ -u9C10YIg0JLQktCfINCh0KjQkCDRg9Cy0LXQu9C40YfQuNCy0LDQu9GB0Y8g0YHRgNC10LTQvdC1 -0LPQvtC00L7QstGL0LzQuCDRgtC10LzQv9Cw0LzQuCAxLDQlLiDQndC4INC/0YDQuCDQvtC00L3Q -vtC8INC/0YDQtdC30LjQtNC10L3RgtC1LCDQvdCw0YfQuNC90LDRjyDRgSDQlNC20L7QvdCwINCa -0LXQvdC90LXQtNC4LCDRgtCw0Log0LzQtdC00LvQtdC90L3QviDRjdC60L7QvdC+0LzQuNC60LAg -0L3QtSDRgNC+0YHQu9CwLjxwPg0KPGJyPg0K0KLQtdC60YPRidC40Lkg0LPQvtC0INC90LUg0L/R -gNC40L3QtdGB0LXRgiDQvdC40YfQtdCz0L4g0YXQvtGA0L7RiNC10LPQvi4g0JIg0L/QtdGA0LLQ -vtC8INC60LLQsNGA0YLQsNC70LUg0JLQktCfINCy0YvRgNC+0YEg0L3QsCAxLDElINCyINCz0L7Q -tNC+0LLQvtC8INC40YHRh9C40YHQu9C10L3QuNC4LCDQstC+INCy0YLQvtGA0L7QvCwg0L/QviDQ -v9C10YDQstC+0Lkg0L7RhtC10L3QutC1LCDQvdCwIDEsMiUuINCf0YDQvtCz0L3QvtC3INC90LAg -0LLQtdGB0Ywg0LPQvtC0IOKAlCAy4oCTMiwyJSwg0YfRgtC+INCx0YPQtNC10YIg0LzQtdC90YzR -iNC1INC/0YDQvtGI0LvQvtCz0L7QtNC90LjRhSAyLDQlLjxwPg0KPHA+DQrQp9C70LXQvSDRgdC+ -0LLQtdGC0LAg0YPQv9GA0LDQstC70Y/RjtGJ0LjRhSDQpNC10LTQtdGA0LDQu9GM0L3QvtC5INGA -0LXQt9C10YDQstC90L7QuSDRgdC40YHRgtC10LzRiyDQodCo0JAg0JTQttC10YDQvtC8INCf0LDR -g9GN0LvQuyDQsiDQuNC90YLQtdGA0LLRjNGOIEZpbmFuY2lhbCBUaW1lcyDQt9Cw0Y/QstC40Lss -INGH0YLQviDQtdGB0YLRjCDRgNC40YHQuiDQstGC0Y/Qs9C40LLQsNC90LjRjyDQsiDQtNC70LjR -gtC10LvRjNC90YvQuSDQv9C10YDQuNC+0LQg0YHQu9Cw0LHQvtCz0L4g0YDQvtGB0YLQsC48YnI+ -DQo8cD4NCtCd0LDQtNC+INGC0LDQutC20LUg0YPRh9C40YLRi9Cy0LDRgtGMLCDRh9GC0L4g0Y3R -gtC+0YIg0LzQuNC90LjQvNCw0LvRjNC90YvQuSDRgNC+0YHRgiDQv9C+0YHQu9C10LTQvdC40YUg -0LvQtdGCINGB0L7Qv9GA0L7QstC+0LbQtNCw0LXRgtGB0Y8g0YPQstC10LvQuNGH0LXQvdC40LXQ -vCDQtNC+0LvQs9CwLiDQk9C+0YHRg9C00LDRgNGB0YLQstC10L3QvdGL0Lkg0LTQvtC70LMg0LLR -i9GA0L7RgSDQt9CwINCy0L7RgdC10LzRjCDQu9C10YIg0L/QvtGH0YLQuCDQsiDQtNCy0LAg0YDQ -sNC30LAg0Lgg0YHQtdC50YfQsNGBINC/0YDQtdCy0YvRiNCw0LXRgiAxMDAlINCS0JLQnyDQodCo -0JAg0Lgg0YHQvtGB0YLQsNCy0LvRj9C10YIgJDE5LDQg0YLRgNC70L0uINCSINGN0YLQvtC8INCz -0L7QtNGDLCDQv9C+INC+0YbQtdC90LrQtSDQsNC80LXRgNC40LrQsNC90YHQutC+0LPQviDQvNC4 -0L3RhNC40L3QsCwg0L7QvSDQstGL0YDQsNGB0YLQtdGCINC10YnQtSDQvdCwICQzODMg0LzQu9GA -0LQuPGJyPg0KPGJyPg0K0J7QtNC90L7QstGA0LXQvNC10L3QvdC+INCk0KDQoSDRg9C00LXRgNC2 -0LjQstCw0LXRgiDQutC70Y7Rh9C10LLRg9GOINGB0YLQsNCy0LrRgyDQvdCwINC80LjQvdC40LzQ -sNC70YzQvdC+0Lwg0YPRgNC+0LLQvdC1INC90LjQttC1IDElINGBINC00LXQutCw0LHRgNGPIDIw -MDgg0LPQvtC00LAuINCX0LAg0Y3RgtC+INCy0YDQtdC80Y8g0LHRi9C70L4g0YDQtdCw0LvQuNC3 -0L7QstCw0L3QviDRgtGA0Lgg0YDQsNGD0L3QtNCwINC/0YDQvtCz0YDQsNC80LzRiyDQutC+0LvQ -uNGH0LXRgdGC0LLQtdC90L3QvtCz0L4g0YHQvNGP0LPRh9C10L3QuNGPIChRRSksINC60L7RgtC+ -0YDQsNGPINC30LDQstC10YDRiNC40LvQsNGB0Ywg0LIg0L7QutGC0Y/QsdGA0LUgMjAxNCDQs9C+ -0LTQsCAo0LIg0YLRgNC10YLRjNC10Lwg0YDQsNGD0L3QtNC1INCyINC80LXRgdGP0YYg0KTQoNCh -INC/0L7QutGD0L/QsNC7INGDINCx0LDQvdC60L7QsiDQs9C+0YHRg9C00LDRgNGB0YLQstC10L3Q -vdGL0YUg0Lgg0LjQv9C+0YLQtdGH0L3Ri9GFINC+0LHQu9C40LPQsNGG0LjQuSDQvdCwICQ4NSDQ -vNC70YDQtCkuPGJyPg0KPHA+DQrQndC+LCDQv9C+0LTRh9C10YDQutC90LXQvCDQtdGJ0LUg0YDQ -sNC3LCDQvdC10YHQvNC+0YLRgNGPINC90LAg0LLRgdC1INGN0YLQuCDRjdC60YHRgtGA0LDQvtGA -0LTQuNC90LDRgNC90YvQtSDQvNC10YDRiywg0YLQtdC80L/RiyDRgNC+0YHRgtCwINC/0YDQtdCx -0YvQstCw0Y7RgiDQvdCwINC80L3QvtCz0L7Qu9C10YLQvdC40YUg0LzQuNC90LjQvNGD0LzQsNGF -LCDQsCDQstGB0Y8g0Y3RgtCwINC40YHRgtC+0YDQuNGPINCy0LXQu9C40LrQvtCz0L4g0YLQvtGA -0LzQvtC20LXQvdC40Y8g0L/RgNC+0LjRgdGF0L7QtNC40YIg0L3QsCDRhNC+0L3QtSDRgNC10LfQ -utC+0LPQviDRg9GB0LjQu9C10L3QuNGPINGA0L7Qu9C4IElULdGB0LXQutGC0L7RgNCwLCDQsCDR -gtCw0LrQttC1INGA0L7RgdGC0LAg0YTQuNC90LDQvdGB0L7QstGL0YUg0YDRi9C90LrQvtCyLjxw -Pg0KPHA+DQrQmtC70Y7Rh9C10LLQvtC5INCx0LjRgNC20LXQstC+0Lkg0LjQvdC00LXQutGBIFMm -UDUwMCDQvdCw0YXQvtC00LjRgtGB0Y8g0L3QsCDQuNGB0YLQvtGA0LjRh9C10YHQutC40YUg0LzQ -sNC60YHQuNC80YPQvNCw0YUuINCQ0L3QsNC70LjRgtC40LrQuCBHb2xkbWFuIFNhY2hzINC/0YDQ -tdC00YPQv9GA0LXQttC00LDRjtGCOiDQutC+0L3QtdGGINGA0LDQu9C70Lgg0LHQu9C40LfQvtC6 -LiDQn9GA0LXQtNGL0LTRg9GJ0LjQtSDQv9C10YDQuNC+0LTRiyDRgNC+0YHRgtCwICjQsiAxOTg0 -4oCTMTk4NyDQuCAxOTk04oCTMTk5OSDQs9C+0LTQsNGFKSDQt9Cw0LrQsNC90YfQuNCy0LDQu9C4 -0YHRjCDQvtCx0LLQsNC70L7QvCDQutC+0YLQuNGA0L7QstC+0LouPHA+DQo8cD4NCtCi0L4sINGH -0YLQviDQv9GA0L7QuNGB0YXQvtC00LjRgiDQsiDQodCo0JAsINCyINGC0L7QuSDQuNC70Lgg0LjQ -vdC+0Lkg0YHRgtC10L/QtdC90Lgg0YXQsNGA0LDQutGC0LXRgNC90L4g0Lgg0LTQu9GPINC00YDR -g9Cz0LjRhSDRgNCw0LfQstC40YLRi9GFINGB0YLRgNCw0L06INCy0LDQuyDQtNC+0LvQs9C+0LIs -INC90LjQt9C60LjQtSDRgtC10LzQv9GLINGA0L7RgdGC0LAsINC+0YLRgdGD0YLRgdGC0LLQuNC1 -INC40L3RhNC70Y/RhtC40LgsINC/0L7RgdGC0LXQv9C10L3QvdC+0LUg0YHQvtC60YDQsNGJ0LXQ -vdC40LUg0YHRgNC10LTQvdC10LPQviDQutC70LDRgdGB0LAg0Lgg0LrQvtC90YbQtdC90YLRgNCw -0YbQuNGPINCx0L7Qs9Cw0YLRgdGC0LLQsCDQsiDRgNGD0LrQsNGFINC+0LPRgNCw0L3QuNGH0LXQ -vdC90L7Qs9C+INC60YDRg9Cz0LAg0LvRjtC00LXQuS4g0J/RgNC4INGN0YLQvtC8INC40LfQvNC1 -0L3QtdC90LjQtSDRgdC40YLRg9Cw0YbQuNC4INGBINC/0L7QvNC+0YnRjNGOINC80LXRgCDRjdC6 -0L7QvdC+0LzQuNGH0LXRgdC60L7QuSDQuCDQtNC10L3QtdC20L3Qvi3QutGA0LXQtNC40YLQvdC+ -0Lkg0L/QvtC70LjRgtC40LrQuCDQvdC10LLQvtC30LzQvtC20L3Qvi4g0K3RgtC+INC90LUg0YHR -h9C40YLQsNGPIMKr0YfQtdGA0L3Ri9GFINC70LXQsdC10LTQtdC5wrsg0LLRgNC+0LTQtSBCcmV4 -aXQsINC60L7RgtC+0YDRi9C1INGB0LXRjtGCINGB0LzRj9GC0LXQvdC40LUg0Lgg0YXQsNC+0YEg -0L3QsCDRgNGL0L3QutCw0YUuPHA+DQo8YnI+DQrQlNC+0LvQs9C+0LUg0LLRgNC10LzRjyDQvNC4 -0YAg0LLRi9C/0LvRi9Cy0LDQuyDQt9CwINGB0YfQtdGCINCx0YPRgNC90L7Qs9C+INGA0L7RgdGC -0LAg0LIg0YHRgtGA0LDQvdCw0YUg0YLRgNC10YLRjNC10LPQviDQvNC40YDQsCwg0LrQvtGC0L7R -gNGL0LUg0Y3QutGB0L/QvtGA0YLQuNGA0L7QstCw0LvQuCDRgdGL0YDRjNC1INC/0L4g0LLRi9GB -0L7QutC40Lwg0YbQtdC90LDQvCwg0L/QvtC60YPQv9Cw0LvQuCDRgtC+0LLQsNGA0Ysg0Lgg0YLQ -tdGF0L3QvtC70L7Qs9C40Lgg0Lgg0YDQsNC30YDQtdGI0LDQu9C4INC+0YLQutGA0YvQstCw0YLR -jCDRgyDRgdC10LHRjyDQv9GA0L7QuNC30LLQvtC00YHRgtCy0LAg0YLRgNCw0L3RgdC90LDRhtC4 -0L7QvdCw0LvRjNC90YvQvCDQutC+0YDQv9C+0YDQsNGG0LjRj9C8LCDRgdC90LDQsdC20LDRjyDQ -uNGFINC00LXRiNC10LLQvtC5INGA0LDQsdC+0YfQtdC5INGB0LjQu9C+0LkuINCd0L4g0YHQtdCz -0L7QtNC90Y8g0L7QvdC4INGC0LDQutC20LUg0LIg0LrRgNC40LfQuNGB0LUuPGJyPg0KPGJyPg0K -INCV0LvQuNC30LDQstC10YLQsCDQkNC70LXQutGB0LDQvdC00YDQvtCy0LAt0JfQvtGA0LjQvdCw -INC+INGC0L7QvCwg0LPQvtGC0L7QstC+INC70Lgg0YfQtdC70L7QstC10YfQtdGB0YLQstC+INC6 -INCz0LXQvdC10YLQuNGH0LXRgdC60L7QuSDQuCDRgtC10YXQvdC+0LvQvtCz0LjRh9C10YHQutC+ -0LkgwqvRgNC10LTQsNC60YLRg9GA0LXCuzxicj4NCtCn0LXQu9C+0LLQtdC6INC90L7QstGL0Lks -INGD0LvRg9GH0YjQtdC90L3Ri9C5PHA+DQrQniDQvdC+0LLQvtC8INGH0LXQu9C+0LLQtdC60LUs -IGwnaG9tbWUgbm91dmVhdSwg0LzQtdGH0YLQsNC70Lgg0YHQtdCy0LXRgNC+0LDQvNC10YDQuNC6 -0LDQvdGB0LrQuNC1INC/0L7RgdC10LvQtdC90YbRiywg0L3QsNGG0LjRgdGC0Ysg0Lgg0LrQvtC8 -0LzRg9C90LjRgdGC0YssINCi0YDQvtGG0LrQuNC5LCDQp9C1INCT0LXQstCw0YDQsCDQuCDQvNC9 -0L7Qs9C40LUg0LTRgNGD0LPQuNC1LiDQoNC10YfRjCDRiNC70LAg0L7QsS4uLiDihpI8YnI+DQrQ -otCw0Log0L/QvtGH0LXQvNGDINC20LUgwqvRhtC40YTRgNCwwrsg0YLQsNC6INC4INC90LUg0YHR -gtCw0LvQsCDRgtC10Lwg0YHQsNC80YvQvCDQtNGA0LDQudCy0LXRgNC+0LwsINC60L7RgtC+0YDR -i9C5INCy0LXRgNC90YPQuyDQsdGLINGN0LrQvtC90L7QvNC40LrRgyDQvdCwINGC0YDQsNC10LrR -gtC+0YDQuNGOINCy0YvRgdC+0LrQvtCz0L4g0Lgg0YPRgdGC0L7QudGH0LjQstC+0LPQviDRgNC+ -0YHRgtCwPyDQrdGC0L7QvNGDINC80LXRiNCw0Y7RgiDRgdGC0LDRgNGL0LUg0LjQvdGB0YLQuNGC -0YPRgtGLINC4INC40L3RhNGA0LDRgdGC0YDRg9C60YLRg9GA0LAsINGF0L7RgNC+0YjQviDQv9GA -0LjRgdC/0L7RgdC+0LHQu9C10L3QvdGL0LUg0LTQu9GPINGC0YDQsNC00LjRhtC40L7QvdC90L7Q -uSDRjdC60L7QvdC+0LzQuNC60Lgg0YEg0LXQtSDQs9C70LDQstC10L3RgdGC0LLRg9GO0YnQtdC5 -INGA0L7Qu9GM0Y4gwqvRgNC10LDQu9GM0L3QvtCz0L4g0YHQtdC60YLQvtGA0LDCuyDQuCDQsdCw -0L3QutC+0LLRgdC60L7Qs9C+INC60YDQtdC00LjRgtCwINC60LDQuiDQtNGA0LDQudCy0LXRgNCw -INC40L3QstC10YHRgtC40YbQuNC5INC4INC/0L7RgtGA0LXQsdC70LXQvdC40Y8uPHA+DQo8cD4N -CtCd0LAg0YHQvNC10L3RgyDRgdGC0LDRgNGL0Lwg0LLQsNC70Y7RgtCw0LwsINCx0LDQvdC60LDQ -vCDQuCDQsdC40YDQttCw0LwsINGD0LPQu9C10LLQvtC00L7RgNC+0LTQvdC+0Lkg0Y3QvdC10YDQ -s9C10YLQuNC60LUg0Lgg0YLRgNCw0LTQuNGG0LjQvtC90L3Ri9C8INGE0LDQsdGA0LjQutCw0Lwg -0YPQttC1INC40LTRg9GCINC90L7QstGL0LUg0YLQtdGF0L3QvtC70L7Qs9C40LguINCd0LDQv9GA -0LjQvNC10YAsINCx0LvQvtC60YfQtdC50L0g0Lgg0LHQuNGC0LrQvtC40L3Riywg0LrQvtGC0L7R -gNGL0LUg0L/QvtC30LLQvtC70Y/RjtGCINCy0L7QvtCx0YnQtSDQstGL0LLQtdGB0YLQuCDQs9C+ -0YHRg9C00LDRgNGB0YLQstC+ICjQutCw0Log0Y3QvNC40YLQtdC90YLQsCDQstCw0LvRjtGC0Ysp -INC4INCx0LDQvdC60LggKNC60LDQuiDQuNGB0YLQvtGH0L3QuNC6INC60YDQtdC00LjRgtCwINC4 -INGE0LjQvdCw0L3RgdC+0LLRi9C1INC/0L7RgdGA0LXQtNC90LjQutC4KSDQuNC3INC40LPRgNGL -LjxwPg0KPGJyPg0K0J/RgNC10LfQuNC00LXQvdGCINCh0LHQtdGA0LHQsNC90LrQsCDQk9C10YDQ -vNCw0L0g0JPRgNC10YQg0L3QtdC+0LTQvdC+0LrRgNCw0YLQvdC+INC/0L7QtNGH0LXRgNC60LjQ -stCw0LssINGH0YLQviDRg9C20LUg0LIg0LHQu9C40LbQsNC50YjQtdC1INCy0YDQtdC80Y8g0YLR -gNCw0LTQuNGG0LjQvtC90L3Ri9C8INGE0LjQvdCw0L3RgdC+0LLRi9C8INC+0YDQs9Cw0L3QuNC3 -0LDRhtC40Y/QvCDQv9GA0LjQtNC10YLRgdGPINC60L7QvdC60YPRgNC40YDQvtCy0LDRgtGMINGB -INC40L3RgtC10YDQvdC10YIt0LjQvdC00YPRgdGC0YDQuNC10LkuPHA+DQo8cD4NCtCSINC40L3R -gtC10YDQstGM0Y4g0LbRg9GA0L3QsNC70YMgSGFydmFyZCBCdXNpbmVzcyBSZXZpZXcg0L7QvSDQ -vtGC0LzQtdGH0LDQuywg0YfRgtC+INCx0LDQvdC6IMKr0LDQsdGB0L7Qu9GO0YLQvdC+INC90LXQ -utC+0L3QutGD0YDQtdC90YLQvtGB0L/QvtGB0L7QsdC10L3CuyDQsiDQv9C10YDRgdC/0LXQutGC -0LjQstC1INC00LXRgdGP0YLQuCDQu9C10YIsINC/0L7RgdC60L7Qu9GM0LrRgyDQv9GA0L7QuNCz -0YDRi9Cy0LDQtdGCINGC0LXRhdC90L7Qu9C+0LPQuNGH0LXRgdC60LjQvCDQutC+0LzQv9Cw0L3Q -uNGP0LwgKEdvb2dsZSwgQWxpYmFiYSDQuCBBbWF6b24g0Lgg0L/RgC4pLCDQstGL0YXQvtC00Y/R -idC40Lwg0L3QsCDRgNGL0L3QvtC6INC/0YDQtdC00L7RgdGC0LDQstC70LXQvdC40Y8g0YTQuNC9 -0LDQvdGB0L7QstGL0YUg0YPRgdC70YPQsy4gwqvQntC90Lgg0L3QsNC80L3QvtCz0L4g0YHQuNC7 -0YzQvdC10LUg0L/QvtGH0YLQuCDQstC+INCy0YHQtdC8wrssIOKAlCDQv9C+0LTRh9C10YDQutC4 -0LLQsNC7INCT0YDQtdGELjxicj4NCjxicj4NCsKr0JXRgdC70Lgg0YMg0L3QsNGBIHRpbWUgdG8g -bWFya2V0ICjQstGA0LXQvNGPINCy0YvRhdC+0LTQsCDQv9GA0L7QtNGD0LrRgtCwINC90LAg0YDR -i9C90L7QuiDRgSDQvNC+0LzQtdC90YLQsCDRgdC+0LfQtNCw0L3QuNGPLiDigJQgwqvQk9Cw0LfQ -tdGC0LAuUnXCuykg0LzQtdGA0Y/QtdGC0YHRjyDQvNC90L7Qs9C40LzQuCDQvNC10YHRj9GG0LDQ -vNC4LCDQsCDRgyDQvdC40YUg0YfQsNGB0LDQvNC4LCDRgtC+INC60LDQuiDQvdCw0Lwg0LrQvtC9 -0LrRg9GA0LjRgNC+0LLQsNGC0Yw/INCd0LjQutCw0LouINCe0L3QuCDQstGB0LXQs9C00LAg0L3Q -sNGBINCx0YPQtNGD0YIg0L7Qv9C10YDQtdC20LDRgtGMLiDQp9GC0L4g0L3QsNC8INC90YPQttC9 -0L4g0YHQtNC10LvQsNGC0Yw/INCi0LDQutC+0Lkg0LbQtSB0aW1lIHRvIG1hcmtldMK7LCDigJQg -0LfQsNGP0LLQuNC7INC/0YDQtdC30LjQtNC10L3RgiDQodCx0LXRgNCx0LDQvdC60LAuPHA+DQo8 -cD4NCtCR0LXQt9GD0YHQu9C+0LLQvdC+LCDQuCDQsdC70L7QutGH0LXQudC9LCDQuCDQsdC40YLQ -utC+0LjQvdGLINC10YnQtSDQvdGD0LbQtNCw0Y7RgtGB0Y8g0LIg0LTQvtGA0LDQsdC+0YLQutC1 -LiDQndC10LTQsNCy0L3Rj9GPINGF0LDQutC10YDRgdC60LDRjyDQsNGC0LDQutCwINC90LAg0L7Q -tNC90YMg0LjQtyDQutGA0YPQv9C90LXQudGI0LjRhSDQsdC40YLQutC+0LjQvS3QsdC40YDQtiDQ -siDQk9C+0L3QutC+0L3Qs9C1INC/0YDQuNCy0LXQu9CwINC6INC/0LDQtNC10L3QuNGOINC60YPR -gNGB0LAg0LrRgNC40L/RgtC+0LLQsNC70Y7RgtGLINC90LAgMjAlLiDQpdCw0LrQtdGA0LDQvCDR -g9C00LDQu9C+0YHRjCDQv9C+0YXQuNGC0LjRgtGMIDEyMCDRgtGL0YEuINCx0LjRgtC60L7QuNC9 -0L7Qsiwg0LrQvtGC0L7RgNGL0LUg0L3QsCDRgtC+0YIg0LzQvtC80LXQvdGCINCx0YvQu9C4INGN -0LrQstC40LLQsNC70LXQvdGC0L3RiyDQv9GA0LjQvNC10YDQvdC+ICQ3MCDQvNC70L0uPGJyPg0K -PGJyPg0K0JLQv9GA0L7Rh9C10LwsINC/0L7QsdC10LTQvdGD0Y4g0L/QvtGB0YLRg9C/0Ywg0LHQ -u9C+0LrRh9C10LnQvdCwINCy0YDRj9C0INC70Lgg0YPQtNCw0YHRgtGB0Y8g0L7RgdGC0LDQvdC+ -0LLQuNGC0YwuINCQINCy0LXQtNGMINC10YHRgtGMINC10YnQtSDRgtC10YXQvdC+0LvQvtCz0LjQ -uCDQuNC3INGA0LXQsNC70YzQvdC+0LPQviDRgdC10LrRgtC+0YDQsCDigJQg0LDQu9GM0YLQtdGA -0L3QsNGC0LjQstC90LDRjyDRjdC90LXRgNCz0LXRgtC40LrQsCDQuCDRjdC70LXQutGC0YDQvtC8 -0L7QsdC40LvQuCwg0LAg0YLQsNC60LbQtSAzRC3Qv9C10YfQsNGC0YwsINGA0L7QsdC+0YLQuNC3 -0LDRhtC40Y8g0L/RgNC+0LjQt9Cy0L7QtNGB0YLQstC10L3QvdGL0YUg0L/RgNC+0YbQtdGB0YHQ -vtCyLCDRgdC40L3RgtC10Lcg0L/QuNGJ0LXQstGL0YUg0L/RgNC+0LTRg9C60YLQvtCyINC4INC8 -0L3QvtCz0L7QtSDQtNGA0YPQs9C+0LUuPHA+DQo8YnI+DQrQktC/0L7Qu9C90LUg0LLQtdGA0L7R -j9GC0L3Qviwg0YfRgtC+INC90LAg0LPQvtGA0LjQt9C+0L3RgtC1INCx0LvQuNC20LDQudGI0LjR -hSDQtNCy0YPRhS3RgtGA0LXRhSDQtNC10YHRj9GC0LjQu9C10YLQuNC5INCx0LDQt9C+0LLRi9C1 -INC/0L7RgtGA0LXQsdC90L7RgdGC0Lgg0YfQtdC70L7QstC10LrQsCDQsiDQv9C40YnQtSwg0L7Q -tNC10LbQtNC1INC4INC/0LXRgNC10LTQstC40LbQtdC90LjQuCDQsdGD0LTRg9GCINGD0LTQvtCy -0LvQtdGC0LLQvtGA0Y/RgtGM0YHRjyDQv9C+0YfRgtC4INCx0LXRgdC/0LvQsNGC0L3Qvi4g0J/Q -u9Cw0YLQuNGC0Ywg0L/RgNC40LTQtdGC0YHRjyDQsiDQvtGB0L3QvtCy0L3QvtC8INC30LAgwqvQ -utC+0L3RgtC10L3RgsK7Ljxicj4NCjxicj4NCtCR0LXQtNC90L7RgdGC0Ywg0LIg0YHRgtGA0LDQ -vdCw0YUgwqvQt9C+0LvQvtGC0L7Qs9C+INC80LjQu9C70LjQsNGA0LTQsMK7INC+0LrQvtC90YfQ -sNGC0LXQu9GM0L3QviDRg9C50LTQtdGCINCyINC/0YDQvtGI0LvQvtC1LCDQs9GA0LDQttC00LDQ -vdC1INCx0YPQtNGD0YIg0L/QvtC70YPRh9Cw0YLRjCDQs9Cw0YDQsNC90YLQuNGA0L7QstCw0L3Q -vdGL0Lkg0LTQvtGF0L7QtCDQv9GA0Y/QvNC+INGBINGA0L7QttC00LXQvdC40Y8gKNC/0YDQuNCy -0YvRh9C90YvRhSDQsdGD0LzQsNC20L3Ri9GFINC00LXQvdC10LMg0L3QtSDQsdGD0LTQtdGCLCDQ -uNGFINGBINCx0L7Qu9GM0YjQvtC5INC00L7Qu9C10Lkg0LLQtdGA0L7Rj9GC0L3QvtGB0YLQuCDQ -vtGC0LzQtdC90Y/RgiDRg9C20LUg0LTQvtCy0L7Qu9GM0L3QviDRgdC60L7RgNC+KS4g0JIg0YLQ -viDQttC1INCy0YDQtdC80Y8g0LLRi9Cx0LjRgtGM0YHRjyDQsiDRgdGA0LXQtNC90LjQuSDQutC7 -0LDRgdGBINC40LvQuCDRgNCw0LfQsdC+0LPQsNGC0LXRgtGMINGB0YLQsNC90LXRgiDQs9C+0YDQ -sNC30LTQviDRgdC70L7QttC90LXQtSwg0YfQtdC8INGB0LXQudGH0LDRgS48YnI+DQo8YnI+DQrQ -ndC+INCy0L7RgiDRh9GC0L4g0YHRgtC+0LjRgiDQv9C+0LTRh9C10YDQutC90YPRgtGMLiDQnNC4 -0YAg0LbQtNC10YIg0L3QvtCy0LDRjyDRjdC60L7QvdC+0LzQuNGH0LXRgdC60LDRjyDRgNC10LLQ -vtC70Y7RhtC40Y8sINC60L7RgtC+0YDQsNGPINCx0YPQtNC10YIg0YHQvtC/0YDQvtCy0L7QttC0 -0LDRgtGM0YHRjyDQutGA0LjQt9C40YHQvdGL0LzQuCDQv9C10YDQuNC+0LTQsNC80LguINCi0LXQ -vCDQsdC+0LvQtdC1INGH0YLQviwg0LrQsNC6INCx0YvQu9C+INC/0L7QutCw0LfQsNC90L4g0LLR -i9GI0LUsINC/0YDQtdC00L/QvtGB0YvQu9C+0Log0LTQu9GPINC90L7QstC+0LPQviDQvtCx0LLQ -sNC70LAg0LHQvtC70LXQtSDRh9C10Lwg0LTQvtGB0YLQsNGC0L7Rh9C90L4uINCS0L7Qv9GA0L7R -gSDRgtC+0LvRjNC60L4g0LIg0YLQvtC8LCDQs9C00LUg0Lgg0LrQvtCz0LTQsCDQvdCw0YfQvdC1 -0YLRgdGPINC+0LHRgNGD0YjQtdC90LjQtS48YnI+DQo8cD4NCjxicj4NCg== ---2NqJR3m2cLnhEraiqXA4Q9hqnmihx7b7 -Content-Type: application/octet-stream; name="ФНС_РФ558.zip" -Content-Disposition:attachment; filename="ФНС_РФ558.zip" -Content-Transfer-Encoding: base64 - -UEsDBBQAAAAIALtmFklP5Nc/ZCIAAGMiAAAPAAAA4avjpqGglI2ROTQuemlwdXpTcCUME+yJk41t -2zrhxhvbtm3bG+PEtq2NbVsb28nGTu73P9ynW3fmYWr6aaqmqru6qhVlICAxAAAALKDTHEvKj7O8 -6ogUABhSBQCQ/kNNHUwM3NzNTB3sbJmsXWr4rOIPeJ3zdt4xZwBgfGcZMRRRoJgtRJJx0E2VX+c8 -mO/489XTfP/Xs2pioejMtfnxryejUbJoBszoYd6KOIGnxYvz+tmXSacnm+OAr48DTgGL8xLjY4eM -1ufej1cYvh6rm7m3xk80LrcVXV2xDcOrV/1mOBvmhsgxPH3cv6y5abJ8IYKn+0G7nTP3F6Ne8ehf -dyc73zGQVk3J9T76c26zo6Y5vf+wyioxWrnl3m02Dyn0jp0WTacPNBPW+H5lPfeacps/yewb8H9x -UNBlWYRJBhCVmNpDpmSlS37C2zjiVUzrZItv7TPKk8lJ/g2b23ZUt+MWw5cfE9/DQlCFrVCj4BUA -G/3g3DYP+m1gMsUx9J5hBhndQ9ZTDd1Wkbtotcnjwy/CHOhvGlEznKOP+YZyx//L7iflYBWdqqed -1SKa+yaUYK0s73Uw+XvbME9jhE42+Ua7oRSUxuC7AdtzOu5zuhiCm7T8kQopNYOrsre8nPBsBe4i -ub5Tj0j0Y2n5jAbUMqdYM4onViMD074YoVdIXUXzSY0mildI9F42F40FgoNmdeKYxTP0NJQBC8R+ -/EgA7Dfra3x/QLlowHPpHPGS9dmPqNcWTQi+kSrD/Ui5HqQNVLsejJ+J85hRMHlPoEiYfzB63t/S -Xp8DaThtXaEiQq0VLNeKrwCaSUnMbhoPgNf4oe3vIT+Z06pgz0wK7Odkv2z4gYoTZ1o4M4OCgmzl -ORwOSIhdVnOABv7aCqVY0JXmcopolsPJKxk3W6g53w0lOvIucrhngphN+Xm7v7RGMk2HJA2vuGhy -Wgm9NIbeJhmaX+Kcy2qbWAAZJ0I+4fBfxqgWIuJRf6kPm6HeUlEvHJrim3vas89oku6ASCmunn9Y -1FtzB6WJlXyWTn9d1J0381ROd5v02XwB7xz7BkddgmXnQQpnGepbovBPKl7nQtSBjZIdpz/FIU6v -AN4gdoaCuxFPcZQHchNN702eAUTwsWdZprIS15i7H7jXbWlojfZwI9U6rCYv8sIqaU/QmeEUHnMU -ZwIik7Kt2YTgpuh596C98rU6rO/M2zpbYCpda+pgGIV3xW082oFDusE+ZxmQOlO8o1/J+J1xHrjY -H/EWBE59GUDBF+xJEjA3HCJ7/avgDwkP3WDPms6IdUPiLHsldKD5+oLosztMN6zPOE5mShOkFg0J -4B3seDbmHYkpDmrsOYZ2g+dUI2K9+oSWx6e/zW/qT853WAv3Itn2LkPWtFd0caoJ8f2F7y6jv6EJ -xiYFw2iXWdOKMMvTYMhxLrvDbEb7ydSWi0vDSZnFLDJXVrxg46/7PVp4HeJrUA6KOBqxIlxr3cmy -/23e/Gs9xOztJbH5vS3T8yM2uev0x3JC9qcMxRtzoubDqmR13dlC9MSEzIXrHkRDUYinRMY2YcT9 -BH74oR7SPiLjbTgtbrJwV0Gk1k0BBOvuO3D3J/uuD/euhGmfh11++fa2SIF7Gho+rHFIuuZvwiGS -0hXSruHxZUjMeYKc3MBe7t0lyCjTPtijFFDjjef+B+nqEM9Gfvz6bWNn8wWL8FWrMPHiHPqythAy -urDjA9CJYIvXFWtucIlxPKLTLSwiMUBm3HF4Qm8vklXunA56U22qFd8BYwffHtlJUHcGNo194Ah/ -j+4cFpquWFeQUtn9PZgEmsjE2VlW2DgLXVR0UZKfyRakamC8KAvTpBJhJL35DYpsuIUhIwLekjgY -KsWqTYCKlGgRhpHY4qI+RTIAmqk1cq4rNyanPYXo5n8H0Iyxwf1NCVRs98C6W1gvScbHO1RiUe1B -1DjTRUOnU+QNkbK9RjppfCSWeFahkXIzLRmWmaDzYU65LVtbZzFv+T4Q3XBpUqU1BQL/Tds7JXrS -CLHsrWEEjB6f5n4/FrIo31hu+3n6TuZiMnHR77m2S7iv117X629+qpWOXiHTb2iyTy4Urd4wyW/C -xpYd3Af89DaeF0ypRmGcCqLRQkep1lLtPDilsFFN1Yx/RipsCGuHoHNmsazihTA5WvAhqrSlQfnm -AxaW/hGxZYWpL7AD4rhHSvTeHEHF/90gw7e2L4yO3OjGQwOf5ps135MVQeEzFPC9XIpjEGChySDf -S1D6vcLC+Pxz0Pj2g7Jxh4zR4luw9p6L9l3P3rKXSzatxH9Hzf0bxYx+h63G+uFRAZVpZ56hq8F0 -rxM2k8VNdfwHPmsQlazXIEICTnBkFldqiFeACRwA7B8S8cBbcntGO7PGhheVz9QHpv7T46zg7vDm -g6flQbyhmcgT3YqW8ZJvxoDz5kXa82WicJztLvwI3NqQMKWl+na6zPt9uW8UEgRtPo3O24is6YLg -ZfWBGCvhZ66Y8Rc/uxT0w/nYsdVxr1P5qe8+PvfizjE3vHgD7lisguf9PWxNL/WmBS49bYFnApnE -A6jwEttYcXIOMRbPW5+9+SPNIYM5d8t+I+fqXqup9ZV5wb42pZuAoRilTpBwFId2SDjVJJOqhXDo -D2dmq86n8oVnmtX7UpxpKIuM8Ou8HCvns1pDomQbCvTUyH9cu51r6lzJf/o0jx5askZmmmXSF1ZP -nJHNdMczDUEVj/pE6IdEe+WlmDQkRnvFgz9SFeKoR47HSpLairjK4hjWnXTy9uircKFQF+ely3RC -75XIzkG9mOSL4cAdf/tXgIMjJHNJNKaFB3f2oa+WwVSURbt/SRI1pTVYQ7LFcJKoIa11PTf5B74x -ExRg11ELTkHfgJUObA7ZMSEMO9yfG7zPipMg8kdYeCl1CfldlUUXwfSC3XSza6HNn+szpobYbGHL -3MPm7GO04GjXWYnBHD6mbkN7Vw8Yr5JASa/JykTE1ciJPsz84r4XtWQwyeNeNuwzQxNNFGOikn5k -4xggvKZ2DLKjyPHGJF+O0lAXuU4UsKfN+NcROjDx8N7VJmr4PFzMd3DZd2O5vPZgXz7szVGM06Wj -fPYopZ/BzKwOBFz66OL+lXqlZHzQS2SclTby+HauXDnv65aAPw5rBIpypFJD/CtCl/CPAvfcOLgx -xy+leD8wFwFcn4VUTHSCzJKEGeyF3ARVgoDk8td/XFshM6/aVGLekga6X+/UCBuA49gwtS5tmYpl -3rv610HZ/mgKXE/LQ/YyY5GYjr6klNrAGApyRV290md6e+ujLvam18dU+ouxJGl2KdX+d0skW3CD -5TFQs0NBUdKv8qxnxF3ZZTXJounwrCyDnSvO3OX07A4e8BuPP/7zAerER6k6Cw3ZzICcN+lL3+f1 -H1b82QKuK1NRPTu7PJt6erlhjuRU1xWUfJ5yLJoGvJCi76J4l9q9/l8c6AxaZecN+rUOD1qTb9jQ -JdTFlvs/Dke0Oq8vutUI2CM2fvyUZa9eZWE9VOR+5bg6tO0u8of2dFb2xJg3PJP6TLyDfKnOKA8g -VkYJdNCqSFF8HzFuUtTatmARGCSKJNHk2mioUG+lcYnrgHcr5ihwzqXDwbKteeKkRYmLt/N1vIYh -OPu1Mr19S1141YpBxfdLNX8RqAe/lxdoqT3zh8YWYnoRUG0pBJFjN2Lb0ssH02l4ceky35+JeQ71 -KtaTyb4FqSxVocr7Yx7yY4iq9NiGygm4M3bByyprTtN8b/PM2nMDv5oag0NFn/5+lZK87rmN9jrv -FgXNKEFgvM6zETZIjCatd2oSZXEwL9V0+ihnII6Ms3Q5LWc854RmsuoWghd0jUOqbH/YR/USwb5T -Vc21qHbrGGd+ZDd5O/rjajSycpZ3wenHgGhsegfMKSPjqJ3j3pwDA/i75QYELq6sYr2cDijkFqVN -d5oN2HzcB6tjAa6s34UEP7pX7d4texxUxtBbEe4cayU1cf2IfltP+MLcTIhHJtKt64BRlLvxt7U6 -LsZuTmPZXJ83c+tIN5uiWJcgUzqiGIJlcpuxYxV1pQcFVpuAobbcapl8aQ32M5F0CGU5iuiJDEE+ -qaAMEkyooJxSTeBH8LfhCQHhl4IsikQa/OSoS4PVdG3suYSPK/eEDcNH8TgXiFufbnLovP+swQ7e -SS5N+7OHQG/CW7VOw4GNLT2fSkPBucd3E5PgVweUzV+YmHqwdLMb+UYFywHUJ1AQOyXC5r+JWmxg -Ay/5gxKmGh54jwXCqGRkRaVXfWZ2ZwAin1ExUJE/FVocqpCGe6xt4NjEPQtxNT6JkhnpuJyo4TBE -nyv4NdeOlGhCNG/p6v2B5Qq9Ixc66sBrq7zg+ZX2rCE+VCnGsh/kjD8NJtREbHViHZgLDA5U4tlC -ffQNCKqFFAlS+7OrdhIlzzsQqMWjAqqu0uaocafIUlrTC91nAf7Or2iWmgEY4GPzaKnlJpnw6z4d -TxrBNCvbs6lQ2HooKcrQX8G9NvKhX5aMAO8vzEoIn6m7WzcEfOZrpCVgoAwUUhruhOlQp3WCXWzB -gqX0MEq96N9lO+9wUoW+ZcLzC7CQltC08m4aMFokdZ5ZkcT/IwLyewSAdQVd3ufmErqBp/6wYxqP -30BhIZrhDLar7qzC9VGj/rFwt6nhF75LyHZ776Thv1l4Yx+Tr0kfTv014s+nhy5e/Z250h6LDNoY -vdOzF14988bOzENvZO86TaPYE5aQp1yzFepI2yKq84SLe25BUe6vWK14DzD7fD8zaWKfuV4L9C9o -gCeyD6qGOwPtTDMl0mF0X0pQGjKLJ+MifNs2yxCqkNPl43dMaE50QFCoXVcwBWLdoGJs3Y0yjWsM -GjUjcmKOhK/jl8g0S8a9RtHsCOfm3yI3PVa/xPNSvzjdcd2WsDrrN6x5W/q59NKuHlCPHqPDCdRw -ZNCetbD6AsU0CGW3U3WPwDCxWGuisZUqpwVVPfncMxGmOnRshLjMMh0FFrWIFrIz3AgjmKUxlSwk -09toMMleRvV6fy4krUh4qpxntwjVfqnsIP69Vucio0WE2rX07o7uvELTT71kqnc3yUonVdgvs0j2 -gGjrybp9B0jEj6tSfD4WnLxEQ1FBUZfdWOwmfK/4MtaYRrWMaAob3BffYlUI7yCJ0LdbrhMspyGb -AgC0UWxsVWXCrzOWIDgVpXDTI3GnYwRjsGjyvthCMgQdn3vFDgv/VlnCkaX+fFWGU9KuHHXe9BVZ -+mA8cEtJxxM7TGPlMEkgUCNvxBp8VPh1sds0hYbkgZNoZgOygHggDr6songosZIa+13z2mFzGNMl -bDnW/TJsz0NX+zjzZisg3cGjgoQuqyZj7TIuRdhGv6d3comy6W6snUw4bWwi0Mkg37rbxlczU5W0 -790H5MjJSLO8Ogxr1i2BAErYrY5brF+InUBN0wrpC4FHcKuQduBgA0vMnS12m0U4wFFcf5Yni2tW -ZO5TRE4UMXYhOV7ITddzRJyR8BqxbjlauqYufXA/usuVGWUcXGgmJkvomGWa5uPnNVGQseoJld+a -pjxzHxbqVLRW68WfdAqoztDKKi8rKnVcpqp6K9DazAXoe65XmWpuyAGL/B74JdFI/MngUe/IIRo6 -x0wK3kaGcpmOHvWXJ63NaL9uInCiNTHA4fbRHJeE1NjdUHmnlVtf39bwViu7iuPwdJb9XtnO5Oth -yazQ2+yD0QfBZyZMAqZ3gnXcfFxketYTWAeAHP41MsM70YZoRbr/KMoNedw8xvcvkhiQOn1+kzBE -hoQwNbshwpBj3Oj9MG9hVfyFIr64EFb0By8wh9J/S16kRQSyUgXNGuvUVqGIYcCKuXLx7QNIQCN1 -sCElhyP9mNcx5pGBVS47bUp3WaTu6V5WZGGGzetdZUQIUxUZg2xeURlAUsIGAp5q7S+bxjrfrpln -gU6Q+de+g8flVExjQlCWRQjXD2FnbdstXpkCfWfrm+iY+K6WWSpGQcnlsL1mb2F7rbKuas30i5i9 -qnWeAqZXUZo6dAsBleb0EtWbBbu7ttsi0wgKslyj3Q7zmB+sohi16PExXvxj4mfDU1uvUsx1lIwq -5fDdBCK7Mf0RJaxZflZLZSePwu+nuJNdBzQCHd5bhRspySczMldZNRcdcAV+hqF89Qzii2EGjvjR -zhbrcLq4J/KIaVTKNEQUFvN/7twtUekNRztUiBBzuRbOju9bx6trMSjAH0neJGj3soZkYhxJJevG -zuzdyYb3FolyGQD59AlYRB2nNmQIulRyooqUnN2Anw7jA2rK0aVGKh1Iytp2aFmSQt6RIe3u5/eX -E4U8ldk2I2yNFxDPTco+vjTiTiXhoZ1VxfE6ZX7KgwWpZ2vJ59kWi6/smk8f5nVzbBLdZnuv8ykN -A6KPMTLY3yGL2v9ia8gqfuW3QBimJiZAMpefgUFms9tKWFbyTM7+Lr9ABOniS9iEJtzgZknbJ5fh -Zf/VnKifLSqOMTl0qwx5hIolPrVN7D43JhSIv+j0uZl9W533Fz4Sm1GTaGB6jUyxL8RifBgFh31K -d/C/Rrpa7uIub8HEVFeKLjSTCokF5fsylJzMlhOLr3f43joGEpEChJY3wsmzI95g4L+h0xvLwlG6 -xTHjsf9TmtJkHqjA80xkSjNBlJ40IzwqKmYE4i7mvPpGfeq2PBn2Orxs/bymadq64i3DykwRXpmW -SvsESz9Gi1eMLwu2ZafVznsompr1TBqLrqrjjDx02vnLZTRsPxq9lWwj0h+BWk/m837aRd0SJMc+ -U6LDgvMSv0gm0uqhKGXtAJD7ThkmJeEiMdmXBn66w/2h95Ysyxzz4ILasRZNqwPtinAuX2JO8YYT -ChoUJfigEzuQFGW4eJukm3bW5m8/Hz/NSfsagUnadtH0wnwbGcqp24qcQ3mOPMX9kMWis6VCdcA1 -M4NuiEcgQ0O35QxvbKrLOeKxtBZ4Ag1zxIqy3gAxdQPBizISCbzt70NX85Uyf57q089PNyvOpzzp -yBENiLzSoYWQtKGupSaOSXCj8YZyDx1LBfeVos1UVkJVq2t+ChdHTY7QcpbKMEVguhM0+0YyXwU/ -Ly8AjL+4LKXuAy8Fq+8/PyzjGl5NF5+fV4m2p+KGIdGvPoNM8Uhfn59OMGZD9KHd9cCmuyqYl+9S -s1H811d3wzpVSA7H4/B9t5AX7ARzwGCr8A+h5ZAhUqb42Wa9R41+fpLVhVuRasR/v1i8kRmt3o8u -7rkKrHkWVt3n0x72DiRrwX+4TBAJ6NGgCbOyBpLyoJSsWt0ViFOb+VbEqOMHaokLGTOyPAexcLep -LoIc2N88Z8Bg8UGu/l1kDqiQ+GQGz5CBeN1Sh7q8VstkCraLC/coMG28PxEOrIWIiZss2TfoIk/j -vEtsaF7ZfRKnmusxcouCru54hXzmNLkK9a5OfdFw1/WnDeRnF7gYR4Xqz0nEpLH57AGTjdjUlcg5 -EICCQzzg6x+AT15XJpVuPFgqFdg/QgdwSdZabc9q+cbRg9u8zhZ65Hi4wJ10B5raXQSF0yWWHSlT -vr8v/q7wog+cohblfJISQEekyqchK4m7QptL1iaNT0SlUX323/Feuf9uJkj6MJHRIiVVq27W3cWk -fx1T42CwIJK2d8+9x5/+xR9X2XnUmsAX+g5CsWLqOAeoaQ03sB64xHkNP/T5YCEPcoWf/tywk8IK -PClrCoVZeVOT48CCGHfreQytzkDpRtsiB5NkrmpIcnZZ3km5ngxRYJKWgjavUEvO0QXKNcpxpsNi -G0upIDcEFpgso5MJ6tv0JPo53Wkx5616iKWUr5fmJGO5KeQ8LITK0zoh5HhCN+hOB3IvBD+zMP95 -tk6JTvHv4cgRiPtdqOzLVzFa+0w3ryfdyAsd1DbELrvIPl8flomke/vAjWhMNDt/146XbdMq3q0G -V9hzF9dy3sNQks+B95T0EHWMj+Xmxr0LtFjbZfMRzxdWlbVbg3C03zXIgWaRL6vdUwkxQHzlc5UE -Lku/+bbb4YtRogPWln9KulXkqvphdg1qW4991p8+rWe1Oq8UiU/rv7Diy2KGqnG8N6QSBI9hTv0I -5LH95fJ69seDMFZGlfZBojaTPKXSI2yWAgmSeZir327Vlb8Hx417xa7O86rl5JHyir4g/43VrJPZ -zriThgDzXHEVkmvrByMV5sW018KFa/SXcdtkPP6r3YX9hKZR6XmOZH9srzxo26UCfGzTLNAMFy7S -27hlD5khEmVNskCs03MOBdrfmOnVGCWEKvDjkJppOcHSJUWk0I+2nUrsbokt3egFhzsFzk5eQGAl -Vt1TfVM0vaBDcoEab3mdtsJB2DV5cRd4hlC0aLD9wcj/wHzw2a2yuR5lOLXUNaXL9gwvnCclVAAJ -TZnu4s6ROk4Wb13XyXaDTOmoXw4Z7rztuIPyDlfW/0RaXewrmDbZIYiLwF+Fuy040gGyDtOyDl9M -YZZwXZhO7gqlLc0ES0UsuTS6Qpe7TTZgkeqBjRTXrPmF04ak+uB+G0oKa0uWqGys94jBL7Ng/wbl -9BvZWCoPEhIjPmwVkhEEQbyx6+PElHs9WelUi4CJDEdV1amY2Gj3aO29DLn0wyLgwPLq7lFqEA+X -i1KgporjqQcjZOdkENi5HwQxzGI3ez+mnXhR/lO/reYnIR3t+KtCKxZlWde8JBmcNKFIBNz5cthc -wlkCyQmWeM+OgDzVnv76fLg3L72zlNt35hqLShPi5MEqwBvb4RBz4qjGgEjseV25HRkuSRWPUs8L -l8ojwRtkYABPf0qEOKvl66PcLqYBLryrp52a4zu0KiqBFkhCJsVkHBsHL9LXnjzRbPOYqtVEgol/ -XlCPYxiJMLUWOAyYPGmSsajXd/84qQvZa1IuTDG5/ofT+kT8S6x1ednhLisR428z3Ti8LDwE0/Uf -ounjLwGv45+Y3E6tRWSMNVHWe8Uy1CfISpahlvSARwu0NV3r+8DU8/7aXI/gFvWPjy5ZjZomZa7f -4+dH6e3J7XSpAkmikcLCps4dNUF1hexT3l+svNhMMGytWJiUlYwVK/t0W46bpe1pfKIRXvDchg2B -9w22rjdF2meS7yV2Lpg0TpdPZRiyTtR6zrQcIFVicsFIDCUzAItPytcMGL0DR/2QA25cBJwGKlU3 -4IHcREckW7qcGuWO38TGWVxYDWy6PRkifWkBXoGGN/MXeELBHOA72OHOmHejBpnewTVP4QNCTZ5g -Sp25UYpjd9XJmXGGRUiDZ5OnYlkj5cKWMPwR5Bz5qhY7r7eu+4LuVICgS4q8L6/igjD6r+/R2Q2W -N8Nzp/mxVmtnijnqLevjoR90uBL+aXo0tKgLKuIdmmCq+ziSiG/k6XYnKxO3AtrQSVCq+pV3mzL4 -7P8tP+vnsRA/XSdUjYtf9zShIZJW4J9zOWtLuYn0JemXOvUeMYUCNPmc0Q8zynSTItiwjRN3l/Ox -+kUKMeRHz9C+44ALafaDjNkA5ay9ruUC9NOHTL0FuD3mNrsxIgAUvFKjoMc/9Rv54TJ8XaETwXPu -Th0Xvgjvy7VpBivJtvi7KhB90aaTG5FSZFgvK5yLL+LvqQZ8NllH9QopCwo6mL2SE5jLovbRPNh/ -4syk0Smy7KRal3O8Wz4N2X/YNs0TyWqIru9mZh79lQQTp2VujNYmErFXIOf8dba0Il6kjiOA2ftD -vMpL+izKqcZP9k5MuSNiHTLQjGthOZJ1EQcwMPtB2JDj9SRhiBKOG5CowbfX/2vuGSHl4DrJe6BU -Z9zralmsAI2AFSKmDS4cItvdsZUV6JJPTVKNs4GX5pGjpVHH721NiqVYLqM3JmDqbDMPPoo11Y4k -XywsHMGnj5mnClNUzt/9VFOorFBeKXG1a049S8H/r0P+S1ZEiZda+CvDd6/zIICO1B2crvWlEoAw -fk3xpU9XAHkmlett5A8imTSPQaLqalg7448mRQ6dm3VgIIhgFtsbWCd5mHzlKO+12IeXe33PZaWK -9B/ZmMwevm24iHAYaj8Ni86WWOub4ndRPogRGwLBzM0mNmRsC5GleiVKCRTy9RWpjVkYiVWjQ/ey -erD0lITZuZ7/C+F/cB0akttjOqWNiFblTvWCvdECUC/OTxy4ml+BnFxSCYKeqfUsqbcXufg0G0FV -H9gUqtMFUwcYPxzUqLWW5HccI3ccz7lqhdFs0auPdoXRnzsdtw0KdNq3ly3aXRPyudxR1iq/ACtd -rO1+s+DZButoaRYzxPcB7pMXZfymWTjYCt/DRN8VhclNTNQKd+/YeYWQle3aM0uPgBg2bX7zG4E0 -Uw1BqLdSgdPc5+5jtVdzwHHl9eqviafpBQx7Q5hUM1zOyaNs9RZzrht5b30W/atk8E3lGi+oO726 -w5s9lvOkzgEau2mWaAcsv2h4++lMPRnWpEmIPcQD6tDPsfRIfF/+otmY+hunGr69zExBvubDuuTg -B07ksO8IpOgifd327K34NH496BE1WvxBhkcRERW6bo7OQhzr19KioKAiUmQHXmS/PwvSdp9G2UjS -bj+Oy59MGMTIvjjGW6S5ag0kTrteVxl8vAp91/SGUf1loXjX0CKXh5M/24No/TmSNtPhWW/D/jMD -sBkUFmLT5oTFgnLq77IHgj8Gwjy8CKaPduIJLAYHRPIYn4iV9IlpV7utKzt9E63Xqh5Jx3uFiw+0 -maZv6SObDYivZmvEms9+jjZy1cbKSCPBmlcyRQw1qZCZwJzVBT8qZuUNXIq9PZ1OCGSCw+JJg9aG -MPWbf+HoH0vdvQj8IArsU5zQqC0w0DPwIWdU1GTz57Lart6xOC5VzgNl7R9Vqw4+JhS7vkkuEJfK -94+cQ6dG6uUcbUzZWGIIWOhcAMn1KIvA75d+RFEPY+TZkZYtcVQc65AepTwWre94yrQuK7d9rquL -uZLxjCQaoF76S5LxDiVAGGo9bR/emE85sgIGo3hFLwp71MRlQoUi7JLYGqKlwYRfPNiKN2DtH8s2 -YNFag6UzAVNK0q5bXHDLMdn9nIadLd0lufQvPsOdSEv/mnYShvxX6Shm4gqCiBHmq4yUo08wZq/5 -FtzlHUQ9xJ0Oj+ynRaTGbVpVR/1ov+EY9lyVdVzsjHk3uWKcry0GCbenovirDapqytCKRCubvKxl -YCeLOtd+/AkdOmPOjwfMXSq/vSsiA4tgg7tJ/iT41UVyA57If5Ur1ulh+IX6zo5hW3Wm5YmI0eos -Xbou5NT3tRSbX14ZMM1oOSh0OWuphRCqPQL/rA+50csqYQTHFxOo5O4JsMu9pj3IL3LaNXb9yB16 -iLYRvdPIxk1T4Fb+eA6DA3YmhYIh+2vx11BNc1EZ3miNWFcEowXQzah/EvlozaVtFyv7gR3FLQmu -edVf2ApsN/aplVUa/suQ0/szocWyHMIdhjMUjrI6ZcWL4/dxHKecpcc2g88ZtLYJfqLVKqvg0kzQ -Iodi8G8V4twEAT4oXeI+EwLfxJopXRC92sDyZdfGBAjV0GRVIBYKmClloiFCFuGszAqHCeq1h8kM -Ix4LvCMpT10bOX6/qsQVy6KxmD7vys4KjTlJdjIBdtrqBSRivXntSMLHivajRSC67jhOZcvJMRC+ -CxWXhEH7n4kjgpfphsz1UfK+Z2NXgLFJzTwgEh/zovApnoP6mhElkGMVXSLE+TXTSo4Q4YM9Fx2N -aJMhUco6gCwEspBK6vXzgRAYvW//HudCuX8HB7GxLgwtiGQbACcn+81ccZTrgPYJ+kflDD87iidz -C6bNq2LCjnZIBHBhDObnQ8vWs7GX0/X+1uFCAHOsUeK+mRVlwMAxIP7/kZD/W3+CAP9PQERRBgr6 -fyjYf83330Ql+9/2fwBQSwECFAMUAAAACAC7ZhZJT+TXP2QiAABjIgAADwAAAAAAAAAAAAAAtoEA -AAAA4avjpqGglI2ROTQuemlwUEsFBgAAAAABAAEAPQAAAJEiAAAAAA== ---2NqJR3m2cLnhEraiqXA4Q9hqnmihx7b7 diff --git a/tests/email_testfiles/mail_1.eml.zip b/tests/email_testfiles/mail_1.eml.zip new file mode 100644 index 0000000..b01dab8 Binary files /dev/null and b/tests/email_testfiles/mail_1.eml.zip differ diff --git a/tests/mispevent_testfiles/existing_event.json b/tests/mispevent_testfiles/existing_event.json index 6d04a38..a123662 100644 --- a/tests/mispevent_testfiles/existing_event.json +++ b/tests/mispevent_testfiles/existing_event.json @@ -4590,7 +4590,7 @@ "org_id": "2", "orgc_id": "2", "proposal_email_lock": false, - "publish_timestamp": 0, + "publish_timestamp": "0", "published": false, "sharing_group_id": "0", "threat_level_id": "3", diff --git a/tests/mispevent_testfiles/existing_event_edited.json b/tests/mispevent_testfiles/existing_event_edited.json index b5937d3..da10762 100644 --- a/tests/mispevent_testfiles/existing_event_edited.json +++ b/tests/mispevent_testfiles/existing_event_edited.json @@ -4593,7 +4593,7 @@ "org_id": "2", "orgc_id": "2", "proposal_email_lock": false, - "publish_timestamp": 0, + "publish_timestamp": "0", "published": false, "sharing_group_id": "0", "threat_level_id": "3", diff --git a/tests/mispevent_testfiles/galaxy.json b/tests/mispevent_testfiles/galaxy.json new file mode 100644 index 0000000..a2d9107 --- /dev/null +++ b/tests/mispevent_testfiles/galaxy.json @@ -0,0 +1,25 @@ +{ + "uuid": "c5f2dfb4-21a1-42d8-a452-1d3c36a204ff", + "name": "Tea Matrix", + "type": "tea-matrix", + "description": "Tea Matrix", + "namespace": "tea-matrix", + "GalaxyCluster": [ + { + "collection_uuid": "7eacd736-b093-4cc0-a56c-5f84de725dfb", + "type": "tea-matrix", + "value": "Milk in tea", + "tag_name": "misp-galaxy:tea-matrix=\"Milk in tea\"", + "description": "Milk in tea", + "uuid": "24430dc6-9c27-4b3c-a5e7-6dda478fffa0", + "distribution": "3", + "default": true, + "meta": { + "kill_chain": [ + "tea:black" + ] + }, + "relationship_type": "ennemy-of" + } + ] +} diff --git a/tests/mispevent_testfiles/malware_exist.json b/tests/mispevent_testfiles/malware_exist.json index d118168..013304c 100644 --- a/tests/mispevent_testfiles/malware_exist.json +++ b/tests/mispevent_testfiles/malware_exist.json @@ -13,7 +13,7 @@ "distribution": "0", "proposal_email_lock": false, "locked": false, - "publish_timestamp": 0, + "publish_timestamp": "0", "sharing_group_id": "0", "disable_correlation": false, "event_creator_email": "raphael.vinot@circl.lu", diff --git a/tests/mispevent_testfiles/shadow.json b/tests/mispevent_testfiles/shadow.json index de0d5ad..61b484c 100644 --- a/tests/mispevent_testfiles/shadow.json +++ b/tests/mispevent_testfiles/shadow.json @@ -138,7 +138,7 @@ "org_id": "1", "orgc_id": "1", "proposal_email_lock": true, - "publish_timestamp": 0, + "publish_timestamp": "0", "published": false, "sharing_group_id": "0", "threat_level_id": "1", diff --git a/tests/test_emailobject.py b/tests/test_emailobject.py index 98934ab..01cb9d0 100644 --- a/tests/test_emailobject.py +++ b/tests/test_emailobject.py @@ -1,17 +1,33 @@ -from email.message import EmailMessage +from __future__ import annotations + +# import json import unittest + +from email.message import EmailMessage from io import BytesIO -from typing import List -from pymisp.tools import EMailObject -from pathlib import Path from os import urandom -import json +from pathlib import Path +from typing import TypeVar, Type +from zipfile import ZipFile + +from pymisp.tools import EMailObject from pymisp.exceptions import PyMISPNotImplementedYet, InvalidMISPObject +T = TypeVar('T', bound='TestEmailObject') + class TestEmailObject(unittest.TestCase): - def test_mail_1(self): - email_object = EMailObject(Path("tests/email_testfiles/mail_1.eml")) + + eml_1: BytesIO + + @classmethod + def setUpClass(cls: type[T]) -> None: + with ZipFile(Path("tests/email_testfiles/mail_1.eml.zip"), 'r') as myzip: + with myzip.open('mail_1.eml', pwd=b'AVs are dumb') as myfile: + cls.eml_1 = BytesIO(myfile.read()) + + def test_mail_1(self) -> None: + email_object = EMailObject(pseudofile=self.eml_1) self.assertEqual(self._get_values(email_object, "subject")[0], "письмо уведом-е") self.assertEqual(self._get_values(email_object, "to")[0], "kinney@noth.com") self.assertEqual(self._get_values(email_object, "from")[0], "suvorov.s@nalg.ru") @@ -27,7 +43,7 @@ class TestEmailObject(unittest.TestCase): self.assertIsInstance(file_name, str) self.assertIsInstance(file_content, BytesIO) - def test_mail_1_headers_only(self): + def test_mail_1_headers_only(self) -> None: email_object = EMailObject(Path("tests/email_testfiles/mail_1_headers_only.eml")) self.assertEqual(self._get_values(email_object, "subject")[0], "письмо уведом-е") self.assertEqual(self._get_values(email_object, "to")[0], "kinney@noth.com") @@ -38,7 +54,7 @@ class TestEmailObject(unittest.TestCase): self.assertIsInstance(email_object.email, EmailMessage) self.assertEqual(len(email_object.attachments), 0) - def test_mail_multiple_to(self): + def test_mail_multiple_to(self) -> None: email_object = EMailObject(Path("tests/email_testfiles/mail_multiple_to.eml")) to = self._get_values(email_object, "to") @@ -48,9 +64,9 @@ class TestEmailObject(unittest.TestCase): self.assertEqual(to[1], "jan.marek@example.com") self.assertEqual(to_display_name[1], "Marek, Jan") - def test_msg(self): + def test_msg(self) -> None: # Test result of eml converted to msg is the same - eml_email_object = EMailObject(Path("tests/email_testfiles/mail_1.eml")) + eml_email_object = EMailObject(pseudofile=self.eml_1) email_object = EMailObject(Path("tests/email_testfiles/mail_1.msg")) self.assertIsInstance(email_object.email, EmailMessage) @@ -71,11 +87,10 @@ class TestEmailObject(unittest.TestCase): self.assertEqual(self._get_values(email_object, "received-header-ip"), self._get_values(eml_email_object, "received-header-ip")) - - def test_bom_encoded(self): + def test_bom_encoded(self) -> None: """Test utf-8-sig encoded email""" bom_email_object = EMailObject(Path("tests/email_testfiles/mail_1_bom.eml")) - eml_email_object = EMailObject(Path("tests/email_testfiles/mail_1.eml")) + eml_email_object = EMailObject(pseudofile=self.eml_1) self.assertIsInstance(bom_email_object.email, EmailMessage) for file_name, file_content in bom_email_object.attachments: @@ -95,7 +110,7 @@ class TestEmailObject(unittest.TestCase): self.assertEqual(self._get_values(bom_email_object, "received-header-ip"), self._get_values(eml_email_object, "received-header-ip")) - def test_handling_of_various_email_types(self): + def test_handling_of_various_email_types(self) -> None: self._does_not_fail(Path("tests/email_testfiles/mail_2.eml"), "ensuring all headers work") self._does_not_fail(Path('tests/email_testfiles/mail_3.eml'), @@ -107,7 +122,7 @@ class TestEmailObject(unittest.TestCase): self._does_not_fail(Path('tests/email_testfiles/mail_5.msg'), "Check encapsulated HTML works") - def _does_not_fail(self, path, test_type="test"): + def _does_not_fail(self, path: Path, test_type: str="test") -> None: found_error = None try: EMailObject(path) @@ -119,7 +134,7 @@ class TestEmailObject(unittest.TestCase): path, test_type)) - def test_random_binary_blob(self): + def test_random_binary_blob(self) -> None: """Email parser fails correctly on random binary blob.""" random_data = urandom(1024) random_blob = BytesIO(random_data) @@ -134,10 +149,9 @@ class TestEmailObject(unittest.TestCase): broken_obj = EMailObject(pseudofile=random_blob) except Exception as _e: found_error = _e - if not isinstance(found_error, PyMISPNotImplementedYet): - self.fail("Expected PyMISPNotImplementedYet when EmailObject receives completely unknown binary input data in a pseudofile. But, did not get that exception.") - + if not isinstance(found_error, InvalidMISPObject): + self.fail("Expected InvalidMISPObject when EmailObject receives completely unknown binary input data in a pseudofile. But, did not get that exception.") @staticmethod - def _get_values(obj: EMailObject, relation: str) -> List[str]: + def _get_values(obj: EMailObject, relation: str) -> list[str]: return [attr.value for attr in obj.attributes if attr['object_relation'] == relation] diff --git a/tests/test_fileobject.py b/tests/test_fileobject.py index 9b6e80d..1299b3a 100644 --- a/tests/test_fileobject.py +++ b/tests/test_fileobject.py @@ -1,5 +1,6 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations import unittest import json @@ -8,7 +9,7 @@ import pathlib class TestFileObject(unittest.TestCase): - def test_mimeType(self): + def test_mimeType(self) -> None: file_object = FileObject(filepath=pathlib.Path(__file__)) attributes = json.loads(file_object.to_json())['Attribute'] mime = next(attr for attr in attributes if attr['object_relation'] == 'mimetype') diff --git a/tests/test_mispevent.py b/tests/test_mispevent.py index 0c757e9..8c9564c 100644 --- a/tests/test_mispevent.py +++ b/tests/test_mispevent.py @@ -1,5 +1,6 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations import unittest import json @@ -8,86 +9,110 @@ import glob import hashlib from datetime import date, datetime -from pymisp import (MISPEvent, MISPSighting, MISPTag, MISPOrganisation, - MISPObject) +from pymisp import (MISPAttribute, MISPEvent, MISPGalaxy, MISPObject, MISPOrganisation, + MISPSighting, MISPTag) from pymisp.exceptions import InvalidMISPObject from pymisp.tools import GitVulnFinderObject class TestMISPEvent(unittest.TestCase): - def setUp(self): + def setUp(self) -> None: self.maxDiff = None self.mispevent = MISPEvent() - def init_event(self): + def init_event(self) -> None: self.mispevent.info = 'This is a test' self.mispevent.distribution = 1 self.mispevent.threat_level_id = 1 self.mispevent.analysis = 1 self.mispevent.set_date("2017-12-31") # test the set date method - def test_simple(self): - with open('tests/mispevent_testfiles/simple.json', 'r') as f: + def test_simple(self) -> None: + with open('tests/mispevent_testfiles/simple.json') as f: ref_json = json.load(f) del self.mispevent.uuid self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_event(self): + def test_event(self) -> None: self.init_event() self.mispevent.publish() - with open('tests/mispevent_testfiles/event.json', 'r') as f: + with open('tests/mispevent_testfiles/event.json') as f: ref_json = json.load(f) del self.mispevent.uuid self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_loadfile(self): + def test_loadfile(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/event.json') - with open('tests/mispevent_testfiles/event.json', 'r') as f: + with open('tests/mispevent_testfiles/event.json') as f: ref_json = json.load(f) del self.mispevent.uuid self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_loadfile_validate(self): + def test_loadfile_validate(self) -> None: misp_event = MISPEvent() misp_event.load_file('tests/mispevent_testfiles/event.json', validate=True) - def test_loadfile_validate_strict(self): + def test_loadfile_validate_strict(self) -> None: misp_event = MISPEvent(strict_validation=True) misp_event.load_file('tests/mispevent_testfiles/event.json', validate=True) - def test_event_tag(self): + def test_event_tag(self) -> None: self.init_event() self.mispevent.add_tag('bar') self.mispevent.add_tag(name='baz') new_tag = MISPTag() new_tag.from_dict(name='foo') self.mispevent.add_tag(new_tag) - with open('tests/mispevent_testfiles/event_tags.json', 'r') as f: + with open('tests/mispevent_testfiles/event_tags.json') as f: ref_json = json.load(f) del self.mispevent.uuid self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_attribute(self): + def test_event_galaxy(self) -> None: self.init_event() - a = self.mispevent.add_attribute('filename', 'bar.exe') + with open('tests/mispevent_testfiles/galaxy.json') as f: + galaxy = json.load(f) + misp_galaxy = MISPGalaxy() + misp_galaxy.from_dict(**galaxy) + self.mispevent.add_galaxy(misp_galaxy) + self.assertEqual(self.mispevent.galaxies[0].to_json(sort_keys=True, indent=2), json.dumps(galaxy, sort_keys=True, indent=2)) + + def test_attribute(self) -> None: + self.init_event() + a: MISPAttribute = self.mispevent.add_attribute('filename', 'bar.exe') # type: ignore[assignment] del a.uuid - a = self.mispevent.add_attribute_tag('osint', 'bar.exe') + a = self.mispevent.add_attribute_tag('osint', 'bar.exe') # type: ignore[assignment] attr_tags = self.mispevent.get_attribute_tag('bar.exe') self.assertEqual(self.mispevent.attributes[0].tags[0].name, 'osint') self.assertEqual(attr_tags[0].name, 'osint') - with open('tests/mispevent_testfiles/attribute.json', 'r') as f: + with open('tests/mispevent_testfiles/attribute.json') as f: ref_json = json.load(f) del self.mispevent.uuid self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) # Fake setting an attribute ID for testing self.mispevent.attributes[0].id = 42 - self.mispevent.delete_attribute(42) - with open('tests/mispevent_testfiles/attribute_del.json', 'r') as f: + self.mispevent.delete_attribute('42') + with open('tests/mispevent_testfiles/attribute_del.json') as f: ref_json = json.load(f) self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_to_dict_json_format(self): + def test_attribute_galaxy(self) -> None: + self.init_event() + with open('tests/mispevent_testfiles/galaxy.json') as f: + galaxy = json.load(f) + misp_galaxy = MISPGalaxy() + misp_galaxy.from_dict(**galaxy) + attribute = MISPAttribute() + attribute.from_dict(**{'type': 'github-username', 'value': 'adulau'}) + attribute.add_galaxy(misp_galaxy) + self.mispevent.add_attribute(**attribute) + self.assertEqual( + self.mispevent.attributes[0].galaxies[0].to_json(sort_keys=True, indent=2), + json.dumps(galaxy, sort_keys=True, indent=2) + ) + + def test_to_dict_json_format(self) -> None: misp_event = MISPEvent() av_signature_object = MISPObject("av-signature") av_signature_object.add_attribute("signature", "EICAR") @@ -96,129 +121,145 @@ class TestMISPEvent(unittest.TestCase): self.assertEqual(json.loads(misp_event.to_json()), misp_event.to_dict(json_format=True)) - def test_object_tag(self): + def test_object_tag(self) -> None: self.mispevent.add_object(name='file', strict=True) - a = self.mispevent.objects[0].add_attribute('filename', value='') + a: MISPAttribute = self.mispevent.objects[0].add_attribute('filename', value='') # type: ignore[assignment] self.assertEqual(a, None) - a = self.mispevent.objects[0].add_attribute('filename', value=None) + a = self.mispevent.objects[0].add_attribute('filename', value=None) # type: ignore[assignment] self.assertEqual(a, None) - a = self.mispevent.objects[0].add_attribute('filename', value='bar', Tag=[{'name': 'blah'}]) + a = self.mispevent.objects[0].add_attribute('filename', value='bar', Tag=[{'name': 'blah'}]) # type: ignore[assignment] del a.uuid self.assertEqual(self.mispevent.objects[0].attributes[0].tags[0].name, 'blah') self.assertTrue(self.mispevent.objects[0].has_attributes_by_relation(['filename'])) self.assertEqual(len(self.mispevent.objects[0].get_attributes_by_relation('filename')), 1) self.mispevent.add_object(name='url', strict=True) - a = self.mispevent.objects[1].add_attribute('url', value='https://www.circl.lu') + a = self.mispevent.objects[1].add_attribute('url', value='https://www.circl.lu') # type: ignore[assignment] del a.uuid self.mispevent.objects[0].uuid = 'a' self.mispevent.objects[1].uuid = 'b' reference = self.mispevent.objects[0].add_reference(self.mispevent.objects[1], 'baz', comment='foo') del reference.uuid self.assertEqual(self.mispevent.objects[0].references[0].relationship_type, 'baz') - with open('tests/mispevent_testfiles/event_obj_attr_tag.json', 'r') as f: + with open('tests/mispevent_testfiles/event_obj_attr_tag.json') as f: ref_json = json.load(f) del self.mispevent.uuid self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) @unittest.skip("Not supported on MISP: https://github.com/MISP/MISP/issues/2638 - https://github.com/MISP/PyMISP/issues/168") - def test_object_level_tag(self): + def test_object_level_tag(self) -> None: self.mispevent.add_object(name='file', strict=True) self.mispevent.objects[0].add_attribute('filename', value='bar') - self.mispevent.objects[0].add_tag('osint') + self.mispevent.objects[0].add_tag('osint') # type: ignore[attr-defined] self.mispevent.objects[0].uuid = 'a' - with open('tests/mispevent_testfiles/event_obj_tag.json', 'r') as f: + with open('tests/mispevent_testfiles/event_obj_tag.json') as f: ref_json = json.load(f) self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_malware(self): + def test_object_galaxy(self) -> None: + self.init_event() + misp_object = MISPObject('github-user') + misp_object.add_attribute('username', 'adulau') + misp_object.add_attribute('repository', 'cve-search') + self.mispevent.add_object(misp_object) + with open('tests/mispevent_testfiles/galaxy.json') as f: + galaxy = json.load(f) + misp_galaxy = MISPGalaxy() + misp_galaxy.from_dict(**galaxy) + self.mispevent.objects[0].attributes[0].add_galaxy(misp_galaxy) + self.assertEqual( + self.mispevent.objects[0].attributes[0].galaxies[0].to_json(sort_keys=True, indent=2), + json.dumps(galaxy, sort_keys=True, indent=2) + ) + + def test_malware(self) -> None: with open('tests/mispevent_testfiles/simple.json', 'rb') as f: pseudofile = BytesIO(f.read()) self.init_event() - a = self.mispevent.add_attribute('malware-sample', 'bar.exe', data=pseudofile) + a: MISPAttribute = self.mispevent.add_attribute('malware-sample', 'bar.exe', data=pseudofile) # type: ignore[assignment] del a.uuid attribute = self.mispevent.attributes[0] self.assertEqual(attribute.malware_binary, pseudofile) - with open('tests/mispevent_testfiles/malware.json', 'r') as f: + with open('tests/mispevent_testfiles/malware.json') as f: ref_json = json.load(f) del self.mispevent.uuid self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_existing_malware(self): + def test_existing_malware(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/malware_exist.json') with open('tests/mispevent_testfiles/simple.json', 'rb') as f: pseudofile = BytesIO(f.read()) - self.assertEqual( - self.mispevent.objects[0].get_attributes_by_relation('malware-sample')[0].malware_binary.read(), - pseudofile.read()) + self.assertTrue(self.mispevent.objects[0].get_attributes_by_relation('malware-sample')[0].malware_binary) + if _mb := self.mispevent.objects[0].get_attributes_by_relation('malware-sample')[0].malware_binary: + self.assertEqual(_mb.read(), pseudofile.read()) - def test_sighting(self): + def test_sighting(self) -> None: sighting = MISPSighting() sighting.from_dict(value='1', type='bar', timestamp=11111111) - with open('tests/mispevent_testfiles/sighting.json', 'r') as f: + with open('tests/mispevent_testfiles/sighting.json') as f: ref_json = json.load(f) self.assertEqual(sighting.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_existing_event(self): + def test_existing_event(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/existing_event.json') - with open('tests/mispevent_testfiles/existing_event.json', 'r') as f: + with open('tests/mispevent_testfiles/existing_event.json') as f: ref_json = json.load(f) self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_shadow_attributes_existing(self): + def test_shadow_attributes_existing(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/shadow.json') - with open('tests/mispevent_testfiles/shadow.json', 'r') as f: + with open('tests/mispevent_testfiles/shadow.json') as f: ref_json = json.load(f) self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) @unittest.skip("Not supported on MISP.") - def test_shadow_attributes(self): + def test_shadow_attributes(self) -> None: self.init_event() p = self.mispevent.add_proposal(type='filename', value='baz.jpg') del p.uuid - a = self.mispevent.add_attribute('filename', 'bar.exe') + a: MISPAttribute = self.mispevent.add_attribute('filename', 'bar.exe') # type: ignore[assignment] del a.uuid p = self.mispevent.attributes[0].add_proposal(type='filename', value='bar.pdf') del p.uuid - with open('tests/mispevent_testfiles/proposals.json', 'r') as f: + with open('tests/mispevent_testfiles/proposals.json') as f: ref_json = json.load(f) self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_default_attributes(self): + def test_default_attributes(self) -> None: self.mispevent.add_object(name='file', strict=True) - a = self.mispevent.objects[0].add_attribute('filename', value='bar', Tag=[{'name': 'blah'}]) + a: MISPAttribute = self.mispevent.objects[0].add_attribute('filename', value='bar', Tag=[{'name': 'blah'}]) # type: ignore[assignment] del a.uuid - a = self.mispevent.objects[0].add_attribute('pattern-in-file', value='baz') + a = self.mispevent.objects[0].add_attribute('pattern-in-file', value='baz') # type: ignore[assignment] self.assertEqual(a.category, 'Artifacts dropped') del a.uuid self.mispevent.add_object(name='file', strict=False, default_attributes_parameters=self.mispevent.objects[0].attributes[0]) - a = self.mispevent.objects[1].add_attribute('filename', value='baz') + a = self.mispevent.objects[1].add_attribute('filename', value='baz') # type: ignore[assignment] del a.uuid self.mispevent.objects[0].uuid = 'a' self.mispevent.objects[1].uuid = 'b' - with open('tests/mispevent_testfiles/event_obj_def_param.json', 'r') as f: + with open('tests/mispevent_testfiles/event_obj_def_param.json') as f: ref_json = json.load(f) del self.mispevent.uuid self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_obj_default_values(self): + def test_obj_default_values(self) -> None: self.init_event() self.mispevent.add_object(name='whois', strict=True) - a = self.mispevent.objects[0].add_attribute('registrar', value='registar.example.com') + a: MISPAttribute = self.mispevent.objects[0].add_attribute('registrar', value='registar.example.com') # type: ignore[assignment] del a.uuid - a = self.mispevent.objects[0].add_attribute('domain', value='domain.example.com') + a = self.mispevent.objects[0].add_attribute('domain', value='domain.example.com') # type: ignore[assignment] del a.uuid - a = self.mispevent.objects[0].add_attribute('nameserver', value='ns1.example.com') + a = self.mispevent.objects[0].add_attribute('nameserver', value='ns1.example.com') # type: ignore[assignment] del a.uuid - a = self.mispevent.objects[0].add_attribute('nameserver', value='ns2.example.com', disable_correlation=False, to_ids=True, category='External analysis') + a = self.mispevent.objects[0].add_attribute('nameserver', value='ns2.example.com', disable_correlation=False, to_ids=True, category='External analysis') # type: ignore[assignment] del a.uuid self.mispevent.objects[0].uuid = 'a' - with open('tests/mispevent_testfiles/def_param.json', 'r') as f: + with open('tests/mispevent_testfiles/def_param.json') as f: ref_json = json.load(f) del self.mispevent.uuid self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_obj_references_export(self): + def test_obj_references_export(self) -> None: self.init_event() obj1 = MISPObject(name="file") obj2 = MISPObject(name="url", standalone=False) @@ -231,29 +272,29 @@ class TestMISPEvent(unittest.TestCase): self.assertTrue("ObjectReference" in obj1.jsonable()) self.assertFalse("ObjectReference" in obj2.jsonable()) - def test_event_not_edited(self): + def test_event_not_edited(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/existing_event.json') self.assertFalse(self.mispevent.edited) - def test_event_edited(self): + def test_event_edited(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/existing_event.json') self.mispevent.info = 'blah' self.assertTrue(self.mispevent.edited) - def test_event_tag_edited(self): + def test_event_tag_edited(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/existing_event.json') self.assertFalse(self.mispevent.edited) self.mispevent.add_tag('foo') self.assertTrue(self.mispevent.edited) - def test_event_attribute_edited(self): + def test_event_attribute_edited(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/existing_event.json') self.mispevent.attributes[0].value = 'blah' self.assertTrue(self.mispevent.attributes[0].edited) self.assertFalse(self.mispevent.attributes[1].edited) self.assertTrue(self.mispevent.edited) - def test_event_attribute_tag_edited(self): + def test_event_attribute_tag_edited(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/existing_event.json') self.assertFalse(self.mispevent.edited) self.mispevent.attributes[0].tags[0].name = 'blah' @@ -262,14 +303,14 @@ class TestMISPEvent(unittest.TestCase): self.assertTrue(self.mispevent.attributes[0].edited) self.assertTrue(self.mispevent.edited) - def test_event_attribute_tag_edited_second(self): + def test_event_attribute_tag_edited_second(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/existing_event.json') self.assertFalse(self.mispevent.edited) self.mispevent.attributes[0].add_tag(name='blah') self.assertTrue(self.mispevent.attributes[0].edited) self.assertTrue(self.mispevent.edited) - def test_event_object_edited(self): + def test_event_object_edited(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/existing_event.json') self.assertFalse(self.mispevent.edited) self.mispevent.objects[0].comment = 'blah' @@ -277,7 +318,7 @@ class TestMISPEvent(unittest.TestCase): self.assertFalse(self.mispevent.objects[1].edited) self.assertTrue(self.mispevent.edited) - def test_event_object_attribute_edited(self): + def test_event_object_attribute_edited(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/existing_event.json') self.assertFalse(self.mispevent.edited) self.mispevent.objects[0].attributes[0].comment = 'blah' @@ -285,23 +326,23 @@ class TestMISPEvent(unittest.TestCase): self.assertTrue(self.mispevent.objects[0].edited) self.assertTrue(self.mispevent.edited) - def test_event_object_attribute_edited_tag(self): + def test_event_object_attribute_edited_tag(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/existing_event.json') self.assertFalse(self.mispevent.edited) self.mispevent.objects[0].attributes[0].add_tag('blah') self.assertTrue(self.mispevent.objects[0].attributes[0].edited) self.assertTrue(self.mispevent.objects[0].edited) self.assertTrue(self.mispevent.edited) - with open('tests/mispevent_testfiles/existing_event_edited.json', 'r') as f: + with open('tests/mispevent_testfiles/existing_event_edited.json') as f: ref_json = json.load(f) self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_obj_by_id(self): + def test_obj_by_id(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/existing_event.json') misp_obj = self.mispevent.get_object_by_id(1556) self.assertEqual(misp_obj.uuid, '5a3cd604-e11c-4de5-bbbf-c170950d210f') - def test_userdefined_object_custom_template(self): + def test_userdefined_object_custom_template(self) -> None: self.init_event() with open('tests/mispevent_testfiles/test_object_template/definition.json') as f: template = json.load(f) @@ -312,16 +353,16 @@ class TestMISPEvent(unittest.TestCase): self.mispevent.to_json(sort_keys=True, indent=2) self.assertEqual(e.exception.message, '{\'member3\'} are required.') - a = self.mispevent.objects[0].add_attribute('member3', value='foo') + a: MISPAttribute = self.mispevent.objects[0].add_attribute('member3', value='foo') # type: ignore[assignment] del a.uuid with self.assertRaises(InvalidMISPObject) as e: # Fail on requiredOneOf self.mispevent.to_json(sort_keys=True, indent=2) self.assertEqual(e.exception.message, 'At least one of the following attributes is required: member1, member2') - a = self.mispevent.objects[0].add_attribute('member1', value='bar') + a = self.mispevent.objects[0].add_attribute('member1', value='bar') # type: ignore[assignment] del a.uuid - a = self.mispevent.objects[0].add_attribute('member1', value='baz') + a = self.mispevent.objects[0].add_attribute('member1', value='baz') # type: ignore[assignment] del a.uuid with self.assertRaises(InvalidMISPObject) as e: # member1 is not a multiple @@ -330,12 +371,12 @@ class TestMISPEvent(unittest.TestCase): self.mispevent.objects[0].attributes = self.mispevent.objects[0].attributes[:2] self.mispevent.objects[0].uuid = 'a' - with open('tests/mispevent_testfiles/misp_custom_obj.json', 'r') as f: + with open('tests/mispevent_testfiles/misp_custom_obj.json') as f: ref_json = json.load(f) del self.mispevent.uuid self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_userdefined_object_custom_dir(self): + def test_userdefined_object_custom_dir(self) -> None: self.init_event() self.mispevent.add_object(name='test_object_template', strict=True, misp_objects_path_custom='tests/mispevent_testfiles') with self.assertRaises(InvalidMISPObject) as e: @@ -343,16 +384,16 @@ class TestMISPEvent(unittest.TestCase): self.mispevent.to_json(sort_keys=True, indent=2) self.assertEqual(e.exception.message, '{\'member3\'} are required.') - a = self.mispevent.objects[0].add_attribute('member3', value='foo') + a: MISPAttribute = self.mispevent.objects[0].add_attribute('member3', value='foo') # type: ignore[assignment] del a.uuid with self.assertRaises(InvalidMISPObject) as e: # Fail on requiredOneOf self.mispevent.to_json(sort_keys=True, indent=2) self.assertEqual(e.exception.message, 'At least one of the following attributes is required: member1, member2') - a = self.mispevent.objects[0].add_attribute('member1', value='bar') + a = self.mispevent.objects[0].add_attribute('member1', value='bar') # type: ignore[assignment] del a.uuid - a = self.mispevent.objects[0].add_attribute('member1', value='baz') + a = self.mispevent.objects[0].add_attribute('member1', value='baz') # type: ignore[assignment] del a.uuid with self.assertRaises(InvalidMISPObject) as e: # member1 is not a multiple @@ -361,15 +402,15 @@ class TestMISPEvent(unittest.TestCase): self.mispevent.objects[0].attributes = self.mispevent.objects[0].attributes[:2] self.mispevent.objects[0].uuid = 'a' - with open('tests/mispevent_testfiles/misp_custom_obj.json', 'r') as f: + with open('tests/mispevent_testfiles/misp_custom_obj.json') as f: ref_json = json.load(f) del self.mispevent.uuid self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_first_last_seen(self): + def test_first_last_seen(self) -> None: me = MISPEvent() me.info = 'Test First and Last Seen' - me.date = '2020.01.12' + me.date = '2020.01.12' # type: ignore[assignment] self.assertEqual(me.date.day, 12) me.add_attribute('ip-dst', '8.8.8.8', first_seen='06-21-1998', last_seen=1580213607.469571) self.assertEqual(me.attributes[0].first_seen.year, 1998) @@ -377,11 +418,11 @@ class TestMISPEvent(unittest.TestCase): now = datetime.now().astimezone() me.attributes[0].last_seen = now today = date.today() - me.attributes[0].first_seen = today + me.attributes[0].first_seen = today # type: ignore[assignment] self.assertEqual(me.attributes[0].first_seen.year, today.year) self.assertEqual(me.attributes[0].last_seen, now) - def test_feed(self): + def test_feed(self) -> None: me = MISPEvent() me.info = 'Test feed' org = MISPOrganisation() @@ -399,7 +440,7 @@ class TestMISPEvent(unittest.TestCase): self.assertEqual(feed['Event']['_manifest'][me.uuid]['info'], 'Test feed') self.assertEqual(len(feed['Event']['Object'][0]['Attribute']), 2) - def test_object_templates(self): + def test_object_templates(self) -> None: me = MISPEvent() for template in glob.glob(str(me.misp_objects_path / '*' / 'definition.json')): with open(template) as f: @@ -418,7 +459,7 @@ class TestMISPEvent(unittest.TestCase): subset = set(entry['categories']).issubset(me.describe_types['categories']) self.assertTrue(subset, f'{t_json["name"]} - {obj_relation}') - def test_git_vuln_finder(self): + def test_git_vuln_finder(self) -> None: with open('tests/git-vuln-finder-quagga.json') as f: dump = json.load(f) diff --git a/tests/test_reportlab.py b/tests/test_reportlab.py index 8fd40ee..d3b589b 100644 --- a/tests/test_reportlab.py +++ b/tests/test_reportlab.py @@ -1,5 +1,6 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations import os import sys diff --git a/tests/testlive_comprehensive.py b/tests/testlive_comprehensive.py index b12d216..3cd6538 100644 --- a/tests/testlive_comprehensive.py +++ b/tests/testlive_comprehensive.py @@ -1,65 +1,77 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- +from __future__ import annotations + +import hashlib +import json +import logging import os -import sys - +import time import unittest -from pymisp.tools import make_binary_objects from datetime import datetime, timedelta, date, timezone from io import BytesIO -import json from pathlib import Path -import hashlib - -import urllib3 # type: ignore -import time +from typing import TypeVar, Any from uuid import uuid4 -import email - -from collections import defaultdict - -import logging -logging.disable(logging.CRITICAL) -logger = logging.getLogger('pymisp') +import urllib3 +from pymisp.tools import make_binary_objects try: - from pymisp import register_user, PyMISP, MISPEvent, MISPOrganisation, MISPUser, Distribution, ThreatLevel, Analysis, MISPObject, MISPAttribute, MISPSighting, MISPShadowAttribute, MISPTag, MISPSharingGroup, MISPFeed, MISPServer, MISPUserSetting, MISPEventBlocklist, MISPEventReport, MISPCorrelationExclusion, MISPGalaxyCluster + from pymisp import (register_user, PyMISP, MISPEvent, MISPOrganisation, + MISPUser, Distribution, ThreatLevel, Analysis, MISPObject, + MISPAttribute, MISPSighting, MISPShadowAttribute, MISPTag, + MISPSharingGroup, MISPFeed, MISPServer, MISPUserSetting, + MISPEventReport, MISPCorrelationExclusion, MISPGalaxyCluster, + MISPGalaxy, MISPOrganisationBlocklist, MISPEventBlocklist, + MISPNote) from pymisp.tools import CSVLoader, DomainIPObject, ASNObject, GenericObjectGenerator - from pymisp.exceptions import MISPServerError except ImportError: - if sys.version_info < (3, 6): - print('This test suite requires Python 3.6+, breaking.') - sys.exit(0) - else: - raise + raise try: from keys import url, key # type: ignore verifycert = False except ImportError as e: print(e) - url = 'https://localhost:8443' - key = 'sL9hrjIyY405RyGQHLx5DoCAM92BNmmGa8P4ck1E' + url = 'https://10.197.206.83' + key = 'OdzzuBSnH83tEjvZbf7SFejC1kC3gS11Cnj2wxLk' verifycert = False +logging.disable(logging.CRITICAL) +logger = logging.getLogger('pymisp') urllib3.disable_warnings() fast_mode = False -if not Path('tests/viper-test-files').exists(): +test_file_path = Path('tests/viper-test-files') + +print(test_file_path, 'exists: ', test_file_path.exists()) + +if not test_file_path.exists(): print('The test files are missing, pulling it.') os.system('git clone https://github.com/viper-framework/viper-test-files.git tests/viper-test-files') +T = TypeVar('T', bound='TestComprehensive') + class TestComprehensive(unittest.TestCase): + admin_misp_connector: PyMISP + user_misp_connector: PyMISP + test_usr: MISPUser + test_pub: MISPUser + test_org: MISPOrganisation + test_org_delegate: MISPOrganisation + delegate_user_misp_connector: PyMISP + pub_misp_connector: PyMISP + test_usr_delegate: MISPUser + @classmethod - def setUpClass(cls): + def setUpClass(cls: type[T]) -> None: cls.maxDiff = None # Connect as admin cls.admin_misp_connector = PyMISP(url, key, verifycert, debug=False) @@ -67,22 +79,21 @@ class TestComprehensive(unittest.TestCase): cls.admin_misp_connector.set_server_setting('debug', 1, force=True) if not fast_mode: r = cls.admin_misp_connector.update_misp() - print(r) # Creates an org organisation = MISPOrganisation() organisation.name = 'Test Org' - cls.test_org = cls.admin_misp_connector.add_organisation(organisation, pythonify=True) + cls.test_org = cls.admin_misp_connector.add_organisation(organisation, pythonify=True) # type: ignore[assignment] # Create an org to delegate to organisation = MISPOrganisation() organisation.name = 'Test Org - delegate' - cls.test_org_delegate = cls.admin_misp_connector.add_organisation(organisation, pythonify=True) + cls.test_org_delegate = cls.admin_misp_connector.add_organisation(organisation, pythonify=True) # type: ignore[assignment] # Set the refault role (id 3 on the VM) cls.admin_misp_connector.set_default_role(3) # Creates a user user = MISPUser() user.email = 'testusr@user.local' user.org_id = cls.test_org.id - cls.test_usr = cls.admin_misp_connector.add_user(user, pythonify=True) + cls.test_usr = cls.admin_misp_connector.add_user(user, pythonify=True) # type: ignore[assignment] cls.user_misp_connector = PyMISP(url, cls.test_usr.authkey, verifycert, debug=True) cls.user_misp_connector.toggle_global_pythonify() # Creates a publisher @@ -90,14 +101,14 @@ class TestComprehensive(unittest.TestCase): user.email = 'testpub@user.local' user.org_id = cls.test_org.id user.role_id = 4 - cls.test_pub = cls.admin_misp_connector.add_user(user, pythonify=True) + cls.test_pub = cls.admin_misp_connector.add_user(user, pythonify=True) # type: ignore[assignment] cls.pub_misp_connector = PyMISP(url, cls.test_pub.authkey, verifycert) # Creates a user that can accept a delegation request user = MISPUser() user.email = 'testusr@delegate.recipient.local' user.org_id = cls.test_org_delegate.id user.role_id = 2 - cls.test_usr_delegate = cls.admin_misp_connector.add_user(user, pythonify=True) + cls.test_usr_delegate = cls.admin_misp_connector.add_user(user, pythonify=True) # type: ignore[assignment] cls.delegate_user_misp_connector = PyMISP(url, cls.test_usr_delegate.authkey, verifycert, debug=False) cls.delegate_user_misp_connector.toggle_global_pythonify() if not fast_mode: @@ -110,7 +121,7 @@ class TestComprehensive(unittest.TestCase): cls.admin_misp_connector.load_default_feeds() @classmethod - def tearDownClass(cls): + def tearDownClass(cls) -> None: # Delete publisher cls.admin_misp_connector.delete_user(cls.test_pub) # Delete user @@ -120,7 +131,7 @@ class TestComprehensive(unittest.TestCase): cls.admin_misp_connector.delete_organisation(cls.test_org) cls.admin_misp_connector.delete_organisation(cls.test_org_delegate) - def create_simple_event(self, force_timestamps=False): + def create_simple_event(self, force_timestamps: bool=False) -> MISPEvent: mispevent = MISPEvent(force_timestamps=force_timestamps) mispevent.info = 'This is a super simple test' mispevent.distribution = Distribution.your_organisation_only @@ -129,7 +140,7 @@ class TestComprehensive(unittest.TestCase): mispevent.add_attribute('text', str(uuid4())) return mispevent - def environment(self): + def environment(self) -> tuple[MISPEvent, MISPEvent, MISPEvent]: first_event = MISPEvent() first_event.info = 'First event - org only - low - completed' first_event.distribution = Distribution.your_organisation_only @@ -171,13 +182,13 @@ class TestComprehensive(unittest.TestCase): # Create first and third event as admin # usr won't be able to see the first one - first = self.admin_misp_connector.add_event(first_event, pythonify=True) - third = self.admin_misp_connector.add_event(third_event, pythonify=True) + first: MISPEvent = self.admin_misp_connector.add_event(first_event, pythonify=True) # type: ignore[assignment] + third: MISPEvent = self.admin_misp_connector.add_event(third_event, pythonify=True) # type: ignore[assignment] # Create second event as user - second = self.user_misp_connector.add_event(second_event) + second: MISPEvent = self.user_misp_connector.add_event(second_event) # type: ignore[assignment] return first, second, third - def test_server_settings(self): + def test_server_settings(self) -> None: settings = self.admin_misp_connector.server_settings() for final_setting in settings['finalSettings']: if final_setting['setting'] == 'MISP.max_correlations_per_event': @@ -202,7 +213,7 @@ class TestComprehensive(unittest.TestCase): setting = self.admin_misp_connector.get_server_setting('MISP.live') self.assertTrue(setting['value']) - def test_search_value_event(self): + def test_search_value_event(self) -> None: '''Search a value on the event controller * Test ACL admin user vs normal user in an other org * Make sure we have one match @@ -210,17 +221,17 @@ class TestComprehensive(unittest.TestCase): try: first, second, third = self.environment() # Search as admin - events = self.admin_misp_connector.search(value=first.attributes[0].value, pythonify=True) + events: list[MISPEvent] = self.admin_misp_connector.search(value=first.attributes[0].value, pythonify=True) # type: ignore[assignment] self.assertEqual(len(events), 2) for e in events: self.assertIn(e.id, [first.id, second.id]) # Search as user - events = self.user_misp_connector.search(value=first.attributes[0].value) + events = self.user_misp_connector.search(value=first.attributes[0].value) # type: ignore[assignment] self.assertEqual(len(events), 1) for e in events: self.assertIn(e.id, [second.id]) # Non-existing value - events = self.user_misp_connector.search(value=str(uuid4())) + events = self.user_misp_connector.search(value=str(uuid4())) # type: ignore[assignment] self.assertEqual(events, []) finally: # Delete events @@ -228,37 +239,37 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_event(second) self.admin_misp_connector.delete_event(third) - def test_search_value_attribute(self): + def test_search_value_attribute(self) -> None: '''Search value in attributes controller''' try: first, second, third = self.environment() # Search as admin - attributes = self.admin_misp_connector.search(controller='attributes', value=first.attributes[0].value, pythonify=True) + attributes: list[MISPAttribute] = self.admin_misp_connector.search(controller='attributes', value=first.attributes[0].value, pythonify=True) # type: ignore[assignment] self.assertEqual(len(attributes), 2) for a in attributes: self.assertIn(a.event_id, [first.id, second.id]) # Search as user - attributes = self.user_misp_connector.search(controller='attributes', value=first.attributes[0].value) + attributes = self.user_misp_connector.search(controller='attributes', value=first.attributes[0].value) # type: ignore[assignment] self.assertEqual(len(attributes), 1) for a in attributes: self.assertIn(a.event_id, [second.id]) # Non-existing value - attributes = self.user_misp_connector.search(controller='attributes', value=str(uuid4())) + attributes = self.user_misp_connector.search(controller='attributes', value=str(uuid4())) # type: ignore[assignment] self.assertEqual(attributes, []) # Include context - search as user (can only see one event) - attributes = self.user_misp_connector.search(controller='attributes', value=first.attributes[0].value, include_context=True, pythonify=True) + attributes = self.user_misp_connector.search(controller='attributes', value=first.attributes[0].value, include_context=True, pythonify=True) # type: ignore[assignment] self.assertTrue(isinstance(attributes[0].Event, MISPEvent)) self.assertEqual(attributes[0].Event.uuid, second.uuid) # Include context - search as admin (can see both event) - attributes = self.admin_misp_connector.search(controller='attributes', value=first.attributes[0].value, include_context=True, pythonify=True) + attributes = self.admin_misp_connector.search(controller='attributes', value=first.attributes[0].value, include_context=True, pythonify=True) # type: ignore[assignment] self.assertTrue(isinstance(attributes[0].Event, MISPEvent)) self.assertEqual(attributes[0].Event.uuid, first.uuid) self.assertEqual(attributes[1].Event.uuid, second.uuid) # Include correlations - search as admin (can see both event) - attributes = self.admin_misp_connector.search(controller='attributes', value=first.attributes[0].value, include_correlations=True, pythonify=True) + attributes = self.admin_misp_connector.search(controller='attributes', value=first.attributes[0].value, include_correlations=True, pythonify=True) # type: ignore[assignment] self.assertTrue(isinstance(attributes[0].Event, MISPEvent)) self.assertEqual(attributes[0].Event.uuid, first.uuid) self.assertEqual(attributes[1].Event.uuid, second.uuid) @@ -266,8 +277,9 @@ class TestComprehensive(unittest.TestCase): self.assertEqual(attributes[1].RelatedAttribute[0].Event.uuid, first.uuid) # Include sightings - search as admin (can see both event) - self.admin_misp_connector.add_sighting({'value': first.attributes[0].value}) - attributes = self.admin_misp_connector.search(controller='attributes', value=first.attributes[0].value, include_sightings=True, pythonify=True) + s: dict[str, Any] = {'value': first.attributes[0].value} + self.admin_misp_connector.add_sighting(s) + attributes = self.admin_misp_connector.search(controller='attributes', value=first.attributes[0].value, include_sightings=True, pythonify=True) # type: ignore[assignment] self.assertTrue(isinstance(attributes[0].Event, MISPEvent)) self.assertEqual(attributes[0].Event.uuid, first.uuid) self.assertEqual(attributes[1].Event.uuid, second.uuid) @@ -279,17 +291,21 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_event(second) self.admin_misp_connector.delete_event(third) - def test_search_type_event(self): + def test_search_type_event(self) -> None: '''Search multiple events, search events containing attributes with specific types''' try: first, second, third = self.environment() # Search as admin - events = self.admin_misp_connector.search(timestamp=first.timestamp.timestamp(), pythonify=True) + if isinstance(first.timestamp, datetime): + ts = first.timestamp.timestamp() + else: + ts = first.timestamp + events: list[MISPEvent] = self.admin_misp_connector.search(timestamp=ts, pythonify=True) # type: ignore[assignment] self.assertEqual(len(events), 3) for e in events: self.assertIn(e.id, [first.id, second.id, third.id]) attributes_types_search = self.admin_misp_connector.build_complex_query(or_parameters=['ip-src', 'ip-dst']) - events = self.admin_misp_connector.search(timestamp=first.timestamp.timestamp(), + events = self.admin_misp_connector.search(timestamp=ts, # type: ignore[assignment,type-var] type_attribute=attributes_types_search, pythonify=True) self.assertEqual(len(events), 2) for e in events: @@ -300,28 +316,32 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_event(second) self.admin_misp_connector.delete_event(third) - def test_search_index(self): + def test_search_index(self) -> None: try: first, second, third = self.environment() # Search as admin - events = self.admin_misp_connector.search_index(timestamp=first.timestamp.timestamp(), pythonify=True) + if isinstance(first.timestamp, datetime): + ts = first.timestamp.timestamp() + else: + ts = first.timestamp + events: MISPEvent = self.admin_misp_connector.search_index(timestamp=ts, pythonify=True) # type: ignore[assignment] self.assertEqual(len(events), 3) for e in events: self.assertIn(e.id, [first.id, second.id, third.id]) # Test limit and pagination - event_one = self.admin_misp_connector.search_index(timestamp=first.timestamp.timestamp(), limit=1, page=1, pythonify=True)[0] - event_two = self.admin_misp_connector.search_index(timestamp=first.timestamp.timestamp(), limit=1, page=2, pythonify=True)[0] + event_one: MISPEvent = self.admin_misp_connector.search_index(timestamp=ts, limit=1, page=1, pythonify=True)[0] # type: ignore[index,assignment] + event_two: MISPEvent = self.admin_misp_connector.search_index(timestamp=ts, limit=1, page=2, pythonify=True)[0] # type: ignore[index,assignment] self.assertTrue(event_one.id != event_two.id) two_events = self.admin_misp_connector.search_index(limit=2) self.assertTrue(len(two_events), 2) # Test ordering by the Info field. Can't use timestamp as each will likely have the same - event = self.admin_misp_connector.search_index(timestamp=first.timestamp.timestamp(), sort="info", desc=True, limit=1, pythonify=True)[0] + event: MISPEvent = self.admin_misp_connector.search_index(timestamp=ts, sort="info", desc=True, limit=1, pythonify=True)[0] # type: ignore[index,assignment] # First|Second|*Third* event self.assertEqual(event.id, third.id) # *First*|Second|Third event - event = self.admin_misp_connector.search_index(timestamp=first.timestamp.timestamp(), sort="info", desc=False, limit=1, pythonify=True)[0] + event = self.admin_misp_connector.search_index(timestamp=ts, sort="info", desc=False, limit=1, pythonify=True)[0] # type: ignore[index,assignment] self.assertEqual(event.id, first.id) finally: # Delete event @@ -329,7 +349,7 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_event(second) self.admin_misp_connector.delete_event(third) - def test_search_objects(self): + def test_search_objects(self) -> None: '''Search for objects''' try: first = self.create_simple_event() @@ -347,7 +367,7 @@ class TestComprehensive(unittest.TestCase): # Delete event self.admin_misp_connector.delete_event(first) - def test_search_type_attribute(self): + def test_search_type_attribute(self) -> None: '''Search multiple attributes, search attributes with specific types''' try: first, second, third = self.environment() @@ -371,7 +391,7 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_event(second) self.admin_misp_connector.delete_event(third) - def test_search_tag_event(self): + def test_search_tag_event(self) -> None: '''Search Tags at events level''' try: first, second, third = self.environment() @@ -405,7 +425,7 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_event(second) self.admin_misp_connector.delete_event(third) - def test_search_tag_attribute(self): + def test_search_tag_attribute(self) -> None: '''Search Tags at attributes level''' try: first, second, third = self.environment() @@ -432,7 +452,7 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_event(second) self.admin_misp_connector.delete_event(third) - def test_search_tag_advanced_event(self): + def test_search_tag_advanced_event(self) -> None: '''Advanced search Tags at events level''' try: first, second, third = self.environment() @@ -462,7 +482,7 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_event(second) self.admin_misp_connector.delete_event(third) - def test_search_tag_advanced_attributes(self): + def test_search_tag_advanced_attributes(self) -> None: '''Advanced search Tags at attributes level''' try: first, second, third = self.environment() @@ -481,7 +501,7 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_event(second) self.admin_misp_connector.delete_event(third) - def test_search_timestamp_event(self): + def test_search_timestamp_event(self) -> None: '''Search specific update timestamps at events level''' # Creating event 1 - timestamp 5 min ago first = self.create_simple_event(force_timestamps=True) @@ -517,7 +537,7 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_event(first) self.admin_misp_connector.delete_event(second) - def test_search_timestamp_attribute(self): + def test_search_timestamp_attribute(self) -> None: '''Search specific update timestamps at attributes level''' # Creating event 1 - timestamp 5 min ago first = self.create_simple_event(force_timestamps=True) @@ -555,7 +575,7 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_event(first) self.admin_misp_connector.delete_event(second) - def test_user_perms(self): + def test_user_perms(self) -> None: '''Test publish rights''' try: first = self.create_simple_event() @@ -571,7 +591,7 @@ class TestComprehensive(unittest.TestCase): # Delete event self.admin_misp_connector.delete_event(first) - def test_delete_with_update(self): + def test_delete_with_update(self) -> None: try: first = self.create_simple_event() obj = MISPObject('file') @@ -596,14 +616,14 @@ class TestComprehensive(unittest.TestCase): # Delete event self.admin_misp_connector.delete_event(first) - def test_get_non_exists_event(self): + def test_get_non_exists_event(self) -> None: event = self.user_misp_connector.get_event(0) # non exists id self.assertEqual(event['errors'][0], 404) event = self.user_misp_connector.get_event("ab2b6e28-fda5-4282-bf60-22b81de77851") # non exists uuid self.assertEqual(event['errors'][0], 404) - def test_delete_by_uuid(self): + def test_delete_by_uuid(self) -> None: try: first = self.create_simple_event() obj = MISPObject('file') @@ -640,7 +660,7 @@ class TestComprehensive(unittest.TestCase): # Delete event self.admin_misp_connector.delete_event(first) - def test_search_publish_timestamp(self): + def test_search_publish_timestamp(self) -> None: '''Search for a specific publication timestamp, an interval, and invalid values.''' # Creating event 1 first = self.create_simple_event() @@ -679,7 +699,7 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_event(first) self.admin_misp_connector.delete_event(second) - def test_search_decay(self): + def test_search_decay(self) -> None: # Creating event 1 first = self.create_simple_event() first.add_attribute('ip-dst', '8.8.8.8') @@ -704,7 +724,7 @@ class TestComprehensive(unittest.TestCase): # Delete event self.admin_misp_connector.delete_event(first) - def test_default_distribution(self): + def test_default_distribution(self) -> None: '''The default distributions on the VM are This community only for the events and Inherit from event for attr/obj)''' first = self.create_simple_event() del first.distribution @@ -746,7 +766,7 @@ class TestComprehensive(unittest.TestCase): # Delete event self.admin_misp_connector.delete_event(first) - def test_exists(self): + def test_exists(self) -> None: """Check event, attribute and object existence""" event = self.create_simple_event() misp_object = MISPObject('domain-ip') @@ -783,7 +803,7 @@ class TestComprehensive(unittest.TestCase): self.assertFalse(self.user_misp_connector.object_exists(misp_object)) self.assertFalse(self.user_misp_connector.object_exists(misp_object.id)) - def test_simple_event(self): + def test_simple_event(self) -> None: '''Search a bunch of parameters: * Value not existing * only return metadata @@ -843,6 +863,19 @@ class TestComprehensive(unittest.TestCase): self.assertEqual(events, []) events = self.user_misp_connector.search(timestamp=timeframe, published=False) self.assertEqual(len(events), 2) + # check publish & search + bg_processing_state = self.admin_misp_connector.get_server_setting('MISP.background_jobs')['value'] + self.admin_misp_connector.set_server_setting('MISP.background_jobs', False, force=True) + publish_result = self.admin_misp_connector.publish(second) + self.assertEqual(publish_result["success"], True) + second = self.admin_misp_connector.get_event(second, pythonify=True) + # check if the publishing succeeded + time.sleep(1) + self.assertEqual(second.published, True) + self.admin_misp_connector.set_server_setting('MISP.background_jobs', bg_processing_state, force=True) + events = self.user_misp_connector.search(timestamp=timeframe, published=False) + self.assertEqual(len(events), 1) + events = self.user_misp_connector.search(eventid=first.id) self.assertEqual(len(events), 1) self.assertEqual(events[0].id, first.id) @@ -1002,7 +1035,7 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_event(first) self.admin_misp_connector.delete_event(second) - def test_event_add_update_metadata(self): + def test_event_add_update_metadata(self) -> None: event = self.create_simple_event() event.add_attribute('ip-src', '9.9.9.9') try: @@ -1016,7 +1049,7 @@ class TestComprehensive(unittest.TestCase): finally: # cleanup self.admin_misp_connector.delete_event(event) - def test_extend_event(self): + def test_extend_event(self) -> None: first = self.create_simple_event() first.info = 'parent event' first.add_tag('tlp:amber___test') @@ -1037,7 +1070,7 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_event(first) self.admin_misp_connector.delete_event(second) - def test_edit_attribute(self): + def test_edit_attribute(self) -> None: first = self.create_simple_event() try: first.attributes[0].comment = 'This is the original comment' @@ -1057,7 +1090,7 @@ class TestComprehensive(unittest.TestCase): # Delete event self.admin_misp_connector.delete_event(first) - def test_sightings(self): + def test_sightings(self) -> None: first = self.create_simple_event() second = self.create_simple_event() try: @@ -1132,7 +1165,7 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_event(first) self.admin_misp_connector.delete_event(second) - def test_search_csv(self): + def test_search_csv(self) -> None: first = self.create_simple_event() first.attributes[0].comment = 'This is the original comment' second = self.create_simple_event() @@ -1212,7 +1245,7 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_event(first) self.admin_misp_connector.delete_event(second) - def test_search_text(self): + def test_search_text(self) -> None: first = self.create_simple_event() first.add_attribute('ip-src', '8.8.8.8') first.publish() @@ -1226,7 +1259,7 @@ class TestComprehensive(unittest.TestCase): # Delete event self.admin_misp_connector.delete_event(first) - def test_search_stix(self): + def test_search_stix(self) -> None: first = self.create_simple_event() first.add_attribute('ip-src', '8.8.8.8') try: @@ -1241,7 +1274,7 @@ class TestComprehensive(unittest.TestCase): # Delete event self.admin_misp_connector.delete_event(first) - def test_update_object(self): + def test_update_object(self) -> None: first = self.create_simple_event() ip_dom = MISPObject('domain-ip') ip_dom.add_attribute('domain', value='google.fr') @@ -1345,7 +1378,7 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_event(first) self.admin_misp_connector.delete_event(second) - def test_custom_template(self): + def test_custom_template(self) -> None: first = self.create_simple_event() try: with open('tests/viper-test-files/test_files/whoami.exe', 'rb') as f: @@ -1387,7 +1420,7 @@ class TestComprehensive(unittest.TestCase): # Delete event self.admin_misp_connector.delete_event(first) - def test_unknown_template(self): + def test_unknown_template(self) -> None: first = self.create_simple_event() attributeAsDict = [{'MyCoolAttribute': {'value': 'critical thing', 'type': 'text'}}, {'MyCoolerAttribute': {'value': 'even worse', 'type': 'text', 'disable_correlation': True}}] @@ -1421,7 +1454,7 @@ class TestComprehensive(unittest.TestCase): # Delete event self.admin_misp_connector.delete_event(first) - def test_domain_ip_object(self): + def test_domain_ip_object(self) -> None: first = self.create_simple_event() try: dom_ip_obj = DomainIPObject({'ip': ['1.1.1.1', {'value': '2.2.2.2', 'to_ids': False}], @@ -1435,7 +1468,7 @@ class TestComprehensive(unittest.TestCase): # Delete event self.admin_misp_connector.delete_event(first) - def test_asn_object(self): + def test_asn_object(self) -> None: first = self.create_simple_event() try: dom_ip_obj = ASNObject({'asn': '12345', @@ -1448,7 +1481,7 @@ class TestComprehensive(unittest.TestCase): # Delete event self.admin_misp_connector.delete_event(first) - def test_object_template(self): + def test_object_template(self) -> None: r = self.admin_misp_connector.update_object_templates() self.assertEqual(type(r), list) object_templates = self.admin_misp_connector.object_templates(pythonify=True) @@ -1467,7 +1500,7 @@ class TestComprehensive(unittest.TestCase): mo.add_attribute('domain', 'google.fr') self.assertEqual(mo.template_uuid, '4') - def test_tags(self): + def test_tags(self) -> None: # Get list tags = self.admin_misp_connector.tags(pythonify=True) self.assertTrue(isinstance(tags, list)) @@ -1556,7 +1589,7 @@ class TestComprehensive(unittest.TestCase): response = self.admin_misp_connector.delete_tag(tag_org_restricted) response = self.admin_misp_connector.delete_tag(tag_user_restricted) - def test_add_event_with_attachment_object_controller(self): + def test_add_event_with_attachment_object_controller(self) -> None: first = self.create_simple_event() try: first = self.user_misp_connector.add_event(first) @@ -1596,7 +1629,48 @@ class TestComprehensive(unittest.TestCase): # Delete event self.admin_misp_connector.delete_event(first) - def test_lief_and_sign(self): + def test_add_event_with_attachment_object_controller__hard(self) -> None: + first = self.create_simple_event() + try: + first = self.user_misp_connector.add_event(first) + fo, peo, seos = make_binary_objects('tests/viper-test-files/test_files/whoami.exe') + for s in seos: + r = self.user_misp_connector.add_object(first, s) + self.assertEqual(r.name, 'pe-section', r) + + r = self.user_misp_connector.add_object(first, peo, pythonify=True) + self.assertEqual(r.name, 'pe', r) + for ref in peo.ObjectReference: + r = self.user_misp_connector.add_object_reference(ref) + self.assertEqual(r.object_uuid, peo.uuid, r.to_json()) + + r = self.user_misp_connector.add_object(first, fo) + obj_attrs = r.get_attributes_by_relation('ssdeep') + self.assertEqual(len(obj_attrs), 1, obj_attrs) + self.assertEqual(r.name, 'file', r) + + # Test break_on_duplicate at object level + fo_dup, peo_dup, _ = make_binary_objects('tests/viper-test-files/test_files/whoami.exe') + r = self.user_misp_connector.add_object(first, peo_dup, break_on_duplicate=True) + self.assertTrue("Duplicate object found" in r['errors'][1]['errors'], r) + + # Test break on duplicate with breakOnDuplicate key in object + fo_dup.breakOnDuplicate = True + r = self.user_misp_connector.add_object(first, fo_dup) + self.assertTrue("Duplicate object found" in r['errors'][1]['errors'], r) + + # Test refs + r = self.user_misp_connector.add_object_reference(fo.ObjectReference[0]) + self.assertEqual(r.object_uuid, fo.uuid, r.to_json()) + self.assertEqual(r.referenced_uuid, peo.uuid, r.to_json()) + r = self.user_misp_connector.delete_object_reference(r, hard=True) + self.assertEqual(r['message'], 'ObjectReference deleted') + # TODO: verify that the reference is not soft-deleted instead + finally: + # Delete event + self.admin_misp_connector.delete_event(first) + + def test_lief_and_sign(self) -> None: first = self.create_simple_event() try: first = self.user_misp_connector.add_event(first) @@ -1640,10 +1714,11 @@ class TestComprehensive(unittest.TestCase): # Delete event self.admin_misp_connector.delete_event(first) - def test_add_event_with_attachment(self): - first = self.create_simple_event() + def test_add_event_with_attachment(self) -> None: + first_send = self.create_simple_event() try: - first = self.user_misp_connector.add_event(first) + first = self.user_misp_connector.add_event(first_send) + self.assertTrue(isinstance(first, MISPEvent), first) file_obj, bin_obj, sections = make_binary_objects('tests/viper-test-files/test_files/whoami.exe', standalone=False) first.add_object(file_obj) first.add_object(bin_obj) @@ -1658,7 +1733,7 @@ class TestComprehensive(unittest.TestCase): # Delete event self.admin_misp_connector.delete_event(first) - def test_taxonomies(self): + def test_taxonomies(self) -> None: # Make sure we're up-to-date r = self.admin_misp_connector.update_taxonomies() self.assertEqual(r['name'], 'All taxonomy libraries are up to date already.') @@ -1698,7 +1773,7 @@ class TestComprehensive(unittest.TestCase): # Return back to default required status r = self.admin_misp_connector.set_taxonomy_required(tax, not tax.required) - def test_warninglists(self): + def test_warninglists(self) -> None: # Make sure we're up-to-date r = self.admin_misp_connector.update_warninglists() self.assertTrue('name' in r, msg=r) @@ -1727,7 +1802,7 @@ class TestComprehensive(unittest.TestCase): r = self.admin_misp_connector.disable_warninglist(testwl) self.assertEqual(r['success'], '1 warninglist(s) disabled') - def test_noticelists(self): + def test_noticelists(self) -> None: # Make sure we're up-to-date r = self.admin_misp_connector.update_noticelists() self.assertEqual(r['name'], 'All noticelists are up to date already.') @@ -1748,7 +1823,7 @@ class TestComprehensive(unittest.TestCase): r = self.admin_misp_connector.disable_noticelist(testnl) self.assertFalse(r['Noticelist']['enabled'], r) - def test_correlation_exclusions(self): + def test_correlation_exclusions(self) -> None: newce = MISPCorrelationExclusion() newce.value = "test-correlation-exclusion" r = self.admin_misp_connector.add_correlation_exclusion(newce, pythonify=True) @@ -1763,7 +1838,7 @@ class TestComprehensive(unittest.TestCase): r = self.admin_misp_connector.clean_correlation_exclusions() self.assertTrue(r['success']) - def test_galaxies(self): + def test_galaxies(self) -> None: # Make sure we're up-to-date r = self.admin_misp_connector.update_galaxies() self.assertEqual(r['name'], 'Galaxies updated.') @@ -1779,7 +1854,7 @@ class TestComprehensive(unittest.TestCase): # FIXME: Fails due to https://github.com/MISP/MISP/issues/4855 # self.assertTrue('GalaxyCluster' in r) - def test_zmq(self): + def test_zmq(self) -> None: first = self.create_simple_event() try: first = self.user_misp_connector.add_event(first) @@ -1789,7 +1864,7 @@ class TestComprehensive(unittest.TestCase): # Delete event self.admin_misp_connector.delete_event(first) - def test_csv_loader(self): + def test_csv_loader(self) -> None: csv1 = CSVLoader(template_name='file', csv_path=Path('tests/csv_testfiles/valid_fieldnames.csv')) event = MISPEvent() event.info = 'Test event from CSV loader' @@ -1797,7 +1872,7 @@ class TestComprehensive(unittest.TestCase): event.add_object(**o) csv2 = CSVLoader(template_name='file', csv_path=Path('tests/csv_testfiles/invalid_fieldnames.csv'), - fieldnames=['SHA1', 'fileName', 'size-in-bytes'], has_fieldnames=True) + fieldnames=['sha1', 'filename', 'size-in-bytes'], has_fieldnames=True) try: first = self.user_misp_connector.add_event(event) for o in csv2.load(): @@ -1807,7 +1882,7 @@ class TestComprehensive(unittest.TestCase): # Delete event self.admin_misp_connector.delete_event(first) - def test_user(self): + def test_user(self) -> None: # Get list users = self.admin_misp_connector.users(pythonify=True) self.assertTrue(isinstance(users, list)) @@ -1829,7 +1904,7 @@ class TestComprehensive(unittest.TestCase): key = self.user_misp_connector.get_new_authkey() self.assertTrue(isinstance(key, str)) - def test_organisation(self): + def test_organisation(self) -> None: # Get list orgs = self.admin_misp_connector.organisations(pythonify=True) self.assertTrue(isinstance(orgs, list)) @@ -1846,7 +1921,7 @@ class TestComprehensive(unittest.TestCase): organisation = self.admin_misp_connector.update_organisation(organisation, pythonify=True) self.assertEqual(organisation.name, 'blah', organisation) - def test_org_search(self): + def test_org_search(self) -> None: orgs = self.admin_misp_connector.organisations(pythonify=True) org_name = 'ORGNAME' # Search by the org name @@ -1857,7 +1932,7 @@ class TestComprehensive(unittest.TestCase): # This org should have the name ORGNAME self.assertEqual(orgs[0].name, org_name) - def test_user_search(self): + def test_user_search(self) -> None: users = self.admin_misp_connector.users(pythonify=True) emailAddr = users[0].email @@ -1873,7 +1948,7 @@ class TestComprehensive(unittest.TestCase): self.assertTrue(len(users) == 1) self.assertEqual(users[0].email, emailAddr) - def test_attribute(self): + def test_attribute(self) -> None: first = self.create_simple_event() second = self.create_simple_event() a = second.add_attribute('ip-src', '11.11.11.11') @@ -1901,6 +1976,15 @@ class TestComprehensive(unittest.TestCase): similar_error = self.user_misp_connector.add_attribute(first, new_similar) self.assertEqual(similar_error['errors'][1]['errors']['value'][0], 'A similar attribute already exists for this event.') + # Test add attribute break_on_duplicate=False + time.sleep(5) + new_similar = MISPAttribute() + new_similar.value = '1.2.3.4' + new_similar.type = 'ip-dst' + new_similar = self.user_misp_connector.add_attribute(first, new_similar, break_on_duplicate=False) + self.assertTrue(isinstance(new_similar, MISPAttribute), new_similar) + self.assertGreater(new_similar.timestamp, new_attribute.timestamp) + # Test add multiple attributes at once attr0 = MISPAttribute() attr0.value = '0.0.0.0' @@ -2033,7 +2117,7 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_event(first) self.admin_misp_connector.delete_event(second) - def test_search_type_event_csv(self): + def test_search_type_event_csv(self) -> None: try: first, second, third = self.environment() # Search as admin @@ -2051,7 +2135,8 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_event(second) self.admin_misp_connector.delete_event(third) - def test_search_logs(self): + @unittest.skip("Not very important, skip for now.") + def test_search_logs(self) -> None: r = self.admin_misp_connector.update_user({'email': 'testusr-changed@user.local'}, self.test_usr) r = self.admin_misp_connector.search_logs(model='User', created=date.today(), pythonify=True) for entry in r[-1:]: @@ -2061,31 +2146,30 @@ class TestComprehensive(unittest.TestCase): self.assertEqual(entry.action, 'edit') self.admin_misp_connector.update_user({'email': 'testusr@user.local'}, self.test_usr) - page = 1 - while True: - r = self.admin_misp_connector.search_logs(model='User', limit=1, page=page, created=date.today(), pythonify=True) - if not r: - break - page += 1 + time.sleep(5) + r = self.admin_misp_connector.search_logs(model='User', limit=1, page=1, created=date.today(), pythonify=True) + if r: last_change = r[0] - self.assertEqual(last_change['change'], 'email (testusr-changed@user.local) => (testusr@user.local)', last_change) + self.assertEqual(last_change['change'], 'email (testusr-changed@user.local) => (testusr@user.local)', last_change) + else: + raise Exception('Unable to find log entry after updating the user') - def test_db_schema(self): + def test_db_schema(self) -> None: diag = self.admin_misp_connector.db_schema_diagnostic() self.assertEqual(diag['actual_db_version'], diag['expected_db_version'], diag) - def test_live_acl(self): + def test_live_acl(self) -> None: missing_acls = self.admin_misp_connector.remote_acl() self.assertEqual(missing_acls, [], msg=missing_acls) - def test_roles(self): + def test_roles(self) -> None: role = self.admin_misp_connector.set_default_role(4) self.assertEqual(role['message'], 'Default role set.') self.admin_misp_connector.set_default_role(3) roles = self.admin_misp_connector.roles(pythonify=True) self.assertTrue(isinstance(roles, list)) - def test_describe_types(self): + def test_describe_types(self) -> None: remote = self.admin_misp_connector.describe_types_remote remote_types = remote.pop('types') remote_categories = remote.pop('categories') @@ -2104,12 +2188,12 @@ class TestComprehensive(unittest.TestCase): for typ in mapping: self.assertIn(typ, remote_types) - def test_versions(self): + def test_versions(self) -> None: self.assertEqual(self.user_misp_connector.version, self.user_misp_connector.pymisp_version_master) self.assertEqual(self.user_misp_connector.misp_instance_version['version'], self.user_misp_connector.misp_instance_version_master['version']) - def test_statistics(self): + def test_statistics(self) -> None: try: # Attributes first, second, third = self.environment() @@ -2158,7 +2242,7 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_event(second) self.admin_misp_connector.delete_event(third) - def test_direct(self): + def test_direct(self) -> None: try: r = self.user_misp_connector.direct_call('events/add', data={'info': 'foo'}) event = MISPEvent() @@ -2175,7 +2259,7 @@ class TestComprehensive(unittest.TestCase): finally: self.admin_misp_connector.delete_event(event) - def test_freetext(self): + def test_freetext(self) -> None: first = self.create_simple_event() try: self.admin_misp_connector.toggle_warninglist(warninglist_name='%dns resolv%', force_enable=True) @@ -2211,7 +2295,7 @@ class TestComprehensive(unittest.TestCase): # Delete event self.admin_misp_connector.delete_event(first) - def test_sharing_groups(self): + def test_sharing_groups(self) -> None: # add sg = MISPSharingGroup() sg.name = 'Testcases SG' @@ -2221,10 +2305,14 @@ class TestComprehensive(unittest.TestCase): self.assertEqual(sharing_group.releasability, 'Testing') # Change releasability - r = self.admin_misp_connector.update_sharing_group({"releasability": "Testing updated"}, sharing_group, pythonify=True) - self.assertEqual(r.releasability, 'Testing updated') - r = self.admin_misp_connector.update_sharing_group({"releasability": "Testing updated - 2"}, sharing_group) - self.assertEqual(r['SharingGroup']['releasability'], 'Testing updated - 2') + r = self.admin_misp_connector.update_sharing_group({"releasability": "Testing updated"}, sharing_group) + self.assertEqual(r['SharingGroup']['releasability'], 'Testing updated') + r = self.admin_misp_connector.update_sharing_group({"releasability": "Testing updated - 2"}, sharing_group, pythonify=True) + self.assertEqual(r.releasability, 'Testing updated - 2') + # Change name + r.name = 'Testcases SG - new name' + r = self.admin_misp_connector.update_sharing_group(r, pythonify=True) + self.assertEqual(r.name, 'Testcases SG - new name') # Test `sharing_group_exists` method self.assertTrue(self.admin_misp_connector.sharing_group_exists(sharing_group)) @@ -2243,7 +2331,7 @@ class TestComprehensive(unittest.TestCase): # Get list sharing_groups = self.admin_misp_connector.sharing_groups(pythonify=True) self.assertTrue(isinstance(sharing_groups, list)) - self.assertEqual(sharing_groups[0].name, 'Testcases SG') + self.assertEqual(sharing_groups[0].name, 'Testcases SG - new name') # Use the SG @@ -2257,7 +2345,7 @@ class TestComprehensive(unittest.TestCase): try: first = self.user_misp_connector.add_event(first) first = self.admin_misp_connector.change_sharing_group_on_entity(first, sharing_group.id, pythonify=True) - self.assertEqual(first.SharingGroup['name'], 'Testcases SG') + self.assertEqual(first.SharingGroup['name'], 'Testcases SG - new name') first_object = self.admin_misp_connector.change_sharing_group_on_entity(first.objects[0], sharing_group.id, pythonify=True) self.assertEqual(first_object.sharing_group_id, sharing_group.id) @@ -2283,7 +2371,7 @@ class TestComprehensive(unittest.TestCase): self.assertFalse(self.admin_misp_connector.sharing_group_exists(sharing_group.id)) self.assertFalse(self.admin_misp_connector.sharing_group_exists(sharing_group.uuid)) - def test_sharing_group(self): + def test_sharing_group(self) -> None: # add sg = MISPSharingGroup() sg.name = 'Testcases SG' @@ -2308,7 +2396,7 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_sharing_group(sharing_group.id) self.assertFalse(self.admin_misp_connector.sharing_group_exists(sharing_group)) - def test_sharing_group_search(self): + def test_sharing_group_search(self) -> None: # Add sharing group sg = MISPSharingGroup() sg.name = 'Testcases SG' @@ -2355,7 +2443,7 @@ class TestComprehensive(unittest.TestCase): self.assertFalse(self.admin_misp_connector.sharing_group_exists(sharing_group)) - def test_feeds(self): + def test_feeds(self) -> None: # Add feed = MISPFeed() feed.name = 'TestFeed' @@ -2438,7 +2526,7 @@ class TestComprehensive(unittest.TestCase): self.assertFalse(updated_feed.enabled) self.assertEqual(updated_feed.settings, e_thread_csv_feed.settings) - def test_servers(self): + def test_servers(self) -> None: # add server = MISPServer() server.name = 'Test Server' @@ -2458,7 +2546,7 @@ class TestComprehensive(unittest.TestCase): r = self.admin_misp_connector.delete_server(server) self.assertEqual(r['name'], 'Server deleted') - def test_roles_expanded(self): + def test_roles_expanded(self) -> None: '''Test all possible things regarding roles 1. Use existing roles (ID in test VM): * Read only (6): Can only connect via API and see events visible by its organisation @@ -2576,7 +2664,7 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_user(test_roles_user) self.admin_misp_connector.delete_tag(test_tag) - def test_expansion(self): + def test_expansion(self) -> None: first = self.create_simple_event() try: md5_disk = hashlib.md5() @@ -2613,7 +2701,7 @@ class TestComprehensive(unittest.TestCase): # Delete event self.admin_misp_connector.delete_event(first) - def test_user_settings(self): + def test_user_settings(self) -> None: first = self.create_simple_event() first.distribution = 3 first.add_tag('test_publish_filter') @@ -2641,7 +2729,8 @@ class TestComprehensive(unittest.TestCase): # # Enable autoalert on admin self.admin_misp_connector._current_user.autoalert = True self.admin_misp_connector._current_user.termsaccepted = True - self.user_misp_connector.update_user(self.admin_misp_connector._current_user) + admin_usr = self.admin_misp_connector.update_user(self.admin_misp_connector._current_user, pythonify=True) + self.assertTrue(admin_usr.autoalert) first = self.admin_misp_connector.add_event(first, pythonify=True) second = self.admin_misp_connector.add_event(second, pythonify=True) @@ -2674,7 +2763,7 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_event(first) self.admin_misp_connector.delete_event(second) - def test_communities(self): + def test_communities(self) -> None: communities = self.admin_misp_connector.communities(pythonify=True) self.assertEqual(communities[0].name, 'CIRCL Private Sector Information Sharing Community - aka MISPPRIV') community = self.admin_misp_connector.get_community(communities[1], pythonify=True) @@ -2688,7 +2777,7 @@ class TestComprehensive(unittest.TestCase): # if k == 'To': # self.assertEqual(v, 'info@circl.lu') - def test_upload_stix(self): + def test_upload_stix(self) -> None: # FIXME https://github.com/MISP/MISP/issues/4892 try: r1 = self.user_misp_connector.upload_stix('tests/stix1.xml-utf8', version='1') @@ -2719,7 +2808,7 @@ class TestComprehensive(unittest.TestCase): except Exception: pass - def test_toggle_global_pythonify(self): + def test_toggle_global_pythonify(self) -> None: first = self.create_simple_event() second = self.create_simple_event() try: @@ -2734,7 +2823,7 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_event(first) self.admin_misp_connector.delete_event(second) - def test_first_last_seen(self): + def test_first_last_seen(self) -> None: event = MISPEvent() event.info = 'Test First Last seen' event.add_attribute('ip-dst', '8.8.8.8', first_seen='2020-01-03', last_seen='2020-01-04T12:30:34.323242+0800') @@ -2781,7 +2870,7 @@ class TestComprehensive(unittest.TestCase): finally: self.admin_misp_connector.delete_event(first) - def test_registrations(self): + def test_registrations(self) -> None: r = register_user(url, 'self_register@user.local', organisation=self.test_org, org_name=self.test_org.name, verify=verifycert) self.assertTrue(r['saved']) @@ -2811,7 +2900,7 @@ class TestComprehensive(unittest.TestCase): m = self.admin_misp_connector.discard_user_registration(registrations[1].id) self.assertEqual(m['name'], '1 registration(s) discarded.') - def test_search_workflow(self): + def test_search_workflow(self) -> None: first = self.create_simple_event() first.add_attribute('domain', 'google.com') tag = MISPTag() @@ -2860,7 +2949,7 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_event(first) self.admin_misp_connector.delete_tag(tag) - def test_search_workflow_ts(self): + def test_search_workflow_ts(self) -> None: first = self.create_simple_event() first.add_attribute('domain', 'google.com') tag = MISPTag() @@ -2909,14 +2998,14 @@ class TestComprehensive(unittest.TestCase): self.admin_misp_connector.delete_event(first) self.admin_misp_connector.delete_tag(tag) - def test_blocklists(self): + def test_blocklists(self) -> None: first = self.create_simple_event() second = self.create_simple_event() second.Orgc = self.test_org - to_delete = {'bl_events': [], 'bl_organisations': []} + to_delete: dict[str, MISPOrganisationBlocklist | MISPEventBlocklist] = {'bl_events': [], 'bl_organisations': []} try: # test events BL - ebl = self.admin_misp_connector.add_event_blocklist(uuids=[first.uuid]) + ebl: MISPEventBlocklist = self.admin_misp_connector.add_event_blocklist(uuids=[first.uuid]) self.assertEqual(ebl['result']['successes'][0], first.uuid, ebl) bl_events = self.admin_misp_connector.event_blocklists(pythonify=True) for ble in bl_events: @@ -2950,7 +3039,7 @@ class TestComprehensive(unittest.TestCase): blo.comment = 'This is a test' blo.org_name = 'bar' - blo = self.admin_misp_connector.update_organisation_blocklist(blo, pythonify=True) + blo: MISPOrganisationBlocklist = self.admin_misp_connector.update_organisation_blocklist(blo, pythonify=True) self.assertEqual(blo.org_name, 'bar') r = self.admin_misp_connector.delete_organisation_blocklist(blo) self.assertTrue(r['success']) @@ -2961,15 +3050,15 @@ class TestComprehensive(unittest.TestCase): for blo in to_delete['bl_organisations']: self.admin_misp_connector.delete_organisation_blocklist(blo) - def test_event_report(self): + def test_event_report(self) -> None: event = self.create_simple_event() - new_event_report = MISPEventReport() + new_event_report: MISPEventReport = MISPEventReport() new_event_report.name = "Test Event Report" new_event_report.content = "# Example report markdown" new_event_report.distribution = 5 # Inherit try: event = self.user_misp_connector.add_event(event) - new_event_report = self.user_misp_connector.add_event_report(event.id, new_event_report) + new_event_report = self.user_misp_connector.add_event_report(event.id, new_event_report) # type: ignore[assignment] # The event report should be linked by Event ID self.assertEqual(event.id, new_event_report.event_id) @@ -2979,12 +3068,12 @@ class TestComprehensive(unittest.TestCase): new_event_report.name = "Updated Event Report" new_event_report.content = "Updated content" - new_event_report = self.user_misp_connector.update_event_report(new_event_report) + new_event_report = self.user_misp_connector.update_event_report(new_event_report) # type: ignore[assignment] # The event report should be updatable self.assertTrue(new_event_report.name == "Updated Event Report") self.assertTrue(new_event_report.content == "Updated content") - event_reports = self.user_misp_connector.get_event_reports(event.id) + event_reports: list[MISPEventReport] = self.user_misp_connector.get_event_reports(event.id) # type: ignore[assignment] # The event report should be requestable by the Event ID self.assertEqual(new_event_report.id, event_reports[0].id) @@ -2999,26 +3088,38 @@ class TestComprehensive(unittest.TestCase): self.user_misp_connector.delete_event(event) self.user_misp_connector.delete_event_report(new_event_report) - def test_galaxy_cluster(self): - self.admin_misp_connector.toggle_global_pythonify() - galaxy = self.admin_misp_connector.galaxies()[0] - new_galaxy_cluster = MISPGalaxyCluster() + def test_search_galaxy(self) -> None: + galaxies: list[MISPGalaxy] = self.admin_misp_connector.galaxies(pythonify=True) # type: ignore[assignment] + galaxy: MISPGalaxy = galaxies[0] + ret = self.admin_misp_connector.search_galaxy(value=galaxy.name, pythonify=True) + self.assertEqual(len(ret), 1) + + def test_galaxy_cluster(self) -> None: + galaxies: list[MISPGalaxy] = self.admin_misp_connector.galaxies(pythonify=True) # type: ignore[assignment] + galaxy: MISPGalaxy = galaxies[0] + new_galaxy_cluster: MISPGalaxyCluster = MISPGalaxyCluster() new_galaxy_cluster.value = "Test Cluster" new_galaxy_cluster.authors = ["MISP"] new_galaxy_cluster.distribution = 1 new_galaxy_cluster.description = "Example test cluster" try: - galaxy = self.admin_misp_connector.get_galaxy(galaxy.id, withCluster=True) + if gid := galaxy.id: + galaxy = self.admin_misp_connector.get_galaxy(gid, withCluster=True, pythonify=True) # type: ignore[assignment] + else: + raise Exception("No galaxy found") existing_galaxy_cluster = galaxy.clusters[0] - new_galaxy_cluster = self.admin_misp_connector.add_galaxy_cluster(galaxy.id, new_galaxy_cluster) + if gid := galaxy.id: + new_galaxy_cluster = self.admin_misp_connector.add_galaxy_cluster(gid, new_galaxy_cluster, pythonify=True) # type: ignore[assignment] + else: + raise Exception("No galaxy found") # The new galaxy cluster should be under the selected galaxy self.assertEqual(galaxy.id, new_galaxy_cluster.galaxy_id) # The cluster should have the right value self.assertEqual(new_galaxy_cluster.value, "Test Cluster") new_galaxy_cluster.add_cluster_element("synonyms", "Test2") - new_galaxy_cluster = self.admin_misp_connector.update_galaxy_cluster(new_galaxy_cluster) + new_galaxy_cluster = self.admin_misp_connector.update_galaxy_cluster(new_galaxy_cluster, pythonify=True) # type: ignore[assignment] # The cluster should have one element that is a synonym self.assertEqual(len(new_galaxy_cluster.cluster_elements), 1) @@ -3031,22 +3132,22 @@ class TestComprehensive(unittest.TestCase): # The cluster element should be updatable element.value = "Test3" - new_galaxy_cluster = self.admin_misp_connector.update_galaxy_cluster(new_galaxy_cluster) + new_galaxy_cluster = self.admin_misp_connector.update_galaxy_cluster(new_galaxy_cluster, pythonify=True) # type: ignore[assignment] element = new_galaxy_cluster.cluster_elements[0] self.assertEqual(element.value, "Test3") new_galaxy_cluster.add_cluster_element("synonyms", "ToDelete") - new_galaxy_cluster = self.admin_misp_connector.update_galaxy_cluster(new_galaxy_cluster) + new_galaxy_cluster = self.admin_misp_connector.update_galaxy_cluster(new_galaxy_cluster, pythonify=True) # type: ignore[assignment] # The cluster should have two elements self.assertEqual(len(new_galaxy_cluster.cluster_elements), 2) new_galaxy_cluster.cluster_elements = [e for e in new_galaxy_cluster.cluster_elements if e.value != "ToDelete"] - new_galaxy_cluster = self.admin_misp_connector.update_galaxy_cluster(new_galaxy_cluster) + new_galaxy_cluster = self.admin_misp_connector.update_galaxy_cluster(new_galaxy_cluster, pythonify=True) # type: ignore[assignment] # The cluster elements should be deletable self.assertEqual(len(new_galaxy_cluster.cluster_elements), 1) new_galaxy_cluster.add_cluster_relation(existing_galaxy_cluster, "is-tested-by") - new_galaxy_cluster = self.admin_misp_connector.update_galaxy_cluster(new_galaxy_cluster) + new_galaxy_cluster = self.admin_misp_connector.update_galaxy_cluster(new_galaxy_cluster, pythonify=True) # type: ignore[assignment] # The cluster should have a relationship self.assertEqual(len(new_galaxy_cluster.cluster_relations), 1) relation = new_galaxy_cluster.cluster_relations[0] @@ -3054,7 +3155,7 @@ class TestComprehensive(unittest.TestCase): self.assertEqual(relation.referenced_galaxy_cluster_uuid, existing_galaxy_cluster.uuid) relation.add_tag("tlp:amber") - new_galaxy_cluster = self.admin_misp_connector.update_galaxy_cluster(new_galaxy_cluster) + new_galaxy_cluster = self.admin_misp_connector.update_galaxy_cluster(new_galaxy_cluster, pythonify=True) # type: ignore[assignment] relation = new_galaxy_cluster.cluster_relations[0] # The relationship should have a tag of tlp:amber self.assertEqual(len(relation.tags), 1) @@ -3064,34 +3165,35 @@ class TestComprehensive(unittest.TestCase): resp = self.admin_misp_connector.delete_galaxy_cluster_relation(relation) self.assertTrue(resp['success']) # The cluster relation should no longer be present - new_galaxy_cluster = self.admin_misp_connector.get_galaxy_cluster(new_galaxy_cluster) + new_galaxy_cluster = self.admin_misp_connector.get_galaxy_cluster(new_galaxy_cluster, pythonify=True) # type: ignore[assignment] self.assertEqual(len(new_galaxy_cluster.cluster_relations), 0) resp = self.admin_misp_connector.delete_galaxy_cluster(new_galaxy_cluster) # Galaxy clusters should be soft deletable self.assertTrue(resp['success']) - new_galaxy_cluster = self.admin_misp_connector.get_galaxy_cluster(new_galaxy_cluster) + new_galaxy_cluster = self.admin_misp_connector.get_galaxy_cluster(new_galaxy_cluster, pythonify=True) # type: ignore[assignment] self.assertTrue(isinstance(new_galaxy_cluster, MISPGalaxyCluster)) resp = self.admin_misp_connector.delete_galaxy_cluster(new_galaxy_cluster, hard=True) # Galaxy clusters should be hard deletable self.assertTrue(resp['success']) - resp = self.admin_misp_connector.get_galaxy_cluster(new_galaxy_cluster) + resp = self.admin_misp_connector.get_galaxy_cluster(new_galaxy_cluster) # type: ignore[assignment] self.assertTrue("errors" in resp) finally: - self.admin_misp_connector.delete_galaxy_cluster_relation(relation) - self.admin_misp_connector.delete_galaxy_cluster(new_galaxy_cluster, hard=True) - self.admin_misp_connector.toggle_global_pythonify() + pass - def test_event_galaxy(self): - self.admin_misp_connector.toggle_global_pythonify() + def test_event_galaxy(self) -> None: event = self.create_simple_event() try: - galaxy = self.admin_misp_connector.galaxies()[0] - galaxy = self.admin_misp_connector.get_galaxy(galaxy.id, withCluster=True) - galaxy_cluster = galaxy.clusters[0] + galaxies: list[MISPGalaxy] = self.admin_misp_connector.galaxies(pythonify=True) # type: ignore[assignment] + galaxy: MISPGalaxy = galaxies[0] + if gid := galaxy.id: + galaxy = self.admin_misp_connector.get_galaxy(gid, withCluster=True, pythonify=True) # type: ignore[assignment] + else: + raise Exception("No galaxy found") + galaxy_cluster: MISPGalaxyCluster = galaxy.clusters[0] event.add_tag(galaxy_cluster.tag_name) - event = self.admin_misp_connector.add_event(event) + event = self.admin_misp_connector.add_event(event, pythonify=True) # The event should have a galaxy attached self.assertEqual(len(event.galaxies), 1) event_galaxy = event.galaxies[0] @@ -3101,10 +3203,182 @@ class TestComprehensive(unittest.TestCase): self.assertEqual(event_galaxy.clusters[0].id, galaxy_cluster.id) finally: self.admin_misp_connector.delete_event(event) - self.admin_misp_connector.toggle_global_pythonify() + + def test_attach_galaxy_cluster(self) -> None: + event = self.create_simple_event() + event = self.admin_misp_connector.add_event(event, pythonify=True) + try: + galaxies: list[MISPGalaxy] = self.admin_misp_connector.galaxies(pythonify=True) + galaxy: MISPGalaxy = galaxies[0] + if gid := galaxy.id: + galaxy = self.admin_misp_connector.get_galaxy(gid, withCluster=True, pythonify=True) + else: + raise Exception("No galaxy found") + galaxy_cluster: MISPGalaxyCluster = galaxy.clusters[0] + response = self.admin_misp_connector.attach_galaxy_cluster(event, galaxy_cluster) + self.assertTrue(response['saved']) + event = self.admin_misp_connector.get_event(event.id, pythonify=True) + + self.assertEqual(len(event.galaxies), 1) + event_galaxy = event.galaxies[0] + # The galaxy ID should equal the galaxy from which the cluster came from + self.assertEqual(event_galaxy.id, galaxy.id) + # The galaxy cluster should equal the cluster added + self.assertEqual(event_galaxy.clusters[0].id, galaxy_cluster.id) + + galaxy_cluster: MISPGalaxyCluster = galaxy.clusters[1] + + # Test on attribute + attribute = event.attributes[0] + response = self.admin_misp_connector.attach_galaxy_cluster(attribute, galaxy_cluster) + self.assertTrue(response['saved']) + event = self.admin_misp_connector.get_event(event.id, pythonify=True) + attribute = event.attributes[0] + self.assertEqual(len(attribute.galaxies), 1) + attribute_galaxy = attribute.galaxies[0] + # The galaxy ID should equal the galaxy from which the cluster came from + self.assertEqual(attribute_galaxy.id, galaxy.id) + # The galaxy cluster should equal the cluster added + self.assertEqual(attribute_galaxy.clusters[0].id, galaxy_cluster.id) + finally: + self.admin_misp_connector.delete_event(event) + + def test_analyst_data_CRUD(self) -> None: + event = self.create_simple_event() + try: + fake_uuid = str(uuid4()) + new_note1 = MISPNote() + new_note1.object_type = 'Event' + new_note1.object_uuid = fake_uuid + new_note1.note = 'Fake note' + new_note1 = self.user_misp_connector.add_note(new_note1) + # The Note should be linked even for non-existing data + self.assertTrue(new_note1.object_uuid == fake_uuid) + + new_note1.note = "Updated Note" + new_note1 = self.user_misp_connector.update_note(new_note1) + # The Note should be updatable + self.assertTrue(new_note1.note == "Updated Note") + + # The Note should be able to get an Opinion + new_opinion = new_note1.add_opinion(42, 'Test Opinion') + new_note1 = self.user_misp_connector.update_note(new_note1) + # Fetch newly added node + new_note1 = self.user_misp_connector.get_note(new_note1) + # The Opinion shoud be able to be created via the Note + self.assertTrue(new_note1.opinions[0].opinion == new_opinion.opinion) + + response = self.user_misp_connector.delete_note(new_note1) + # The Note should be deletable + self.assertTrue(response['success']) + self.assertEqual(response['message'], 'Note deleted.') + # The Opinion should not be deleted + opinion_resp = self.user_misp_connector.get_opinion(new_opinion) + self.assertTrue(opinion_resp.opinion == new_opinion.opinion) + + new_note: MISPNote = event.add_note(note='Test Note', language='en') + new_note.distribution = 1 # Community + event = self.user_misp_connector.add_event(event) + # The note should be linked by Event UUID + self.assertEqual(new_note.object_type, 'Event') + self.assertTrue(new_note.object_uuid == event.uuid) + + event = self.user_misp_connector.get_event(event) + # The Note should be present on the event + self.assertTrue(event.notes[0].object_uuid == event.uuid) + + finally: + self.admin_misp_connector.delete_event(event) + try: + self.admin_misp_connector.delete_opinion(new_opinion) + self.admin_misp_connector.delete_note(new_note) + self.admin_misp_connector.delete_note(new_note1) # Should already be deleted + except Exception: + pass + + def test_analyst_data_ACL(self) -> None: + event = self.create_simple_event() + event.distribution = 2 + sg = MISPSharingGroup() + sg.name = 'Testcases SG' + sg.releasability = 'Testing' + sharing_group = self.admin_misp_connector.add_sharing_group(sg, pythonify=True) + # Chec that sharing group was created + self.assertEqual(sharing_group.name, 'Testcases SG') + + try: + new_note: MISPNote = event.add_note(note='Test Note', language='en') + new_note.distribution = 0 # Org only + event = self.admin_misp_connector.add_event(event, pythonify=True) + + # The note should be linked by Event UUID + self.assertEqual(new_note.object_type, 'Event') + self.assertEqual(event.uuid, new_note.object_uuid) + + event = self.admin_misp_connector.get_event(event, pythonify=True) + # The note should be visible for the creator + self.assertEqual(len(event.notes), 1) + self.assertTrue(new_note.note == "Test Note") + + resp = self.user_misp_connector.get_note(new_note) + # The note should not be visible to another org + self.assertTrue(len(resp), 0) + + event = self.user_misp_connector.get_event(event) + # The Note attached to the event should not be visible for another org than the creator + self.assertEqual(len(event.Note), 0) + + new_note = self.admin_misp_connector.get_note(new_note, pythonify=True) + new_note.distribution = 4 + new_note.sharing_group_id = sharing_group.id + new_note = self.admin_misp_connector.update_note(new_note, pythonify=True) + self.assertEqual(int(new_note.sharing_group_id), int(sharing_group.id)) + + event = self.user_misp_connector.get_event(event) + # The Note attached to the event should not be visible for another org not part of the sharing group + self.assertEqual(len(event.Note), 0) + + # Add org to the sharing group + r = self.admin_misp_connector.add_org_to_sharing_group(sharing_group, + self.test_org, extend=True) + self.assertEqual(r['name'], 'Organisation added to the sharing group.') + + event = self.user_misp_connector.get_event(event) + # The Note attached to the event should now be visible + self.assertEqual(len(event.Note), 1) + + new_note.note = "Updated Note" + resp = self.user_misp_connector.update_note(new_note) + # The Note should not be updatable by another organisation + self.assertTrue(resp['errors']) + + resp = self.user_misp_connector.delete_note(new_note) + # The Note should not be deletable by another organisation + self.assertTrue(resp['errors']) + + organisation = MISPOrganisation() + organisation.name = 'Fake Org' + fake_org = self.admin_misp_connector.add_organisation(organisation, pythonify=True) + new_note_2 = new_note.add_note('Test Note 2') + new_note_2.orgc_uuid = fake_org.uuid + new_note_2 = self.user_misp_connector.add_note(new_note_2) + # Regular user should not be able to create a note on behalf of another organisation + self.assertFalse(new_note_2.orgc_uuid == fake_org.uuid) + # Note should have the orgc set to the use's organisation for non-privileged users + self.assertTrue(new_note_2.orgc_uuid == self.test_org.uuid) + + finally: + self.admin_misp_connector.delete_event(event) + try: + pass + self.admin_misp_connector.delete_sharing_group(sharing_group.id) + self.admin_misp_connector.delete_organisation(fake_org) + self.admin_misp_connector.delete_note(new_note) + except Exception: + pass @unittest.skip("Internal use only") - def missing_methods(self): + def missing_methods(self) -> None: skip = [ "attributes/download", "attributes/add_attachment", diff --git a/tests/testlive_sync.py b/tests/testlive_sync.py index 24971c4..8d0e820 100644 --- a/tests/testlive_sync.py +++ b/tests/testlive_sync.py @@ -1,23 +1,19 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- + +from __future__ import annotations import time -import sys import unittest import subprocess -import urllib3 # type: ignore +import urllib3 import logging logging.disable(logging.CRITICAL) try: - from pymisp import ExpandedPyMISP, MISPOrganisation, MISPUser, MISPEvent, MISPObject, MISPSharingGroup, Distribution + from pymisp import PyMISP, MISPOrganisation, MISPUser, MISPEvent, MISPObject, MISPSharingGroup, Distribution except ImportError: - if sys.version_info < (3, 6): - print('This test suite requires Python 3.6+, breaking.') - sys.exit(0) - else: - raise + raise key = 'eYQdGTEWZJ8C2lm9EpnMqxQGwGiPNyoR75JvLdlE' verifycert = False @@ -73,7 +69,7 @@ fast_mode = True class MISPInstance(): def __init__(self, params): - self.initial_user_connector = ExpandedPyMISP(params['url'], params['key'], ssl=False, debug=False) + self.initial_user_connector = PyMISP(params['url'], params['key'], ssl=False, debug=False) # Git pull self.initial_user_connector.update_misp() # Set the default role (id 3 on the VM is normal user) @@ -101,7 +97,7 @@ class MISPInstance(): user.org_id = self.test_org.id user.role_id = 1 # Site admin self.test_site_admin = self.initial_user_connector.add_user(user) - self.site_admin_connector = ExpandedPyMISP(params['url'], self.test_site_admin.authkey, ssl=False, debug=False) + self.site_admin_connector = PyMISP(params['url'], self.test_site_admin.authkey, ssl=False, debug=False) self.site_admin_connector.toggle_global_pythonify() # Create org admin user = MISPUser() @@ -109,14 +105,14 @@ class MISPInstance(): user.org_id = self.test_org.id user.role_id = 2 # Org admin self.test_org_admin = self.site_admin_connector.add_user(user) - self.org_admin_connector = ExpandedPyMISP(params['url'], self.test_org_admin.authkey, ssl=False, debug=False) + self.org_admin_connector = PyMISP(params['url'], self.test_org_admin.authkey, ssl=False, debug=False) self.org_admin_connector.toggle_global_pythonify() # Create user user = MISPUser() user.email = params['email_user'] user.org_id = self.test_org.id self.test_usr = self.org_admin_connector.add_user(user) - self.user_connector = ExpandedPyMISP(params['url'], self.test_usr.authkey, ssl=False, debug=False) + self.user_connector = PyMISP(params['url'], self.test_usr.authkey, ssl=False, debug=False) self.user_connector.toggle_global_pythonify() # Setup external_baseurl @@ -141,7 +137,7 @@ class MISPInstance(): user.org_id = sync_org.id user.role_id = 5 # Org admin sync_user = self.site_admin_connector.add_user(user) - sync_user_connector = ExpandedPyMISP(self.site_admin_connector.root_url, sync_user.authkey, ssl=False, debug=False) + sync_user_connector = PyMISP(self.site_admin_connector.root_url, sync_user.authkey, ssl=False, debug=False) sync_server_config = sync_user_connector.get_sync_config(pythonify=True) self.sync.append((sync_org, sync_user, sync_server_config))