howler-api 3.4.0.dev912__tar.gz → 3.4.0.dev929__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/PKG-INFO +1 -1
- howler_api-3.4.0.dev929/howler/cronjobs/retention.py +129 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/datastore/collection.py +107 -4
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/datastore/types.py +4 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/pyproject.toml +1 -1
- howler_api-3.4.0.dev912/howler/cronjobs/retention.py +0 -61
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/README.md +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/actions/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/actions/add_label.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/actions/add_to_bundle.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/actions/change_field.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/actions/demote.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/actions/example_plugin.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/actions/prioritization.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/actions/promote.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/actions/remove_from_bundle.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/actions/remove_label.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/actions/transition.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/api/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/api/base.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/api/socket.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/api/v1/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/api/v1/action.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/api/v1/analytic.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/api/v1/auth.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/api/v1/clue.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/api/v1/configs.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/api/v1/dossier.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/api/v1/help.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/api/v1/hit.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/api/v1/notebook.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/api/v1/overview.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/api/v1/search.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/api/v1/template.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/api/v1/tool.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/api/v1/user.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/api/v1/utils/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/api/v1/utils/etag.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/api/v1/view.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/app.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/common/README.md +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/common/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/common/classification.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/common/classification.yml +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/common/exceptions.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/common/loader.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/common/logging/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/common/logging/audit.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/common/logging/format.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/common/net.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/common/net_static.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/common/random_user.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/common/swagger.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/config.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/cronjobs/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/cronjobs/rules.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/cronjobs/view_cleanup.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/datastore/README.md +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/datastore/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/datastore/bulk.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/datastore/constants.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/datastore/exceptions.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/datastore/howler_store.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/datastore/migrations/fix_process.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/datastore/operations.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/datastore/schemas.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/datastore/store.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/datastore/support/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/datastore/support/build.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/datastore/support/schemas.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/error.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/external/README.md +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/external/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/external/generate_mitre.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/external/generate_sigma_rules.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/external/generate_tlds.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/external/reindex_data.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/external/wipe_databases.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/gunicorn_config.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/healthz.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/helper/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/helper/azure.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/helper/discover.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/helper/hit.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/helper/oauth.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/helper/search.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/helper/workflow.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/helper/ws.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/README.md +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/base.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/charter.txt +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/helper.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/howler_enum.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/action.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/analytic.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/assemblyline.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/aws.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/azure.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/cbs.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/clue.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/config.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/dossier.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/agent.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/autonomous_system.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/client.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/cloud.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/code_signature.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/container.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/dns.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/egress.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/elf.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/email.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/error.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/event.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/faas.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/file.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/geo.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/group.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/hash.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/host.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/http.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/ingress.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/interface.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/network.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/observer.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/organization.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/os.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/pe.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/process.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/registry.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/related.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/rule.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/server.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/threat.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/tls.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/url.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/user.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/user_agent.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/ecs/vulnerability.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/gcp.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/hit.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/howler_data.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/lead.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/localized_label.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/overview.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/pivot.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/template.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/user.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/models/view.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/random_data.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/odm/randomizer.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/patched.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/plugins/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/plugins/config.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/remote/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/remote/datatypes/README.md +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/remote/datatypes/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/remote/datatypes/counters.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/remote/datatypes/events.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/remote/datatypes/hash.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/remote/datatypes/lock.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/remote/datatypes/queues/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/remote/datatypes/queues/comms.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/remote/datatypes/queues/multi.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/remote/datatypes/queues/named.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/remote/datatypes/queues/priority.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/remote/datatypes/set.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/remote/datatypes/user_quota_tracker.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/security/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/security/socket.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/security/utils.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/services/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/services/action_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/services/analytic_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/services/auth_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/services/config_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/services/dossier_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/services/event_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/services/hit_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/services/jwt_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/services/lucene_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/services/notebook_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/services/overview_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/services/template_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/services/user_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/telemetry.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/utils/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/utils/annotations.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/utils/chunk.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/utils/compat.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/utils/constants.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/utils/dict_utils.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/utils/isotime.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/utils/list_utils.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/utils/lucene.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/utils/path.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/utils/socket_utils.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/utils/str_utils.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/utils/uid.py +0 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from apscheduler.schedulers.base import BaseScheduler
|
|
6
|
+
from apscheduler.triggers.cron import CronTrigger
|
|
7
|
+
from pytz import timezone
|
|
8
|
+
|
|
9
|
+
from howler.common.logging import get_logger
|
|
10
|
+
from howler.config import DEBUG, config
|
|
11
|
+
from howler.datastore.howler_store import HowlerDatastore
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__file__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def execute():
|
|
17
|
+
"""Delete any hits older than the configured time"""
|
|
18
|
+
from howler.common.loader import datastore
|
|
19
|
+
|
|
20
|
+
delta_kwargs = {str(config.system.retention.limit_unit): config.system.retention.limit_amount}
|
|
21
|
+
|
|
22
|
+
cutoff = (datetime.now() - timedelta(**delta_kwargs)).strftime("%Y-%m-%d")
|
|
23
|
+
|
|
24
|
+
logger.debug("Removing hits older than %s", cutoff)
|
|
25
|
+
|
|
26
|
+
ds = datastore()
|
|
27
|
+
|
|
28
|
+
ds.hit.delete_by_query(f"event.created:{{* TO {cutoff}}} OR howler.expiry:{{* TO now}}")
|
|
29
|
+
|
|
30
|
+
ds.hit.commit()
|
|
31
|
+
|
|
32
|
+
logger.debug("Deletion complete")
|
|
33
|
+
|
|
34
|
+
logger.debug("Cleaning analytics with no matching hits")
|
|
35
|
+
_remove_analytics_without_hits(ds)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _remove_analytics_without_hits(ds: HowlerDatastore):
|
|
39
|
+
|
|
40
|
+
matched_analytics = _find_analytics_with_hits(ds)
|
|
41
|
+
|
|
42
|
+
if matched_analytics is not None:
|
|
43
|
+
ds.analytic.delete_by_search_object(
|
|
44
|
+
{
|
|
45
|
+
"bool": {
|
|
46
|
+
"filter": [
|
|
47
|
+
{
|
|
48
|
+
"bool": {
|
|
49
|
+
"must_not": [
|
|
50
|
+
{"exists": {"field": "rule"}},
|
|
51
|
+
{"exists": {"field": "rule_type"}},
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"must_not": [{"terms": {"name": matched_analytics}}],
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
ds.analytic.commit()
|
|
61
|
+
else:
|
|
62
|
+
logger.warning(
|
|
63
|
+
"Aggregation search for matched analytics did not run or returned no results. "
|
|
64
|
+
"There is likely an issue with the query. Skipping cleanup."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _find_analytics_with_hits(ds: HowlerDatastore) -> list[str] | None:
|
|
69
|
+
|
|
70
|
+
total_analytics = ds.analytic.count("id:*", filters=None)["count"]
|
|
71
|
+
|
|
72
|
+
if total_analytics:
|
|
73
|
+
matched_analytics = ds.hit.search(
|
|
74
|
+
"howler.id:*",
|
|
75
|
+
aggregations=[
|
|
76
|
+
(
|
|
77
|
+
"matched_analytics",
|
|
78
|
+
{
|
|
79
|
+
"terms": {
|
|
80
|
+
"field": "howler.analytic",
|
|
81
|
+
"size": total_analytics,
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
],
|
|
86
|
+
rows=0,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if "matched_analytics" in matched_analytics["aggregations"]:
|
|
90
|
+
matched_analytic_names = [
|
|
91
|
+
bucket["key"] for bucket in matched_analytics["aggregations"]["matched_analytics"]["buckets"]
|
|
92
|
+
]
|
|
93
|
+
else:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
else:
|
|
97
|
+
matched_analytic_names = []
|
|
98
|
+
|
|
99
|
+
return matched_analytic_names
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def setup_job(sched: BaseScheduler):
|
|
103
|
+
"""Initialize the retention job"""
|
|
104
|
+
if not config.system.retention.enabled:
|
|
105
|
+
if not DEBUG or config.system.type == "production":
|
|
106
|
+
logger.warning("Retention cronjob disabled! This is not recommended for a production settings.")
|
|
107
|
+
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
logger.debug("Initializing retention cronjob with cron %s", config.system.retention.crontab)
|
|
111
|
+
|
|
112
|
+
if DEBUG:
|
|
113
|
+
_kwargs: dict[str, Any] = {"next_run_time": datetime.now()}
|
|
114
|
+
else:
|
|
115
|
+
_kwargs = {}
|
|
116
|
+
|
|
117
|
+
if sched.get_job("retention"):
|
|
118
|
+
logger.debug("Retention job already running!")
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
sched.add_job(
|
|
122
|
+
id="retention",
|
|
123
|
+
func=execute,
|
|
124
|
+
trigger=CronTrigger.from_crontab(
|
|
125
|
+
config.system.retention.crontab, timezone=timezone(os.getenv("SCHEDULER_TZ", "America/Toronto"))
|
|
126
|
+
),
|
|
127
|
+
**_kwargs,
|
|
128
|
+
)
|
|
129
|
+
logger.debug("Initialization complete")
|
|
@@ -38,7 +38,7 @@ from howler.datastore.support.schemas import (
|
|
|
38
38
|
default_index,
|
|
39
39
|
default_mapping,
|
|
40
40
|
)
|
|
41
|
-
from howler.datastore.types import SearchResult
|
|
41
|
+
from howler.datastore.types import AggSearchResult, SearchResult
|
|
42
42
|
from howler.odm.base import (
|
|
43
43
|
BANNED_FIELDS,
|
|
44
44
|
IP,
|
|
@@ -211,9 +211,11 @@ class ESCollection(Generic[ModelType]):
|
|
|
211
211
|
"sort": DEFAULT_SORT,
|
|
212
212
|
"df": None,
|
|
213
213
|
"script_fields": [],
|
|
214
|
+
"aggregations": None,
|
|
214
215
|
}
|
|
215
216
|
IGNORE_ENSURE_COLLECTION: bool = False
|
|
216
217
|
ENSURE_COLLECTION_WARNED: bool = False
|
|
218
|
+
CUSTOM_AGG_PREFIX: str = "_custom_agg__"
|
|
217
219
|
|
|
218
220
|
def __init__(self, datastore: ESStore, name, model_class=None, validate=True, max_attempts=10):
|
|
219
221
|
self.replicas = int(
|
|
@@ -1119,7 +1121,7 @@ class ESCollection(Generic[ModelType]):
|
|
|
1119
1121
|
except elasticsearch.NotFoundError:
|
|
1120
1122
|
return False
|
|
1121
1123
|
|
|
1122
|
-
def delete_by_query(self, query
|
|
1124
|
+
def delete_by_query(self, query: str, sort=None, max_docs=None):
|
|
1123
1125
|
"""This function should delete the underlying documents referenced by the query.
|
|
1124
1126
|
It should return true if the documents were in fact properly deleted.
|
|
1125
1127
|
|
|
@@ -1127,7 +1129,18 @@ class ESCollection(Generic[ModelType]):
|
|
|
1127
1129
|
:param workers: Number of workers used for deletion if basic currency delete is used
|
|
1128
1130
|
:return: True is delete successful
|
|
1129
1131
|
"""
|
|
1130
|
-
|
|
1132
|
+
query_obj = {"bool": {"must": {"query_string": {"query": query}}}}
|
|
1133
|
+
success = self.delete_by_search_object(query=query_obj, sort=sort, max_docs=max_docs)
|
|
1134
|
+
return success
|
|
1135
|
+
|
|
1136
|
+
def delete_by_search_object(self, query: dict, sort=None, max_docs=None):
|
|
1137
|
+
"""Delete the underlying documents matching the query object.
|
|
1138
|
+
Returns true if the documents were in fact properly deleted.
|
|
1139
|
+
|
|
1140
|
+
:param query: Query object following elasticsearch request structure
|
|
1141
|
+
:param workers: Number of workers used for deletion if basic currency delete is used
|
|
1142
|
+
:return: True is delete successful
|
|
1143
|
+
"""
|
|
1131
1144
|
info = self._delete_async(self.name, query=query, sort=sort_str(parse_sort(sort)), max_docs=max_docs)
|
|
1132
1145
|
return info.get("deleted", 0) != 0
|
|
1133
1146
|
|
|
@@ -1503,6 +1516,26 @@ class ESCollection(Generic[ModelType]):
|
|
|
1503
1516
|
},
|
|
1504
1517
|
}
|
|
1505
1518
|
|
|
1519
|
+
# Add any arbitrary aggregations
|
|
1520
|
+
if parsed_values["aggregations"]:
|
|
1521
|
+
query_body.setdefault("aggregations", {})
|
|
1522
|
+
cluster_settings = self.datastore.client.cluster.get_settings(include_defaults=True, flat_settings=True)
|
|
1523
|
+
flattened_settings = {
|
|
1524
|
+
**cluster_settings["defaults"],
|
|
1525
|
+
**cluster_settings["transient"],
|
|
1526
|
+
**cluster_settings["persistent"],
|
|
1527
|
+
}
|
|
1528
|
+
max_buckets = int(flattened_settings["search.max_buckets"])
|
|
1529
|
+
for agg_name, agg_args in parsed_values["aggregations"]:
|
|
1530
|
+
if any("size" in agg_def and agg_def["size"] > max_buckets for agg_def in agg_args.values()):
|
|
1531
|
+
# verify the size of the agg query doesn't exceed the max
|
|
1532
|
+
warnings.warn(
|
|
1533
|
+
f"Aggregation {agg_name} has a size argument higher than the maximum allowed "
|
|
1534
|
+
f"buckets of the cluster ({max_buckets}). Skipping aggregation."
|
|
1535
|
+
)
|
|
1536
|
+
continue
|
|
1537
|
+
query_body["aggregations"][f"{self.CUSTOM_AGG_PREFIX}{agg_name}"] = agg_args
|
|
1538
|
+
|
|
1506
1539
|
try:
|
|
1507
1540
|
if deep_paging_id is not None and not deep_paging_id == "*":
|
|
1508
1541
|
# Get the next page
|
|
@@ -1554,6 +1587,8 @@ class ESCollection(Generic[ModelType]):
|
|
|
1554
1587
|
use_archive: bool = False,
|
|
1555
1588
|
track_total_hits: bool = False,
|
|
1556
1589
|
script_fields: list[str] = [],
|
|
1590
|
+
*,
|
|
1591
|
+
aggregations: None = None,
|
|
1557
1592
|
) -> SearchResult[ModelType]: ...
|
|
1558
1593
|
|
|
1559
1594
|
@overload
|
|
@@ -1572,8 +1607,50 @@ class ESCollection(Generic[ModelType]):
|
|
|
1572
1607
|
use_archive: bool = False,
|
|
1573
1608
|
track_total_hits: bool = False,
|
|
1574
1609
|
script_fields: list[str] = [],
|
|
1610
|
+
*,
|
|
1611
|
+
aggregations: None = None,
|
|
1575
1612
|
) -> SearchResult[dict[str, typing.Any]]: ...
|
|
1576
1613
|
|
|
1614
|
+
@overload
|
|
1615
|
+
def search(
|
|
1616
|
+
self,
|
|
1617
|
+
query: str | None,
|
|
1618
|
+
as_obj: Literal[True] = True,
|
|
1619
|
+
offset: int = 0,
|
|
1620
|
+
rows: int | None = None,
|
|
1621
|
+
sort: typing.Any = None,
|
|
1622
|
+
fl: str | None = None,
|
|
1623
|
+
timeout: int | None = None,
|
|
1624
|
+
filters: list[str] | str | None = None,
|
|
1625
|
+
access_control: typing.Any = None,
|
|
1626
|
+
deep_paging_id: str | None = None,
|
|
1627
|
+
use_archive: bool = False,
|
|
1628
|
+
track_total_hits: bool = False,
|
|
1629
|
+
script_fields: list[str] = [],
|
|
1630
|
+
*,
|
|
1631
|
+
aggregations: list[tuple[str, dict]],
|
|
1632
|
+
) -> AggSearchResult[ModelType]: ...
|
|
1633
|
+
|
|
1634
|
+
@overload
|
|
1635
|
+
def search(
|
|
1636
|
+
self,
|
|
1637
|
+
query: str | None,
|
|
1638
|
+
as_obj: Literal[False],
|
|
1639
|
+
offset: int = 0,
|
|
1640
|
+
rows: int | None = None,
|
|
1641
|
+
sort: typing.Any = None,
|
|
1642
|
+
fl: str | None = None,
|
|
1643
|
+
timeout: int | None = None,
|
|
1644
|
+
filters: list[str] | str | None = None,
|
|
1645
|
+
access_control: typing.Any = None,
|
|
1646
|
+
deep_paging_id: str | None = None,
|
|
1647
|
+
use_archive: bool = False,
|
|
1648
|
+
track_total_hits: bool = False,
|
|
1649
|
+
script_fields: list[str] = [],
|
|
1650
|
+
*,
|
|
1651
|
+
aggregations: list[tuple[str, dict]],
|
|
1652
|
+
) -> AggSearchResult[dict[str, typing.Any]]: ...
|
|
1653
|
+
|
|
1577
1654
|
def search(
|
|
1578
1655
|
self,
|
|
1579
1656
|
query,
|
|
@@ -1589,6 +1666,8 @@ class ESCollection(Generic[ModelType]):
|
|
|
1589
1666
|
use_archive=False,
|
|
1590
1667
|
track_total_hits=None,
|
|
1591
1668
|
script_fields=[],
|
|
1669
|
+
*,
|
|
1670
|
+
aggregations=None,
|
|
1592
1671
|
):
|
|
1593
1672
|
"""This function should perform a search through the datastore and return a
|
|
1594
1673
|
search result object that consist on the following::
|
|
@@ -1605,6 +1684,14 @@ class ESCollection(Generic[ModelType]):
|
|
|
1605
1684
|
}, ...]
|
|
1606
1685
|
}
|
|
1607
1686
|
|
|
1687
|
+
If aggregations are provided the search result will include an additional field::
|
|
1688
|
+
|
|
1689
|
+
{
|
|
1690
|
+
"aggregations": { # Dictionary where the keys are the keys of the `aggregations` parameter
|
|
1691
|
+
"agg_name": {...} # and the values are the results of the aggregations
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1608
1695
|
:param script_fields: List of name/script tuple of fields to be evaluated at runtime
|
|
1609
1696
|
:param track_total_hits: Return to total matching document count
|
|
1610
1697
|
:param use_archive: Query also the archive
|
|
@@ -1618,6 +1705,8 @@ class ESCollection(Generic[ModelType]):
|
|
|
1618
1705
|
:param timeout: maximum time of execution
|
|
1619
1706
|
:param filters: additional queries to run on the original query to reduce the scope
|
|
1620
1707
|
:param access_control: access control parameters to limiti the scope of the query
|
|
1708
|
+
:param aggregations: optional list of arbitrary aggregations to run alongside the query
|
|
1709
|
+
structured the same way as the es rest query aggs field
|
|
1621
1710
|
:return: a search result object
|
|
1622
1711
|
"""
|
|
1623
1712
|
if offset is None:
|
|
@@ -1660,6 +1749,9 @@ class ESCollection(Generic[ModelType]):
|
|
|
1660
1749
|
if script_fields:
|
|
1661
1750
|
args.append(("script_fields", script_fields))
|
|
1662
1751
|
|
|
1752
|
+
if aggregations:
|
|
1753
|
+
args.append(("aggregations", aggregations))
|
|
1754
|
+
|
|
1663
1755
|
result = self._search(
|
|
1664
1756
|
args,
|
|
1665
1757
|
deep_paging_id=deep_paging_id,
|
|
@@ -1667,13 +1759,24 @@ class ESCollection(Generic[ModelType]):
|
|
|
1667
1759
|
track_total_hits=track_total_hits,
|
|
1668
1760
|
)
|
|
1669
1761
|
|
|
1670
|
-
ret_data: SearchResult
|
|
1762
|
+
ret_data: SearchResult | AggSearchResult
|
|
1763
|
+
ret_data = {
|
|
1671
1764
|
"offset": int(offset),
|
|
1672
1765
|
"rows": int(rows),
|
|
1673
1766
|
"total": int(result["hits"]["total"]["value"]),
|
|
1674
1767
|
"items": [self._format_output(doc, field_list, as_obj=as_obj) for doc in result["hits"]["hits"]],
|
|
1675
1768
|
}
|
|
1676
1769
|
|
|
1770
|
+
if aggregations:
|
|
1771
|
+
ret_data = {
|
|
1772
|
+
**ret_data,
|
|
1773
|
+
"aggregations": {
|
|
1774
|
+
k[len(self.CUSTOM_AGG_PREFIX) :]: v
|
|
1775
|
+
for k, v in result.get("aggregations", {}).items()
|
|
1776
|
+
if k.startswith(self.CUSTOM_AGG_PREFIX)
|
|
1777
|
+
},
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1677
1780
|
new_deep_paging_id = result.get("_scroll_id", None)
|
|
1678
1781
|
|
|
1679
1782
|
# Check if the scroll is finished and close it
|
|
@@ -152,7 +152,7 @@ suppress-none-returning = true
|
|
|
152
152
|
[tool.poetry]
|
|
153
153
|
package-mode = true
|
|
154
154
|
name = "howler-api"
|
|
155
|
-
version = "3.4.0.
|
|
155
|
+
version = "3.4.0.dev929"
|
|
156
156
|
description = "Howler - API server"
|
|
157
157
|
authors = [
|
|
158
158
|
"Canadian Centre for Cyber Security <howler@cyber.gc.ca>",
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from datetime import datetime, timedelta
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
from apscheduler.schedulers.base import BaseScheduler
|
|
6
|
-
from apscheduler.triggers.cron import CronTrigger
|
|
7
|
-
from pytz import timezone
|
|
8
|
-
|
|
9
|
-
from howler.common.logging import get_logger
|
|
10
|
-
from howler.config import DEBUG, config
|
|
11
|
-
|
|
12
|
-
logger = get_logger(__file__)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def execute():
|
|
16
|
-
"""Delete any hits older than the configured time"""
|
|
17
|
-
from howler.common.loader import datastore
|
|
18
|
-
|
|
19
|
-
delta_kwargs = {str(config.system.retention.limit_unit): config.system.retention.limit_amount}
|
|
20
|
-
|
|
21
|
-
cutoff = (datetime.now() - timedelta(**delta_kwargs)).strftime("%Y-%m-%d")
|
|
22
|
-
|
|
23
|
-
logger.debug("Removing hits older than %s", cutoff)
|
|
24
|
-
|
|
25
|
-
ds = datastore()
|
|
26
|
-
|
|
27
|
-
ds.hit.delete_by_query(f"event.created:{{* TO {cutoff}}} OR howler.expiry:{{* TO now}}")
|
|
28
|
-
|
|
29
|
-
ds.hit.commit()
|
|
30
|
-
|
|
31
|
-
logger.debug("Deletion complete")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def setup_job(sched: BaseScheduler):
|
|
35
|
-
"""Initialize the retention job"""
|
|
36
|
-
if not config.system.retention.enabled:
|
|
37
|
-
if not DEBUG or config.system.type == "production":
|
|
38
|
-
logger.warning("Retention cronjob disabled! This is not recommended for a production settings.")
|
|
39
|
-
|
|
40
|
-
return
|
|
41
|
-
|
|
42
|
-
logger.debug("Initializing retention cronjob with cron %s", config.system.retention.crontab)
|
|
43
|
-
|
|
44
|
-
if DEBUG:
|
|
45
|
-
_kwargs: dict[str, Any] = {"next_run_time": datetime.now()}
|
|
46
|
-
else:
|
|
47
|
-
_kwargs = {}
|
|
48
|
-
|
|
49
|
-
if sched.get_job("retention"):
|
|
50
|
-
logger.debug("Retention job already running!")
|
|
51
|
-
return
|
|
52
|
-
|
|
53
|
-
sched.add_job(
|
|
54
|
-
id="retention",
|
|
55
|
-
func=execute,
|
|
56
|
-
trigger=CronTrigger.from_crontab(
|
|
57
|
-
config.system.retention.crontab, timezone=timezone(os.getenv("SCHEDULER_TZ", "America/Toronto"))
|
|
58
|
-
),
|
|
59
|
-
**_kwargs,
|
|
60
|
-
)
|
|
61
|
-
logger.debug("Initialization complete")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{howler_api-3.4.0.dev912 → howler_api-3.4.0.dev929}/howler/datastore/migrations/fix_process.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|