port-ocean 0.28.5__tar.gz → 0.28.7__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.
Potentially problematic release.
This version of port-ocean might be problematic. Click here for more details.
- {port_ocean-0.28.5 → port_ocean-0.28.7}/PKG-INFO +1 -1
- {port_ocean-0.28.5 → port_ocean-0.28.7}/integrations/_infra/Dockerfile.Deb +1 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/integrations/_infra/Dockerfile.local +1 -0
- port_ocean-0.28.7/port_ocean/core/handlers/entity_processor/jq_entity_processor.py +679 -0
- port_ocean-0.28.7/port_ocean/core/handlers/entity_processor/jq_input_evaluator.py +69 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/handlers/port_app_config/models.py +1 -1
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/integrations/mixins/sync_raw.py +1 -1
- port_ocean-0.28.7/port_ocean/core/integrations/mixins/utils.py +348 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py +1 -1
- {port_ocean-0.28.5 → port_ocean-0.28.7}/pyproject.toml +1 -1
- port_ocean-0.28.5/port_ocean/core/handlers/entity_processor/jq_entity_processor.py +0 -357
- port_ocean-0.28.5/port_ocean/core/integrations/mixins/utils.py +0 -136
- {port_ocean-0.28.5 → port_ocean-0.28.7}/LICENSE.md +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/README.md +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/integrations/_infra/Dockerfile.alpine +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/integrations/_infra/Dockerfile.base.builder +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/integrations/_infra/Dockerfile.base.runner +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/integrations/_infra/Dockerfile.dockerignore +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/integrations/_infra/Makefile +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/integrations/_infra/README.md +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/integrations/_infra/entry_local.sh +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/integrations/_infra/grpcio.sh +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/integrations/_infra/init.sh +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/bootstrap.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cache/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cache/base.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cache/disk.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cache/errors.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cache/memory.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/cli.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/commands/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/commands/defaults/__init___.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/commands/defaults/clean.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/commands/defaults/dock.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/commands/defaults/group.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/commands/list_integrations.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/commands/main.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/commands/new.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/commands/pull.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/commands/sail.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/commands/version.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/cookiecutter/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/cookiecutter/cookiecutter.json +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/cookiecutter/extensions.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/cookiecutter/hooks/post_gen_project.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.env.example +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.gitignore +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/.gitignore +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/blueprints.json +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/port-app-config.yml +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/spec.yaml +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CHANGELOG.md +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CONTRIBUTING.md +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/README.md +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/changelog/.gitignore +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/debug.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/main.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/poetry.toml +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/pyproject.toml +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/sonar-project.properties +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/tests/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/tests/test_sample.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/cli/utils.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/clients/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/clients/auth/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/clients/auth/auth_client.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/clients/auth/oauth_client.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/clients/port/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/clients/port/authentication.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/clients/port/client.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/clients/port/mixins/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/clients/port/mixins/blueprints.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/clients/port/mixins/entities.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/clients/port/mixins/integrations.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/clients/port/mixins/migrations.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/clients/port/mixins/organization.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/clients/port/retry_transport.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/clients/port/types.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/clients/port/utils.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/config/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/config/base.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/config/dynamic.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/config/settings.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/consumers/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/consumers/kafka_consumer.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/context/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/context/event.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/context/metric_resource.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/context/ocean.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/context/resource.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/defaults/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/defaults/clean.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/defaults/common.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/defaults/initialize.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/event_listener/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/event_listener/base.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/event_listener/factory.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/event_listener/http.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/event_listener/kafka.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/event_listener/once.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/event_listener/polling.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/event_listener/webhooks_only.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/handlers/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/handlers/base.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/handlers/entities_state_applier/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/handlers/entities_state_applier/base.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/handlers/entities_state_applier/port/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/handlers/entities_state_applier/port/applier.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/handlers/entities_state_applier/port/get_related_entities.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/handlers/entities_state_applier/port/order_by_entities_dependencies.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/handlers/entity_processor/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/handlers/entity_processor/base.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/handlers/port_app_config/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/handlers/port_app_config/api.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/handlers/port_app_config/base.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/handlers/queue/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/handlers/queue/abstract_queue.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/handlers/queue/group_queue.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/handlers/queue/local_queue.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/handlers/resync_state_updater/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/handlers/resync_state_updater/updater.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/handlers/webhook/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/handlers/webhook/abstract_webhook_processor.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/handlers/webhook/processor_manager.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/handlers/webhook/webhook_event.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/integrations/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/integrations/base.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/integrations/mixins/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/integrations/mixins/events.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/integrations/mixins/handler.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/integrations/mixins/live_events.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/integrations/mixins/sync.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/models.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/ocean_types.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/utils/entity_topological_sorter.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/core/utils/utils.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/debug_cli.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/exceptions/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/exceptions/api.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/exceptions/base.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/exceptions/clients.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/exceptions/context.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/exceptions/core.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/exceptions/port_defaults.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/exceptions/utils.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/exceptions/webhook_processor.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/helpers/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/helpers/async_client.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/helpers/metric/metric.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/helpers/metric/utils.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/helpers/retry.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/helpers/stream.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/log/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/log/handlers.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/log/logger_setup.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/log/sensetive.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/middlewares.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/ocean.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/py.typed +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/run.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/sonar-project.properties +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/cache/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/cache/test_disk_cache.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/cache/test_memory_cache.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/clients/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/clients/oauth/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/clients/oauth/test_oauth_client.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/clients/port/mixins/test_entities.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/clients/port/mixins/test_integrations.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/clients/port/mixins/test_organization_mixin.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/config/test_config.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/conftest.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/core/conftest.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/core/defaults/test_common.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/core/event_listener/test_kafka.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/core/handlers/entities_state_applier/test_applier.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/core/handlers/mixins/test_live_events.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/core/handlers/mixins/test_sync_raw.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/core/handlers/port_app_config/test_api.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/core/handlers/port_app_config/test_base.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/core/handlers/queue/test_group_queue.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/core/handlers/queue/test_local_queue.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/core/handlers/webhook/test_abstract_webhook_processor.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/core/handlers/webhook/test_processor_manager.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/core/handlers/webhook/test_webhook_event.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/core/test_utils.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/core/utils/test_entity_topological_sorter.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/core/utils/test_resolve_entities_diff.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/helpers/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/helpers/fake_port_api.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/helpers/fixtures.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/helpers/integration.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/helpers/ocean_app.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/helpers/port_client.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/helpers/smoke_test.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/helpers/test_retry.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/log/test_handlers.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/test_metric.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/test_ocean.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/test_smoke.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/utils/test_async_iterators.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/tests/utils/test_cache.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/utils/__init__.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/utils/async_http.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/utils/async_iterators.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/utils/cache.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/utils/ipc.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/utils/misc.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/utils/queue_utils.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/utils/repeat.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/utils/signal.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/utils/time.py +0 -0
- {port_ocean-0.28.5 → port_ocean-0.28.7}/port_ocean/version.py +0 -0
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from asyncio import Task
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
import jq # type: ignore
|
|
8
|
+
from loguru import logger
|
|
9
|
+
from port_ocean.context.ocean import ocean
|
|
10
|
+
from port_ocean.core.handlers.entity_processor.base import BaseEntityProcessor
|
|
11
|
+
from port_ocean.core.handlers.port_app_config.models import ResourceConfig
|
|
12
|
+
from port_ocean.core.models import Entity
|
|
13
|
+
from port_ocean.core.ocean_types import (
|
|
14
|
+
RAW_ITEM,
|
|
15
|
+
EntitySelectorDiff,
|
|
16
|
+
CalculationResult,
|
|
17
|
+
)
|
|
18
|
+
from port_ocean.core.utils.utils import (
|
|
19
|
+
gather_and_split_errors_from_results,
|
|
20
|
+
zip_and_sum,
|
|
21
|
+
)
|
|
22
|
+
from port_ocean.exceptions.core import EntityProcessorException
|
|
23
|
+
from port_ocean.utils.queue_utils import process_in_queue
|
|
24
|
+
from port_ocean.core.handlers.entity_processor.jq_input_evaluator import (
|
|
25
|
+
InputEvaluationResult,
|
|
26
|
+
evaluate_input,
|
|
27
|
+
should_shortcut_no_input,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ExampleStates:
|
|
32
|
+
__succeed: list[dict[str, Any]]
|
|
33
|
+
__errors: list[dict[str, Any]]
|
|
34
|
+
__max_size: int
|
|
35
|
+
|
|
36
|
+
def __init__(self, max_size: int = 0) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Store two sequences:
|
|
39
|
+
- succeed: items that succeeded
|
|
40
|
+
- errors: items that failed
|
|
41
|
+
"""
|
|
42
|
+
self.__succeed = []
|
|
43
|
+
self.__errors = []
|
|
44
|
+
self.__max_size = max_size
|
|
45
|
+
|
|
46
|
+
def add_example(self, succeed: bool, item: dict[str, Any]) -> None:
|
|
47
|
+
if succeed:
|
|
48
|
+
self.__succeed.append(item)
|
|
49
|
+
else:
|
|
50
|
+
self.__errors.append(item)
|
|
51
|
+
|
|
52
|
+
def __len__(self) -> int:
|
|
53
|
+
"""
|
|
54
|
+
Total number of items (successes + errors).
|
|
55
|
+
"""
|
|
56
|
+
return len(self.__succeed) + len(self.__errors)
|
|
57
|
+
|
|
58
|
+
def get_examples(self, number: int = 0) -> list[dict[str, Any]]:
|
|
59
|
+
"""
|
|
60
|
+
Return a list of up to number items, taking successes first,
|
|
61
|
+
"""
|
|
62
|
+
if number <= 0:
|
|
63
|
+
number = self.__max_size
|
|
64
|
+
# how many from succeed?
|
|
65
|
+
s_count = min(number, len(self.__succeed))
|
|
66
|
+
result = list(self.__succeed[:s_count])
|
|
67
|
+
# how many more from errors?
|
|
68
|
+
e_count = number - s_count
|
|
69
|
+
if e_count > 0:
|
|
70
|
+
result.extend(self.__errors[:e_count])
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class MappedEntity:
|
|
76
|
+
"""Represents the entity after applying the mapping
|
|
77
|
+
|
|
78
|
+
This class holds the mapping entity along with the selector boolean value and optionally the raw data.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
entity: dict[str, Any] = field(default_factory=dict)
|
|
82
|
+
did_entity_pass_selector: bool = False
|
|
83
|
+
raw_data: Optional[dict[str, Any] | tuple[dict[str, Any], str]] = None
|
|
84
|
+
misconfigurations: dict[str, str] = field(default_factory=dict)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class JQEntityProcessor(BaseEntityProcessor):
|
|
88
|
+
"""Processes and parses entities using JQ expressions.
|
|
89
|
+
|
|
90
|
+
This class extends the BaseEntityProcessor and provides methods for processing and
|
|
91
|
+
parsing entities based on PyJQ queries. It supports compiling and executing PyJQ patterns,
|
|
92
|
+
searching for data in dictionaries, and transforming data based on object mappings.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
@lru_cache
|
|
96
|
+
def _compile(self, pattern: str) -> Any:
|
|
97
|
+
if not ocean.config.allow_environment_variables_jq_access:
|
|
98
|
+
pattern = "def env: {}; {} as $ENV | " + pattern
|
|
99
|
+
return jq.compile(pattern)
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def _stop_iterator_handler(func: Any) -> Any:
|
|
103
|
+
"""
|
|
104
|
+
Wrap the function to handle StopIteration exceptions.
|
|
105
|
+
Prevents StopIteration from stopping the thread and skipping further queue processing.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def inner() -> Any:
|
|
109
|
+
try:
|
|
110
|
+
return func()
|
|
111
|
+
except StopIteration:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
return inner
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def _notify_mapping_issues(
|
|
118
|
+
entity_misconfigurations: dict[str, str],
|
|
119
|
+
missing_required_fields: bool,
|
|
120
|
+
entity_mapping_fault_counter: int,
|
|
121
|
+
) -> None:
|
|
122
|
+
|
|
123
|
+
if len(entity_misconfigurations) > 0:
|
|
124
|
+
logger.info(
|
|
125
|
+
f"Unable to find valid data for: {entity_misconfigurations} (null, missing, or misconfigured)"
|
|
126
|
+
)
|
|
127
|
+
if missing_required_fields:
|
|
128
|
+
logger.info(
|
|
129
|
+
f"{entity_mapping_fault_counter} transformations of batch failed due to empty, null or missing values"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
async def _search(self, data: dict[str, Any], pattern: str) -> Any:
|
|
133
|
+
try:
|
|
134
|
+
loop = asyncio.get_event_loop()
|
|
135
|
+
compiled_pattern = self._compile(pattern)
|
|
136
|
+
func = compiled_pattern.input_value(data)
|
|
137
|
+
return await loop.run_in_executor(
|
|
138
|
+
None, self._stop_iterator_handler(func.first)
|
|
139
|
+
)
|
|
140
|
+
except Exception as exc:
|
|
141
|
+
logger.error(
|
|
142
|
+
f"Search failed for pattern '{pattern}' in data: {data}, Error: {exc}"
|
|
143
|
+
)
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
@lru_cache
|
|
147
|
+
async def _search_stringified(self, data: str, pattern: str) -> Any:
|
|
148
|
+
try:
|
|
149
|
+
loop = asyncio.get_event_loop()
|
|
150
|
+
compiled_pattern = self._compile(pattern)
|
|
151
|
+
func = compiled_pattern.input_text(data)
|
|
152
|
+
return await loop.run_in_executor(
|
|
153
|
+
None, self._stop_iterator_handler(func.first)
|
|
154
|
+
)
|
|
155
|
+
except Exception as exc:
|
|
156
|
+
logger.debug(
|
|
157
|
+
f"Search failed for pattern '{pattern}' in data: {data}, Error: {exc}"
|
|
158
|
+
)
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
async def _search_as_bool(self, data: dict[str, Any] | str, pattern: str) -> bool:
|
|
162
|
+
loop = asyncio.get_event_loop()
|
|
163
|
+
|
|
164
|
+
compiled_pattern = self._compile(pattern)
|
|
165
|
+
if isinstance(data, str):
|
|
166
|
+
func = compiled_pattern.input_text(data)
|
|
167
|
+
else:
|
|
168
|
+
func = compiled_pattern.input_value(data)
|
|
169
|
+
|
|
170
|
+
value = await loop.run_in_executor(
|
|
171
|
+
None, self._stop_iterator_handler(func.first)
|
|
172
|
+
)
|
|
173
|
+
if isinstance(value, bool):
|
|
174
|
+
return value
|
|
175
|
+
raise EntityProcessorException(
|
|
176
|
+
f"Expected boolean value, got value:{value} of type: {type(value)} instead"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
async def _search_as_object(
|
|
180
|
+
self,
|
|
181
|
+
data: dict[str, Any] | str,
|
|
182
|
+
obj: dict[str, Any],
|
|
183
|
+
misconfigurations: dict[str, str] | None = None,
|
|
184
|
+
) -> dict[str, Any | None]:
|
|
185
|
+
"""
|
|
186
|
+
Identify and extract the relevant value for the chosen key and populate it into the entity
|
|
187
|
+
:param data: the property itself that holds the key and the value, it is being passed to the task and we get back a task item,
|
|
188
|
+
if the data is a dict, we will recursively call this function again.
|
|
189
|
+
:param obj: the key that we want its value to be mapped into our entity.
|
|
190
|
+
:param misconfigurations: due to the recursive nature of this function,
|
|
191
|
+
we aim to have a dict that represents all of the misconfigured properties and when used recursively,
|
|
192
|
+
we pass this reference to misfoncigured object to add the relevant misconfigured keys.
|
|
193
|
+
:return: Mapped object with found value.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
search_tasks: dict[
|
|
197
|
+
str, Task[dict[str, Any | None]] | list[Task[dict[str, Any | None]]]
|
|
198
|
+
] = {}
|
|
199
|
+
for key, value in obj.items():
|
|
200
|
+
if isinstance(value, list):
|
|
201
|
+
search_tasks[key] = [
|
|
202
|
+
asyncio.create_task(
|
|
203
|
+
self._search_as_object(data, obj, misconfigurations)
|
|
204
|
+
)
|
|
205
|
+
for obj in value
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
elif isinstance(value, dict):
|
|
209
|
+
search_tasks[key] = asyncio.create_task(
|
|
210
|
+
self._search_as_object(data, value, misconfigurations)
|
|
211
|
+
)
|
|
212
|
+
else:
|
|
213
|
+
if isinstance(data, str):
|
|
214
|
+
search_tasks[key] = asyncio.create_task(
|
|
215
|
+
self._search_stringified(data, value)
|
|
216
|
+
)
|
|
217
|
+
else:
|
|
218
|
+
search_tasks[key] = asyncio.create_task(self._search(data, value))
|
|
219
|
+
|
|
220
|
+
result: dict[str, Any | None] = {}
|
|
221
|
+
for key, task in search_tasks.items():
|
|
222
|
+
try:
|
|
223
|
+
if isinstance(task, list):
|
|
224
|
+
result_list = []
|
|
225
|
+
for task in task:
|
|
226
|
+
task_result = await task
|
|
227
|
+
if task_result is None and misconfigurations is not None:
|
|
228
|
+
misconfigurations[key] = obj[key]
|
|
229
|
+
result_list.append(task_result)
|
|
230
|
+
result[key] = result_list
|
|
231
|
+
else:
|
|
232
|
+
task_result = await task
|
|
233
|
+
if task_result is None and misconfigurations is not None:
|
|
234
|
+
misconfigurations[key] = obj[key]
|
|
235
|
+
result[key] = task_result
|
|
236
|
+
except Exception:
|
|
237
|
+
result[key] = None
|
|
238
|
+
return result
|
|
239
|
+
|
|
240
|
+
async def _get_mapped_entity(
|
|
241
|
+
self,
|
|
242
|
+
data: dict[str, Any] | tuple[dict[str, Any], str],
|
|
243
|
+
raw_entity_mappings: dict[str, Any],
|
|
244
|
+
items_to_parse_key: str | None,
|
|
245
|
+
selector_query: str,
|
|
246
|
+
parse_all: bool = False,
|
|
247
|
+
) -> MappedEntity:
|
|
248
|
+
should_run = await self._should_map_entity(
|
|
249
|
+
data, selector_query, items_to_parse_key
|
|
250
|
+
)
|
|
251
|
+
if parse_all or should_run:
|
|
252
|
+
misconfigurations, mapped_entity = await self._map_entity(
|
|
253
|
+
data, raw_entity_mappings, items_to_parse_key
|
|
254
|
+
)
|
|
255
|
+
return MappedEntity(
|
|
256
|
+
mapped_entity,
|
|
257
|
+
did_entity_pass_selector=should_run,
|
|
258
|
+
raw_data=data,
|
|
259
|
+
misconfigurations=misconfigurations,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
return MappedEntity(
|
|
263
|
+
{},
|
|
264
|
+
did_entity_pass_selector=False,
|
|
265
|
+
raw_data=data,
|
|
266
|
+
misconfigurations={},
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
async def _map_entity(
|
|
270
|
+
self,
|
|
271
|
+
data: dict[str, Any] | tuple[dict[str, Any], str],
|
|
272
|
+
raw_entity_mappings: dict[str, Any],
|
|
273
|
+
items_to_parse_key: str | None,
|
|
274
|
+
) -> tuple[dict[str, str], dict[str, Any]]:
|
|
275
|
+
if not items_to_parse_key:
|
|
276
|
+
misconfigurations: dict[str, str] = {}
|
|
277
|
+
data_to_search = data if isinstance(data, dict) else data[0]
|
|
278
|
+
mapped_entity = await self._search_as_object(
|
|
279
|
+
data_to_search, raw_entity_mappings, misconfigurations
|
|
280
|
+
)
|
|
281
|
+
return misconfigurations, mapped_entity
|
|
282
|
+
|
|
283
|
+
modified_data: tuple[dict[str, Any], str | dict[str, Any]] = (
|
|
284
|
+
data
|
|
285
|
+
if isinstance(data, tuple)
|
|
286
|
+
else (
|
|
287
|
+
{items_to_parse_key: data[items_to_parse_key]},
|
|
288
|
+
data,
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
misconfigurations_item: dict[str, str] = {}
|
|
293
|
+
misconfigurations_all: dict[str, str] = {}
|
|
294
|
+
mapped_entity_item = await self._search_as_object(
|
|
295
|
+
modified_data[0], raw_entity_mappings["item"], misconfigurations_item
|
|
296
|
+
)
|
|
297
|
+
if misconfigurations_item:
|
|
298
|
+
filtered_item_mappings = self._filter_mappings_by_keys(
|
|
299
|
+
raw_entity_mappings["item"], list(misconfigurations_item.keys())
|
|
300
|
+
)
|
|
301
|
+
raw_entity_mappings["all"] = self._deep_merge(
|
|
302
|
+
raw_entity_mappings["all"], filtered_item_mappings
|
|
303
|
+
)
|
|
304
|
+
mapped_entity_all = await self._search_as_object(
|
|
305
|
+
modified_data[1], raw_entity_mappings["all"], misconfigurations_all
|
|
306
|
+
)
|
|
307
|
+
mapped_entity_empty = await self._search_as_object(
|
|
308
|
+
{}, raw_entity_mappings["empty"], misconfigurations_all
|
|
309
|
+
)
|
|
310
|
+
mapped_entity = self._deep_merge(mapped_entity_item, mapped_entity_all)
|
|
311
|
+
mapped_entity = self._deep_merge(mapped_entity, mapped_entity_empty)
|
|
312
|
+
return misconfigurations_all, mapped_entity
|
|
313
|
+
|
|
314
|
+
async def _should_map_entity(
|
|
315
|
+
self,
|
|
316
|
+
data: dict[str, Any] | tuple[dict[str, Any], str],
|
|
317
|
+
selector_query: str,
|
|
318
|
+
items_to_parse_key: str | None,
|
|
319
|
+
) -> bool:
|
|
320
|
+
if should_shortcut_no_input(selector_query):
|
|
321
|
+
return await self._search_as_bool({}, selector_query)
|
|
322
|
+
if isinstance(data, tuple):
|
|
323
|
+
return await self._search_as_bool(
|
|
324
|
+
data[0], selector_query
|
|
325
|
+
) or await self._search_as_bool(data[1], selector_query)
|
|
326
|
+
if items_to_parse_key:
|
|
327
|
+
return await self._search_as_bool(
|
|
328
|
+
data[items_to_parse_key], selector_query
|
|
329
|
+
) or await self._search_as_bool(data, selector_query)
|
|
330
|
+
return await self._search_as_bool(data, selector_query)
|
|
331
|
+
|
|
332
|
+
async def _calculate_entity(
|
|
333
|
+
self,
|
|
334
|
+
data: dict[str, Any],
|
|
335
|
+
raw_entity_mappings: dict[str, Any],
|
|
336
|
+
items_to_parse: str | None,
|
|
337
|
+
items_to_parse_name: str,
|
|
338
|
+
selector_query: str,
|
|
339
|
+
parse_all: bool = False,
|
|
340
|
+
) -> tuple[list[MappedEntity], list[Exception]]:
|
|
341
|
+
raw_data: list[dict[str, Any]] | list[tuple[dict[str, Any], str]] = [
|
|
342
|
+
data.copy()
|
|
343
|
+
]
|
|
344
|
+
items_to_parse_key = None
|
|
345
|
+
if items_to_parse:
|
|
346
|
+
items_to_parse_key = items_to_parse_name
|
|
347
|
+
if not ocean.config.yield_items_to_parse:
|
|
348
|
+
if data.get("file", {}).get("content", {}).get("path", None):
|
|
349
|
+
with open(data["file"]["content"]["path"], "r") as f:
|
|
350
|
+
data["file"]["content"] = json.loads(f.read())
|
|
351
|
+
items = await self._search(data, items_to_parse)
|
|
352
|
+
if not isinstance(items, list):
|
|
353
|
+
logger.warning(
|
|
354
|
+
f"Failed to parse items for JQ expression {items_to_parse}, Expected list but got {type(items)}."
|
|
355
|
+
f" Skipping..."
|
|
356
|
+
)
|
|
357
|
+
return [], []
|
|
358
|
+
raw_all_payload_stringified = json.dumps(data)
|
|
359
|
+
raw_data = [
|
|
360
|
+
({items_to_parse_name: item}, raw_all_payload_stringified)
|
|
361
|
+
for item in items
|
|
362
|
+
]
|
|
363
|
+
single_item_mappings, all_items_mappings, empty_items_mappings = (
|
|
364
|
+
self._build_raw_entity_mappings(
|
|
365
|
+
raw_entity_mappings, items_to_parse_name
|
|
366
|
+
)
|
|
367
|
+
)
|
|
368
|
+
raw_entity_mappings = {
|
|
369
|
+
"item": single_item_mappings,
|
|
370
|
+
"all": all_items_mappings,
|
|
371
|
+
"empty": empty_items_mappings,
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
entities, errors = await gather_and_split_errors_from_results(
|
|
375
|
+
[
|
|
376
|
+
self._get_mapped_entity(
|
|
377
|
+
raw,
|
|
378
|
+
raw_entity_mappings,
|
|
379
|
+
items_to_parse_key,
|
|
380
|
+
selector_query,
|
|
381
|
+
parse_all,
|
|
382
|
+
)
|
|
383
|
+
for raw in raw_data
|
|
384
|
+
]
|
|
385
|
+
)
|
|
386
|
+
if errors:
|
|
387
|
+
logger.error(
|
|
388
|
+
f"Failed to calculate entities with {len(errors)} errors. errors: {errors}"
|
|
389
|
+
)
|
|
390
|
+
return entities, errors
|
|
391
|
+
|
|
392
|
+
def _build_raw_entity_mappings(
|
|
393
|
+
self, raw_entity_mappings: dict[str, Any], items_to_parse_name: str
|
|
394
|
+
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
|
|
395
|
+
"""Filter entity mappings to only include values that start with f'.{items_to_parse_name}'"""
|
|
396
|
+
mappings: dict[InputEvaluationResult, dict[str, Any]] = {
|
|
397
|
+
InputEvaluationResult.NONE: {},
|
|
398
|
+
InputEvaluationResult.SINGLE: {},
|
|
399
|
+
InputEvaluationResult.ALL: {},
|
|
400
|
+
}
|
|
401
|
+
pattern = f".{items_to_parse_name}"
|
|
402
|
+
for key, value in raw_entity_mappings.items():
|
|
403
|
+
if isinstance(value, str):
|
|
404
|
+
# Direct string values (identifier, title, icon, blueprint, team)
|
|
405
|
+
self.group_string_mapping_value(
|
|
406
|
+
pattern,
|
|
407
|
+
mappings,
|
|
408
|
+
key,
|
|
409
|
+
value,
|
|
410
|
+
)
|
|
411
|
+
elif isinstance(value, dict):
|
|
412
|
+
# Complex objects (IngestSearchQuery for identifier/team, properties, relations)
|
|
413
|
+
self.group_complex_mapping_value(
|
|
414
|
+
pattern,
|
|
415
|
+
mappings,
|
|
416
|
+
key,
|
|
417
|
+
value,
|
|
418
|
+
)
|
|
419
|
+
return (
|
|
420
|
+
mappings[InputEvaluationResult.SINGLE],
|
|
421
|
+
mappings[InputEvaluationResult.ALL],
|
|
422
|
+
mappings[InputEvaluationResult.NONE],
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
def group_complex_mapping_value(
|
|
426
|
+
self,
|
|
427
|
+
pattern: str,
|
|
428
|
+
mappings: dict[InputEvaluationResult, dict[str, Any]],
|
|
429
|
+
key: str,
|
|
430
|
+
value: dict[str, Any],
|
|
431
|
+
) -> None:
|
|
432
|
+
mapping_dicts: dict[InputEvaluationResult, dict[str, Any]] = {
|
|
433
|
+
InputEvaluationResult.SINGLE: {},
|
|
434
|
+
InputEvaluationResult.ALL: {},
|
|
435
|
+
InputEvaluationResult.NONE: {},
|
|
436
|
+
}
|
|
437
|
+
if key in ["properties", "relations"]:
|
|
438
|
+
# For properties and relations, filter the dictionary values
|
|
439
|
+
for dict_key, dict_value in value.items():
|
|
440
|
+
if isinstance(dict_value, str):
|
|
441
|
+
self.group_string_mapping_value(
|
|
442
|
+
pattern,
|
|
443
|
+
mapping_dicts,
|
|
444
|
+
dict_key,
|
|
445
|
+
dict_value,
|
|
446
|
+
)
|
|
447
|
+
elif isinstance(dict_value, dict):
|
|
448
|
+
# Handle IngestSearchQuery objects
|
|
449
|
+
self.group_search_query_mapping_value(
|
|
450
|
+
pattern,
|
|
451
|
+
mapping_dicts[InputEvaluationResult.SINGLE],
|
|
452
|
+
mapping_dicts[InputEvaluationResult.ALL],
|
|
453
|
+
dict_key,
|
|
454
|
+
dict_value,
|
|
455
|
+
)
|
|
456
|
+
else:
|
|
457
|
+
# For identifier/team IngestSearchQuery objects
|
|
458
|
+
self.group_search_query_mapping_value(
|
|
459
|
+
pattern,
|
|
460
|
+
mapping_dicts[InputEvaluationResult.SINGLE],
|
|
461
|
+
mapping_dicts[InputEvaluationResult.ALL],
|
|
462
|
+
key,
|
|
463
|
+
value,
|
|
464
|
+
)
|
|
465
|
+
if mapping_dicts[InputEvaluationResult.SINGLE]:
|
|
466
|
+
mappings[InputEvaluationResult.SINGLE][key] = mapping_dicts[
|
|
467
|
+
InputEvaluationResult.SINGLE
|
|
468
|
+
][key]
|
|
469
|
+
if mapping_dicts[InputEvaluationResult.ALL]:
|
|
470
|
+
mappings[InputEvaluationResult.ALL][key] = mapping_dicts[
|
|
471
|
+
InputEvaluationResult.ALL
|
|
472
|
+
][key]
|
|
473
|
+
if mapping_dicts[InputEvaluationResult.NONE]:
|
|
474
|
+
mappings[InputEvaluationResult.NONE][key] = mapping_dicts[
|
|
475
|
+
InputEvaluationResult.NONE
|
|
476
|
+
][key]
|
|
477
|
+
|
|
478
|
+
def group_search_query_mapping_value(
|
|
479
|
+
self,
|
|
480
|
+
pattern: str,
|
|
481
|
+
single_item_dict: dict[str, Any],
|
|
482
|
+
all_item_dict: dict[str, Any],
|
|
483
|
+
dict_key: str,
|
|
484
|
+
dict_value: dict[str, Any],
|
|
485
|
+
) -> None:
|
|
486
|
+
if self._should_keep_ingest_search_query(dict_value, pattern):
|
|
487
|
+
single_item_dict[dict_key] = dict_value
|
|
488
|
+
else:
|
|
489
|
+
all_item_dict[dict_key] = dict_value
|
|
490
|
+
|
|
491
|
+
def group_string_mapping_value(
|
|
492
|
+
self,
|
|
493
|
+
pattern: str,
|
|
494
|
+
mappings: dict[InputEvaluationResult, dict[str, Any]],
|
|
495
|
+
key: str,
|
|
496
|
+
value: str,
|
|
497
|
+
) -> None:
|
|
498
|
+
input_evaluation_result = evaluate_input(value, pattern)
|
|
499
|
+
mappings[input_evaluation_result][key] = value
|
|
500
|
+
|
|
501
|
+
def _should_keep_ingest_search_query(
|
|
502
|
+
self, query_dict: dict[str, Any], pattern: str
|
|
503
|
+
) -> bool:
|
|
504
|
+
"""Check if an IngestSearchQuery should be kept based on its rules"""
|
|
505
|
+
if "rules" not in query_dict:
|
|
506
|
+
return False
|
|
507
|
+
|
|
508
|
+
rules = query_dict["rules"]
|
|
509
|
+
if not isinstance(rules, list):
|
|
510
|
+
return False
|
|
511
|
+
|
|
512
|
+
# Check if any rule contains a value starting with the pattern
|
|
513
|
+
for rule in rules:
|
|
514
|
+
if isinstance(rule, dict):
|
|
515
|
+
if "value" in rule and isinstance(rule["value"], str):
|
|
516
|
+
if pattern in rule["value"]:
|
|
517
|
+
return True
|
|
518
|
+
# Recursively check nested IngestSearchQuery objects
|
|
519
|
+
elif "rules" in rule:
|
|
520
|
+
if self._should_keep_ingest_search_query(rule, pattern):
|
|
521
|
+
return True
|
|
522
|
+
return False
|
|
523
|
+
|
|
524
|
+
def _filter_mappings_by_keys(
|
|
525
|
+
self, mappings: dict[str, Any], target_keys: list[str]
|
|
526
|
+
) -> dict[str, Any]:
|
|
527
|
+
"""
|
|
528
|
+
Filter mappings to preserve structure with only the specified keys present.
|
|
529
|
+
Recursively handles nested dictionaries and lists, searching for keys at any level.
|
|
530
|
+
"""
|
|
531
|
+
if not target_keys:
|
|
532
|
+
return {}
|
|
533
|
+
|
|
534
|
+
filtered_mappings: dict[str, Any] = {}
|
|
535
|
+
|
|
536
|
+
for key, value in mappings.items():
|
|
537
|
+
filtered_value = self._process_mapping_value(key, value, target_keys)
|
|
538
|
+
|
|
539
|
+
# Include if it's a direct match or contains nested target keys
|
|
540
|
+
if key in target_keys or filtered_value:
|
|
541
|
+
filtered_mappings[key] = filtered_value
|
|
542
|
+
|
|
543
|
+
return filtered_mappings
|
|
544
|
+
|
|
545
|
+
def _process_mapping_value(
|
|
546
|
+
self, key: str, value: Any, target_keys: list[str]
|
|
547
|
+
) -> Any:
|
|
548
|
+
"""Process a single mapping value, handling different types recursively."""
|
|
549
|
+
if isinstance(value, dict):
|
|
550
|
+
# Recursively filter nested dictionary
|
|
551
|
+
filtered_dict = self._filter_mappings_by_keys(value, target_keys)
|
|
552
|
+
return filtered_dict if filtered_dict else None
|
|
553
|
+
else:
|
|
554
|
+
# Return simple values as-is
|
|
555
|
+
return value if key in target_keys else None
|
|
556
|
+
|
|
557
|
+
def _deep_merge(
|
|
558
|
+
self, dict1: dict[str, Any], dict2: dict[str, Any]
|
|
559
|
+
) -> dict[str, Any]:
|
|
560
|
+
"""
|
|
561
|
+
Deep merge two dictionaries, preserving nested structures.
|
|
562
|
+
Values from dict2 override values from dict1 for the same keys.
|
|
563
|
+
"""
|
|
564
|
+
result = dict1.copy()
|
|
565
|
+
|
|
566
|
+
for key, value in dict2.items():
|
|
567
|
+
if (
|
|
568
|
+
key in result
|
|
569
|
+
and isinstance(result[key], dict)
|
|
570
|
+
and isinstance(value, dict)
|
|
571
|
+
):
|
|
572
|
+
# Recursively merge nested dictionaries
|
|
573
|
+
result[key] = self._deep_merge(result[key], value)
|
|
574
|
+
elif (
|
|
575
|
+
key in result
|
|
576
|
+
and isinstance(result[key], list)
|
|
577
|
+
and isinstance(value, list)
|
|
578
|
+
):
|
|
579
|
+
# Merge lists by extending
|
|
580
|
+
result[key].extend(value)
|
|
581
|
+
else:
|
|
582
|
+
# Override with value from dict2
|
|
583
|
+
result[key] = value
|
|
584
|
+
|
|
585
|
+
return result
|
|
586
|
+
|
|
587
|
+
@staticmethod
|
|
588
|
+
async def _send_examples(data: list[dict[str, Any]], kind: str) -> None:
|
|
589
|
+
try:
|
|
590
|
+
if data:
|
|
591
|
+
await ocean.port_client.ingest_integration_kind_examples(
|
|
592
|
+
kind, data, should_log=False
|
|
593
|
+
)
|
|
594
|
+
except Exception as ex:
|
|
595
|
+
logger.warning(
|
|
596
|
+
f"Failed to send raw data example {ex}",
|
|
597
|
+
exc_info=True,
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
async def _parse_items(
|
|
601
|
+
self,
|
|
602
|
+
mapping: ResourceConfig,
|
|
603
|
+
raw_results: list[RAW_ITEM],
|
|
604
|
+
parse_all: bool = False,
|
|
605
|
+
send_raw_data_examples_amount: int = 0,
|
|
606
|
+
) -> CalculationResult:
|
|
607
|
+
raw_entity_mappings: dict[str, Any] = mapping.port.entity.mappings.dict(
|
|
608
|
+
exclude_unset=True
|
|
609
|
+
)
|
|
610
|
+
logger.info(f"Parsing {len(raw_results)} raw results into entities")
|
|
611
|
+
calculated_entities_results, errors = zip_and_sum(
|
|
612
|
+
await process_in_queue(
|
|
613
|
+
raw_results,
|
|
614
|
+
self._calculate_entity,
|
|
615
|
+
raw_entity_mappings,
|
|
616
|
+
mapping.port.items_to_parse,
|
|
617
|
+
mapping.port.items_to_parse_name,
|
|
618
|
+
mapping.selector.query,
|
|
619
|
+
parse_all,
|
|
620
|
+
)
|
|
621
|
+
)
|
|
622
|
+
logger.debug(
|
|
623
|
+
f"Finished parsing raw results into entities with {len(errors)} errors. errors: {errors}"
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
passed_entities = []
|
|
627
|
+
failed_entities = []
|
|
628
|
+
examples_to_send = ExampleStates(send_raw_data_examples_amount)
|
|
629
|
+
entity_misconfigurations: dict[str, str] = {}
|
|
630
|
+
missing_required_fields: bool = False
|
|
631
|
+
entity_mapping_fault_counter: int = 0
|
|
632
|
+
for result in calculated_entities_results:
|
|
633
|
+
if len(result.misconfigurations) > 0:
|
|
634
|
+
entity_misconfigurations |= result.misconfigurations
|
|
635
|
+
|
|
636
|
+
if (
|
|
637
|
+
len(examples_to_send) < send_raw_data_examples_amount
|
|
638
|
+
and result.raw_data is not None
|
|
639
|
+
):
|
|
640
|
+
examples_to_send.add_example(
|
|
641
|
+
result.did_entity_pass_selector,
|
|
642
|
+
self._get_raw_data_for_example(
|
|
643
|
+
result.raw_data, mapping.port.items_to_parse_name
|
|
644
|
+
),
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
if result.entity.get("identifier") and result.entity.get("blueprint"):
|
|
648
|
+
parsed_entity = Entity.parse_obj(result.entity)
|
|
649
|
+
if result.did_entity_pass_selector:
|
|
650
|
+
passed_entities.append(parsed_entity)
|
|
651
|
+
else:
|
|
652
|
+
failed_entities.append(parsed_entity)
|
|
653
|
+
else:
|
|
654
|
+
missing_required_fields = True
|
|
655
|
+
entity_mapping_fault_counter += 1
|
|
656
|
+
|
|
657
|
+
self._notify_mapping_issues(
|
|
658
|
+
entity_misconfigurations,
|
|
659
|
+
missing_required_fields,
|
|
660
|
+
entity_mapping_fault_counter,
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
await self._send_examples(examples_to_send.get_examples(), mapping.kind)
|
|
664
|
+
|
|
665
|
+
return CalculationResult(
|
|
666
|
+
EntitySelectorDiff(passed=passed_entities, failed=failed_entities),
|
|
667
|
+
errors,
|
|
668
|
+
misconfigured_entity_keys=entity_misconfigurations,
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
def _get_raw_data_for_example(
|
|
672
|
+
self,
|
|
673
|
+
data: dict[str, Any] | tuple[dict[str, Any], str],
|
|
674
|
+
items_to_parse_name: str,
|
|
675
|
+
) -> dict[str, Any]:
|
|
676
|
+
if isinstance(data, tuple):
|
|
677
|
+
raw_data = json.loads(data[1])
|
|
678
|
+
return {items_to_parse_name: data[0], **raw_data}
|
|
679
|
+
return data
|