port-ocean 0.23.5__tar.gz → 0.24.1__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.23.5 → port_ocean-0.24.1}/PKG-INFO +1 -1
- port_ocean-0.24.1/port_ocean/clients/port/mixins/entities.py +570 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/config/settings.py +4 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/handlers/entities_state_applier/port/applier.py +1 -1
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/models.py +20 -1
- port_ocean-0.24.1/port_ocean/tests/clients/port/mixins/test_entities.py +174 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/core/conftest.py +50 -15
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/core/handlers/entities_state_applier/test_applier.py +41 -69
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/core/handlers/mixins/test_sync_raw.py +32 -10
- {port_ocean-0.23.5 → port_ocean-0.24.1}/pyproject.toml +1 -1
- port_ocean-0.23.5/port_ocean/clients/port/mixins/entities.py +0 -309
- port_ocean-0.23.5/port_ocean/tests/clients/port/mixins/test_entities.py +0 -56
- {port_ocean-0.23.5 → port_ocean-0.24.1}/LICENSE.md +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/README.md +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/integrations/_infra/Dockerfile.Deb +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/integrations/_infra/Dockerfile.alpine +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/integrations/_infra/Dockerfile.base.builder +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/integrations/_infra/Dockerfile.base.runner +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/integrations/_infra/Dockerfile.dockerignore +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/integrations/_infra/Dockerfile.local +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/integrations/_infra/Makefile +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/integrations/_infra/entry_local.sh +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/integrations/_infra/grpcio.sh +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/integrations/_infra/init.sh +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/bootstrap.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cache/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cache/base.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cache/disk.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cache/errors.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cache/memory.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/cli.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/commands/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/commands/defaults/__init___.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/commands/defaults/clean.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/commands/defaults/dock.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/commands/defaults/group.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/commands/list_integrations.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/commands/main.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/commands/new.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/commands/pull.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/commands/sail.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/commands/version.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/cookiecutter/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/cookiecutter/cookiecutter.json +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/cookiecutter/extensions.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/cookiecutter/hooks/post_gen_project.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.env.example +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.gitignore +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/.gitignore +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/blueprints.json +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/port-app-config.yml +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/spec.yaml +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CHANGELOG.md +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CONTRIBUTING.md +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/README.md +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/changelog/.gitignore +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/debug.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/main.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/poetry.toml +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/pyproject.toml +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/sonar-project.properties +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/tests/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/tests/test_sample.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/cli/utils.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/clients/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/clients/auth/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/clients/auth/auth_client.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/clients/auth/oauth_client.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/clients/port/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/clients/port/authentication.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/clients/port/client.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/clients/port/mixins/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/clients/port/mixins/blueprints.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/clients/port/mixins/integrations.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/clients/port/mixins/migrations.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/clients/port/mixins/organization.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/clients/port/retry_transport.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/clients/port/types.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/clients/port/utils.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/config/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/config/base.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/config/dynamic.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/consumers/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/consumers/kafka_consumer.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/context/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/context/event.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/context/metric_resource.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/context/ocean.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/context/resource.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/defaults/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/defaults/clean.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/defaults/common.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/defaults/initialize.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/event_listener/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/event_listener/base.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/event_listener/factory.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/event_listener/http.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/event_listener/kafka.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/event_listener/once.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/event_listener/polling.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/event_listener/webhooks_only.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/handlers/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/handlers/base.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/handlers/entities_state_applier/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/handlers/entities_state_applier/base.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/handlers/entities_state_applier/port/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/handlers/entities_state_applier/port/get_related_entities.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/handlers/entities_state_applier/port/order_by_entities_dependencies.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/handlers/entity_processor/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/handlers/entity_processor/base.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/handlers/entity_processor/jq_entity_processor.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/handlers/port_app_config/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/handlers/port_app_config/api.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/handlers/port_app_config/base.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/handlers/port_app_config/models.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/handlers/queue/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/handlers/queue/abstract_queue.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/handlers/queue/local_queue.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/handlers/resync_state_updater/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/handlers/resync_state_updater/updater.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/handlers/webhook/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/handlers/webhook/abstract_webhook_processor.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/handlers/webhook/processor_manager.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/handlers/webhook/webhook_event.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/integrations/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/integrations/base.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/integrations/mixins/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/integrations/mixins/events.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/integrations/mixins/handler.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/integrations/mixins/live_events.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/integrations/mixins/sync.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/integrations/mixins/sync_raw.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/integrations/mixins/utils.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/ocean_types.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/utils/entity_topological_sorter.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/core/utils/utils.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/debug_cli.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/exceptions/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/exceptions/api.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/exceptions/base.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/exceptions/clients.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/exceptions/context.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/exceptions/core.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/exceptions/port_defaults.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/exceptions/utils.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/exceptions/webhook_processor.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/helpers/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/helpers/async_client.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/helpers/metric/metric.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/helpers/metric/utils.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/helpers/retry.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/log/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/log/handlers.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/log/logger_setup.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/log/sensetive.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/middlewares.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/ocean.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/py.typed +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/run.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/sonar-project.properties +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/cache/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/cache/test_disk_cache.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/cache/test_memory_cache.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/clients/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/clients/oauth/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/clients/oauth/test_oauth_client.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/clients/port/mixins/test_organization_mixin.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/conftest.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/core/defaults/test_common.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/core/handlers/mixins/test_live_events.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/core/handlers/port_app_config/test_api.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/core/handlers/port_app_config/test_base.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/core/handlers/queue/test_local_queue.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/core/handlers/webhook/test_abstract_webhook_processor.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/core/handlers/webhook/test_processor_manager.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/core/handlers/webhook/test_webhook_event.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/core/test_utils.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/core/utils/test_entity_topological_sorter.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/core/utils/test_resolve_entities_diff.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/helpers/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/helpers/fake_port_api.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/helpers/fixtures.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/helpers/integration.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/helpers/ocean_app.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/helpers/port_client.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/helpers/smoke_test.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/log/test_handlers.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/test_metric.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/test_ocean.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/test_smoke.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/utils/test_async_iterators.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/tests/utils/test_cache.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/utils/__init__.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/utils/async_http.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/utils/async_iterators.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/utils/cache.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/utils/ipc.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/utils/misc.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/utils/queue_utils.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/utils/repeat.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/utils/signal.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/utils/time.py +0 -0
- {port_ocean-0.23.5 → port_ocean-0.24.1}/port_ocean/version.py +0 -0
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Any, Literal
|
|
3
|
+
from urllib.parse import quote_plus
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from loguru import logger
|
|
8
|
+
from port_ocean.context.ocean import ocean
|
|
9
|
+
from port_ocean.clients.port.authentication import PortAuthentication
|
|
10
|
+
from port_ocean.clients.port.types import RequestOptions, UserAgentType
|
|
11
|
+
from port_ocean.clients.port.utils import (
|
|
12
|
+
handle_port_status_code,
|
|
13
|
+
PORT_HTTP_MAX_CONNECTIONS_LIMIT,
|
|
14
|
+
)
|
|
15
|
+
from port_ocean.core.models import (
|
|
16
|
+
BulkUpsertResponse,
|
|
17
|
+
Entity,
|
|
18
|
+
PortAPIErrorMessage,
|
|
19
|
+
)
|
|
20
|
+
from starlette import status
|
|
21
|
+
|
|
22
|
+
from port_ocean.helpers.metric.metric import MetricPhase, MetricType
|
|
23
|
+
|
|
24
|
+
ENTITIES_BULK_SAMPLES_SIZE = 10
|
|
25
|
+
ENTITIES_BULK_ESTIMATED_SIZE_MULTIPLIER = 1.5
|
|
26
|
+
ENTITIES_BULK_MINIMUM_BATCH_SIZE = 1
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EntityClientMixin:
|
|
30
|
+
def __init__(self, auth: PortAuthentication, client: httpx.AsyncClient):
|
|
31
|
+
self.auth = auth
|
|
32
|
+
self.client = client
|
|
33
|
+
# Semaphore is used to limit the number of concurrent requests to port, to avoid overloading it.
|
|
34
|
+
# The number of concurrent requests is set to 90% of the max connections limit, to leave some room for other
|
|
35
|
+
# requests that are not related to entities.
|
|
36
|
+
self.semaphore = asyncio.Semaphore(
|
|
37
|
+
round(0.5 * PORT_HTTP_MAX_CONNECTIONS_LIMIT)
|
|
38
|
+
) # 50% of the max connections limit in order to avoid overloading port
|
|
39
|
+
|
|
40
|
+
def calculate_entities_batch_size(self, entities: list[Entity]) -> int:
|
|
41
|
+
"""
|
|
42
|
+
Calculate the optimal batch size based on entity size and configured limits.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
entities: List of entities to calculate batch size for
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
int: The optimal batch size to use
|
|
49
|
+
"""
|
|
50
|
+
if not entities:
|
|
51
|
+
return ENTITIES_BULK_MINIMUM_BATCH_SIZE
|
|
52
|
+
|
|
53
|
+
# Calculate average entity size from a sample
|
|
54
|
+
SAMPLE_SIZE = min(ENTITIES_BULK_SAMPLES_SIZE, len(entities))
|
|
55
|
+
sample_entities = entities[:SAMPLE_SIZE]
|
|
56
|
+
average_entity_size = (
|
|
57
|
+
sum(
|
|
58
|
+
len(json.dumps(entity.dict(exclude_unset=True, by_alias=True)).encode())
|
|
59
|
+
for entity in sample_entities
|
|
60
|
+
)
|
|
61
|
+
/ SAMPLE_SIZE
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Use a conservative estimate to ensure we stay under the limit
|
|
65
|
+
estimated_entity_size = int(
|
|
66
|
+
average_entity_size * ENTITIES_BULK_ESTIMATED_SIZE_MULTIPLIER
|
|
67
|
+
)
|
|
68
|
+
max_entities_per_batch = min(
|
|
69
|
+
ocean.config.upsert_entities_batch_max_length,
|
|
70
|
+
ocean.config.upsert_entities_batch_max_size_in_bytes
|
|
71
|
+
// estimated_entity_size,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return max(ENTITIES_BULK_MINIMUM_BATCH_SIZE, max_entities_per_batch)
|
|
75
|
+
|
|
76
|
+
async def upsert_entity(
|
|
77
|
+
self,
|
|
78
|
+
entity: Entity,
|
|
79
|
+
request_options: RequestOptions,
|
|
80
|
+
user_agent_type: UserAgentType | None = None,
|
|
81
|
+
should_raise: bool = True,
|
|
82
|
+
) -> Entity | None | Literal[False]:
|
|
83
|
+
"""
|
|
84
|
+
This function upserts an entity into Port.
|
|
85
|
+
|
|
86
|
+
Usage:
|
|
87
|
+
```python
|
|
88
|
+
upsertedEntity = await self.context.port_client.upsert_entity(
|
|
89
|
+
entity,
|
|
90
|
+
event.port_app_config.get_port_request_options(),
|
|
91
|
+
user_agent_type,
|
|
92
|
+
should_raise=False,
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
:param entity: An Entity to be upserted
|
|
96
|
+
:param request_options: A dictionary specifying how to upsert the entity
|
|
97
|
+
:param user_agent_type: a UserAgentType specifying who is preforming the action
|
|
98
|
+
:param should_raise: A boolean specifying whether the error should be raised or handled silently
|
|
99
|
+
:return: [Entity] if the upsert occured successfully
|
|
100
|
+
:return: [None] will be returned if entity is using search identifier
|
|
101
|
+
:return: [False] will be returned if upsert failed because of unmet dependency
|
|
102
|
+
"""
|
|
103
|
+
validation_only = request_options["validation_only"]
|
|
104
|
+
async with self.semaphore:
|
|
105
|
+
logger.debug(
|
|
106
|
+
f"{'Validating' if validation_only else 'Upserting'} entity: {entity.identifier} of blueprint: {entity.blueprint}"
|
|
107
|
+
)
|
|
108
|
+
headers = await self.auth.headers(user_agent_type)
|
|
109
|
+
response = await self.client.post(
|
|
110
|
+
f"{self.auth.api_url}/blueprints/{entity.blueprint}/entities",
|
|
111
|
+
json=entity.dict(exclude_unset=True, by_alias=True),
|
|
112
|
+
headers=headers,
|
|
113
|
+
params={
|
|
114
|
+
"upsert": "true",
|
|
115
|
+
"merge": str(request_options["merge"]).lower(),
|
|
116
|
+
"create_missing_related_entities": str(
|
|
117
|
+
request_options["create_missing_related_entities"]
|
|
118
|
+
).lower(),
|
|
119
|
+
"validation_only": str(validation_only).lower(),
|
|
120
|
+
},
|
|
121
|
+
extensions={"retryable": True},
|
|
122
|
+
)
|
|
123
|
+
if response.is_error:
|
|
124
|
+
logger.error(
|
|
125
|
+
f"Error {'Validating' if validation_only else 'Upserting'} "
|
|
126
|
+
f"entity: {entity.identifier} of "
|
|
127
|
+
f"blueprint: {entity.blueprint}"
|
|
128
|
+
)
|
|
129
|
+
result = response.json()
|
|
130
|
+
ocean.metrics.inc_metric(
|
|
131
|
+
name=MetricType.OBJECT_COUNT_NAME,
|
|
132
|
+
labels=[
|
|
133
|
+
ocean.metrics.current_resource_kind(),
|
|
134
|
+
MetricPhase.LOAD,
|
|
135
|
+
MetricPhase.LoadResult.FAILED,
|
|
136
|
+
],
|
|
137
|
+
value=1,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if (
|
|
141
|
+
response.status_code == status.HTTP_404_NOT_FOUND
|
|
142
|
+
and not result.get("ok")
|
|
143
|
+
and result.get("error") == PortAPIErrorMessage.NOT_FOUND.value
|
|
144
|
+
):
|
|
145
|
+
# Return false to differentiate from `result_entity.is_using_search_identifier`
|
|
146
|
+
return False
|
|
147
|
+
else:
|
|
148
|
+
ocean.metrics.inc_metric(
|
|
149
|
+
name=MetricType.OBJECT_COUNT_NAME,
|
|
150
|
+
labels=[
|
|
151
|
+
ocean.metrics.current_resource_kind(),
|
|
152
|
+
MetricPhase.LOAD,
|
|
153
|
+
MetricPhase.LoadResult.LOADED,
|
|
154
|
+
],
|
|
155
|
+
value=1,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
handle_port_status_code(response, should_raise)
|
|
159
|
+
result = response.json()
|
|
160
|
+
|
|
161
|
+
result_entity = (
|
|
162
|
+
Entity.parse_obj(result["entity"]) if result.get("entity") else entity
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Happens when upsert fails and search identifier is defined.
|
|
166
|
+
# We return None to ignore the entity later in the delete process
|
|
167
|
+
if result_entity.is_using_search_identifier:
|
|
168
|
+
return None
|
|
169
|
+
return self._reduce_entity(result_entity)
|
|
170
|
+
|
|
171
|
+
async def upsert_entities_bulk(
|
|
172
|
+
self,
|
|
173
|
+
blueprint: str,
|
|
174
|
+
entities: list[Entity],
|
|
175
|
+
request_options: RequestOptions,
|
|
176
|
+
user_agent_type: UserAgentType | None = None,
|
|
177
|
+
should_raise: bool = True,
|
|
178
|
+
) -> list[tuple[bool | None, Entity]] | httpx.HTTPStatusError:
|
|
179
|
+
"""
|
|
180
|
+
This function upserts a list of entities into Port.
|
|
181
|
+
|
|
182
|
+
Usage:
|
|
183
|
+
```python
|
|
184
|
+
upsertedEntities = await self.context.port_client.upsert_entities_batch(
|
|
185
|
+
entities,
|
|
186
|
+
event.port_app_config.get_port_request_options(),
|
|
187
|
+
user_agent_type,
|
|
188
|
+
should_raise=False,
|
|
189
|
+
)
|
|
190
|
+
```
|
|
191
|
+
:param blueprint: The blueprint of the entities to be upserted
|
|
192
|
+
:param entities: A list of Entities to be upserted
|
|
193
|
+
:param request_options: A dictionary specifying how to upsert the entity
|
|
194
|
+
:param user_agent_type: a UserAgentType specifying who is preforming the action
|
|
195
|
+
:param should_raise: A boolean specifying whether the error should be raised or handled silently
|
|
196
|
+
:return: A list of tuples where each tuple contains:
|
|
197
|
+
- First value: True if entity was created successfully, False if there was an error, None if there was an error and the entity use search identifier
|
|
198
|
+
- Second value: The original entity (if failed) or the reduced entity with updated identifier (if successful)
|
|
199
|
+
:return: httpx.HTTPStatusError if there was an HTTP error and should_raise is False
|
|
200
|
+
"""
|
|
201
|
+
validation_only = request_options["validation_only"]
|
|
202
|
+
async with self.semaphore:
|
|
203
|
+
logger.debug(
|
|
204
|
+
f"{'Validating' if validation_only else 'Upserting'} {len(entities)} of blueprint: {blueprint}"
|
|
205
|
+
)
|
|
206
|
+
headers = await self.auth.headers(user_agent_type)
|
|
207
|
+
response = await self.client.post(
|
|
208
|
+
f"{self.auth.api_url}/blueprints/{blueprint}/entities/bulk",
|
|
209
|
+
json={
|
|
210
|
+
"entities": [
|
|
211
|
+
entity.dict(exclude_unset=True, by_alias=True)
|
|
212
|
+
for entity in entities
|
|
213
|
+
]
|
|
214
|
+
},
|
|
215
|
+
headers=headers,
|
|
216
|
+
params={
|
|
217
|
+
"upsert": "true",
|
|
218
|
+
"merge": str(request_options["merge"]).lower(),
|
|
219
|
+
"create_missing_related_entities": str(
|
|
220
|
+
request_options["create_missing_related_entities"]
|
|
221
|
+
).lower(),
|
|
222
|
+
"validation_only": str(validation_only).lower(),
|
|
223
|
+
},
|
|
224
|
+
extensions={"retryable": True},
|
|
225
|
+
)
|
|
226
|
+
if response.is_error:
|
|
227
|
+
logger.error(
|
|
228
|
+
f"Error {'Validating' if validation_only else 'Upserting'} "
|
|
229
|
+
f"{len(entities)} entities of "
|
|
230
|
+
f"blueprint: {blueprint}"
|
|
231
|
+
)
|
|
232
|
+
handle_port_status_code(response, should_raise)
|
|
233
|
+
return httpx.HTTPStatusError(
|
|
234
|
+
f"HTTP {response.status_code}",
|
|
235
|
+
request=response.request,
|
|
236
|
+
response=response,
|
|
237
|
+
)
|
|
238
|
+
handle_port_status_code(response, should_raise)
|
|
239
|
+
result = response.json()
|
|
240
|
+
|
|
241
|
+
return self._parse_upsert_entities_batch_response(entities, result)
|
|
242
|
+
|
|
243
|
+
def _parse_upsert_entities_batch_response(
|
|
244
|
+
self,
|
|
245
|
+
entities: list[Entity],
|
|
246
|
+
result: BulkUpsertResponse,
|
|
247
|
+
) -> list[tuple[bool | None, Entity]]:
|
|
248
|
+
"""
|
|
249
|
+
Parse the response from a bulk upsert operation and map it to the original entities.
|
|
250
|
+
|
|
251
|
+
:param entities: The original entities
|
|
252
|
+
:param result: The response from the bulk upsert operation
|
|
253
|
+
:return: A list of tuples containing the success status and the entity
|
|
254
|
+
"""
|
|
255
|
+
index_to_entity = {i: entity for i, entity in enumerate(entities)}
|
|
256
|
+
successful_entities = {
|
|
257
|
+
entity_result["index"]: entity_result
|
|
258
|
+
for entity_result in result.get("entities", [])
|
|
259
|
+
}
|
|
260
|
+
error_entities = {error["index"]: error for error in result.get("errors", [])}
|
|
261
|
+
|
|
262
|
+
batch_results: list[tuple[bool | None, Entity]] = []
|
|
263
|
+
for entity_index, original_entity in index_to_entity.items():
|
|
264
|
+
reduced_entity = self._reduce_entity(original_entity)
|
|
265
|
+
if entity_index in successful_entities:
|
|
266
|
+
ocean.metrics.inc_metric(
|
|
267
|
+
name=MetricType.OBJECT_COUNT_NAME,
|
|
268
|
+
labels=[
|
|
269
|
+
ocean.metrics.current_resource_kind(),
|
|
270
|
+
MetricPhase.LOAD,
|
|
271
|
+
MetricPhase.LoadResult.LOADED,
|
|
272
|
+
],
|
|
273
|
+
value=1,
|
|
274
|
+
)
|
|
275
|
+
success_entity = successful_entities[entity_index]
|
|
276
|
+
# Create a copy of the original entity with the new identifier
|
|
277
|
+
updated_entity = reduced_entity.copy()
|
|
278
|
+
updated_entity.identifier = success_entity["identifier"]
|
|
279
|
+
batch_results.append((True, updated_entity))
|
|
280
|
+
elif entity_index in error_entities:
|
|
281
|
+
ocean.metrics.inc_metric(
|
|
282
|
+
name=MetricType.OBJECT_COUNT_NAME,
|
|
283
|
+
labels=[
|
|
284
|
+
ocean.metrics.current_resource_kind(),
|
|
285
|
+
MetricPhase.LOAD,
|
|
286
|
+
MetricPhase.LoadResult.FAILED,
|
|
287
|
+
],
|
|
288
|
+
value=1,
|
|
289
|
+
)
|
|
290
|
+
error = error_entities[entity_index]
|
|
291
|
+
if (
|
|
292
|
+
error.get("identifier") == "unknown"
|
|
293
|
+
): # when using the search identifier we might not have an actual identifier
|
|
294
|
+
batch_results.append((None, reduced_entity))
|
|
295
|
+
else:
|
|
296
|
+
batch_results.append((False, reduced_entity))
|
|
297
|
+
else:
|
|
298
|
+
batch_results.append((False, reduced_entity))
|
|
299
|
+
|
|
300
|
+
return batch_results
|
|
301
|
+
|
|
302
|
+
async def _upsert_entities_batch_individually(
|
|
303
|
+
self,
|
|
304
|
+
entities: list[Entity],
|
|
305
|
+
request_options: RequestOptions,
|
|
306
|
+
user_agent_type: UserAgentType | None = None,
|
|
307
|
+
should_raise: bool = True,
|
|
308
|
+
) -> list[tuple[bool, Entity]]:
|
|
309
|
+
entities_results: list[tuple[bool, Entity]] = []
|
|
310
|
+
modified_entities_results = await asyncio.gather(
|
|
311
|
+
*(
|
|
312
|
+
self.upsert_entity(
|
|
313
|
+
entity,
|
|
314
|
+
request_options,
|
|
315
|
+
user_agent_type,
|
|
316
|
+
should_raise=should_raise,
|
|
317
|
+
)
|
|
318
|
+
for entity in entities
|
|
319
|
+
),
|
|
320
|
+
return_exceptions=True,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
for original_entity, single_result in zip(entities, modified_entities_results):
|
|
324
|
+
if isinstance(single_result, Exception) and should_raise:
|
|
325
|
+
raise single_result
|
|
326
|
+
elif isinstance(single_result, Entity):
|
|
327
|
+
entities_results.append((True, single_result))
|
|
328
|
+
elif single_result is False:
|
|
329
|
+
entities_results.append((False, original_entity))
|
|
330
|
+
|
|
331
|
+
return entities_results
|
|
332
|
+
|
|
333
|
+
async def upsert_entities_in_batches(
|
|
334
|
+
self,
|
|
335
|
+
entities: list[Entity],
|
|
336
|
+
request_options: RequestOptions,
|
|
337
|
+
user_agent_type: UserAgentType | None = None,
|
|
338
|
+
should_raise: bool = True,
|
|
339
|
+
) -> list[tuple[bool, Entity]]:
|
|
340
|
+
"""
|
|
341
|
+
This function upserts a list of entities into Port in batches.
|
|
342
|
+
The batch size is calculated based on both the number of entities and their size.
|
|
343
|
+
Batches are processed in parallel using asyncio.gather, with concurrency controlled by the semaphore.
|
|
344
|
+
|
|
345
|
+
:param entities: A list of Entities to be upserted
|
|
346
|
+
:param request_options: A dictionary specifying how to upsert the entity
|
|
347
|
+
:param user_agent_type: a UserAgentType specifying who is preforming the action
|
|
348
|
+
:param should_raise: A boolean specifying whether the error should be raised or handled silently
|
|
349
|
+
:return: A list of tuples where each tuple contains:
|
|
350
|
+
- First value: True if entity was created successfully, False if there was an error
|
|
351
|
+
- Second value: The reduced entity with updated identifier (if successful) or the original entity (if failed)
|
|
352
|
+
"""
|
|
353
|
+
entities_results: list[tuple[bool, Entity]] = []
|
|
354
|
+
blueprint = entities[0].blueprint
|
|
355
|
+
|
|
356
|
+
if ocean.app.is_saas():
|
|
357
|
+
bulk_size = self.calculate_entities_batch_size(entities)
|
|
358
|
+
bulks = [
|
|
359
|
+
entities[i : i + bulk_size] for i in range(0, len(entities), bulk_size)
|
|
360
|
+
]
|
|
361
|
+
|
|
362
|
+
bulk_results = await asyncio.gather(
|
|
363
|
+
*(
|
|
364
|
+
self.upsert_entities_bulk(
|
|
365
|
+
blueprint,
|
|
366
|
+
bulk,
|
|
367
|
+
request_options,
|
|
368
|
+
user_agent_type,
|
|
369
|
+
should_raise=should_raise,
|
|
370
|
+
)
|
|
371
|
+
for bulk in bulks
|
|
372
|
+
),
|
|
373
|
+
return_exceptions=True,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
for bulk, bulk_result in zip(bulks, bulk_results):
|
|
377
|
+
if isinstance(bulk_result, httpx.HTTPStatusError) or isinstance(
|
|
378
|
+
bulk_result, Exception
|
|
379
|
+
):
|
|
380
|
+
if should_raise:
|
|
381
|
+
raise bulk_result
|
|
382
|
+
# If should_raise is False, retry batch in sequential order as a fallback only for 413 errors
|
|
383
|
+
if (
|
|
384
|
+
isinstance(bulk_result, httpx.HTTPStatusError)
|
|
385
|
+
and bulk_result.response.status_code == 413
|
|
386
|
+
):
|
|
387
|
+
individual_upsert_results = (
|
|
388
|
+
await self._upsert_entities_batch_individually(
|
|
389
|
+
bulk, request_options, user_agent_type, should_raise
|
|
390
|
+
)
|
|
391
|
+
)
|
|
392
|
+
entities_results.extend(individual_upsert_results)
|
|
393
|
+
else:
|
|
394
|
+
# For other errors, mark all entities in the batch as failed
|
|
395
|
+
for entity in bulk:
|
|
396
|
+
failed_result: tuple[bool, Entity] = (
|
|
397
|
+
False,
|
|
398
|
+
self._reduce_entity(entity),
|
|
399
|
+
)
|
|
400
|
+
entities_results.append(failed_result)
|
|
401
|
+
elif isinstance(bulk_result, list):
|
|
402
|
+
for status, entity in bulk_result:
|
|
403
|
+
if (
|
|
404
|
+
status is not None
|
|
405
|
+
): # when using the search identifier we might not have an actual identifier
|
|
406
|
+
bulk_result_tuple: tuple[bool, Entity] = (
|
|
407
|
+
bool(status),
|
|
408
|
+
entity,
|
|
409
|
+
)
|
|
410
|
+
entities_results.append(bulk_result_tuple)
|
|
411
|
+
else:
|
|
412
|
+
individual_upsert_results = await self._upsert_entities_batch_individually(
|
|
413
|
+
entities, request_options, user_agent_type, should_raise
|
|
414
|
+
)
|
|
415
|
+
entities_results.extend(individual_upsert_results)
|
|
416
|
+
|
|
417
|
+
return entities_results
|
|
418
|
+
|
|
419
|
+
async def delete_entity(
|
|
420
|
+
self,
|
|
421
|
+
entity: Entity,
|
|
422
|
+
request_options: RequestOptions,
|
|
423
|
+
user_agent_type: UserAgentType | None = None,
|
|
424
|
+
should_raise: bool = True,
|
|
425
|
+
) -> None:
|
|
426
|
+
async with self.semaphore:
|
|
427
|
+
logger.info(
|
|
428
|
+
f"Delete entity: {entity.identifier} of blueprint: {entity.blueprint}"
|
|
429
|
+
)
|
|
430
|
+
response = await self.client.delete(
|
|
431
|
+
f"{self.auth.api_url}/blueprints/{entity.blueprint}/entities/{quote_plus(entity.identifier)}",
|
|
432
|
+
headers=await self.auth.headers(user_agent_type),
|
|
433
|
+
params={
|
|
434
|
+
"delete_dependents": str(
|
|
435
|
+
request_options["delete_dependent_entities"]
|
|
436
|
+
).lower()
|
|
437
|
+
},
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
if response.is_error:
|
|
441
|
+
if response.status_code == 404:
|
|
442
|
+
logger.info(
|
|
443
|
+
f"Failed to delete entity: {entity.identifier} of blueprint: {entity.blueprint},"
|
|
444
|
+
f" as it was already deleted from port"
|
|
445
|
+
)
|
|
446
|
+
return
|
|
447
|
+
logger.error(
|
|
448
|
+
f"Error deleting "
|
|
449
|
+
f"entity: {entity.identifier} of "
|
|
450
|
+
f"blueprint: {entity.blueprint}"
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
handle_port_status_code(response, should_raise)
|
|
454
|
+
|
|
455
|
+
async def batch_delete_entities(
|
|
456
|
+
self,
|
|
457
|
+
entities: list[Entity],
|
|
458
|
+
request_options: RequestOptions,
|
|
459
|
+
user_agent_type: UserAgentType | None = None,
|
|
460
|
+
should_raise: bool = True,
|
|
461
|
+
) -> None:
|
|
462
|
+
await asyncio.gather(
|
|
463
|
+
*(
|
|
464
|
+
self.delete_entity(
|
|
465
|
+
entity,
|
|
466
|
+
request_options,
|
|
467
|
+
user_agent_type,
|
|
468
|
+
should_raise=should_raise,
|
|
469
|
+
)
|
|
470
|
+
for entity in entities
|
|
471
|
+
),
|
|
472
|
+
return_exceptions=True,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
async def search_entities(
|
|
476
|
+
self,
|
|
477
|
+
user_agent_type: UserAgentType,
|
|
478
|
+
query: dict[Any, Any] | None = None,
|
|
479
|
+
parameters_to_include: list[str] | None = None,
|
|
480
|
+
) -> list[Entity]:
|
|
481
|
+
default_query = {
|
|
482
|
+
"combinator": "and",
|
|
483
|
+
"rules": [
|
|
484
|
+
{
|
|
485
|
+
"property": "$datasource",
|
|
486
|
+
"operator": "contains",
|
|
487
|
+
"value": f"port-ocean/{self.auth.integration_type}/",
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
"property": "$datasource",
|
|
491
|
+
"operator": "contains",
|
|
492
|
+
"value": f"/{self.auth.integration_identifier}/{user_agent_type.value}",
|
|
493
|
+
},
|
|
494
|
+
],
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if query is None:
|
|
498
|
+
query = default_query
|
|
499
|
+
elif query.get("rules"):
|
|
500
|
+
query["rules"].append(default_query)
|
|
501
|
+
|
|
502
|
+
logger.info(f"Searching entities with query {query}")
|
|
503
|
+
response = await self.client.post(
|
|
504
|
+
f"{self.auth.api_url}/entities/search",
|
|
505
|
+
json=query,
|
|
506
|
+
headers=await self.auth.headers(user_agent_type),
|
|
507
|
+
params={
|
|
508
|
+
"exclude_calculated_properties": "true",
|
|
509
|
+
"include": parameters_to_include or ["blueprint", "identifier"],
|
|
510
|
+
},
|
|
511
|
+
extensions={"retryable": True},
|
|
512
|
+
)
|
|
513
|
+
handle_port_status_code(response)
|
|
514
|
+
return [Entity.parse_obj(result) for result in response.json()["entities"]]
|
|
515
|
+
|
|
516
|
+
async def search_batch_entities(
|
|
517
|
+
self, user_agent_type: UserAgentType, entities_to_search: list[Entity]
|
|
518
|
+
) -> list[Entity]:
|
|
519
|
+
search_rules = []
|
|
520
|
+
for entity in entities_to_search:
|
|
521
|
+
search_rules.append(
|
|
522
|
+
{
|
|
523
|
+
"combinator": "and",
|
|
524
|
+
"rules": [
|
|
525
|
+
{
|
|
526
|
+
"property": "$identifier",
|
|
527
|
+
"operator": "=",
|
|
528
|
+
"value": entity.identifier,
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
"property": "$blueprint",
|
|
532
|
+
"operator": "=",
|
|
533
|
+
"value": entity.blueprint,
|
|
534
|
+
},
|
|
535
|
+
],
|
|
536
|
+
}
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
return await self.search_entities(
|
|
540
|
+
user_agent_type,
|
|
541
|
+
{
|
|
542
|
+
"combinator": "and",
|
|
543
|
+
"rules": [{"combinator": "or", "rules": search_rules}],
|
|
544
|
+
},
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
@staticmethod
|
|
548
|
+
def _reduce_entity(entity: Entity) -> Entity:
|
|
549
|
+
"""
|
|
550
|
+
Reduces an entity to only keep identifier, blueprint and processed relations.
|
|
551
|
+
This helps save memory by removing unnecessary data.
|
|
552
|
+
|
|
553
|
+
Args:
|
|
554
|
+
entity: The entity to reduce
|
|
555
|
+
|
|
556
|
+
Returns:
|
|
557
|
+
Entity: A new entity with only the essential data
|
|
558
|
+
"""
|
|
559
|
+
reduced_entity = Entity(
|
|
560
|
+
identifier=entity.identifier, blueprint=entity.blueprint
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
# Turning dict typed relations (raw search relations) is required
|
|
564
|
+
# for us to be able to successfully calculate the participation related entities
|
|
565
|
+
# and ignore the ones that don't as they weren't upserted
|
|
566
|
+
reduced_entity.relations = {
|
|
567
|
+
key: None if isinstance(relation, dict) else relation
|
|
568
|
+
for key, relation in entity.relations.items()
|
|
569
|
+
}
|
|
570
|
+
return reduced_entity
|
|
@@ -101,6 +101,10 @@ class IntegrationConfiguration(BaseOceanSettings, extra=Extra.allow):
|
|
|
101
101
|
caching_storage_mode: Optional[CachingStorageMode] = Field(default=None)
|
|
102
102
|
process_execution_mode: Optional[ProcessExecutionMode] = Field(default=None)
|
|
103
103
|
|
|
104
|
+
upsert_entities_batch_max_length: int = 20
|
|
105
|
+
upsert_entities_batch_max_size_in_bytes: int = 1024 * 1024
|
|
106
|
+
bulk_upserts_enabled: bool = False
|
|
107
|
+
|
|
104
108
|
@validator("metrics", pre=True)
|
|
105
109
|
def validate_metrics(cls, v: Any) -> MetricsSettings | dict[str, Any] | None:
|
|
106
110
|
if v is None:
|
|
@@ -127,7 +127,7 @@ class HttpEntitiesStateApplier(BaseEntitiesStateApplier):
|
|
|
127
127
|
modified_entities: list[Entity] = []
|
|
128
128
|
upserted_entities: list[tuple[bool, Entity]] = []
|
|
129
129
|
|
|
130
|
-
upserted_entities = await self.context.port_client.
|
|
130
|
+
upserted_entities = await self.context.port_client.upsert_entities_in_batches(
|
|
131
131
|
entities,
|
|
132
132
|
event.port_app_config.get_port_request_options(),
|
|
133
133
|
user_agent_type,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from dataclasses import dataclass, field
|
|
2
2
|
from enum import Enum, StrEnum
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any, TypedDict
|
|
4
4
|
|
|
5
5
|
from pydantic import BaseModel
|
|
6
6
|
from pydantic.fields import Field
|
|
@@ -67,6 +67,25 @@ class Entity(BaseModel):
|
|
|
67
67
|
)
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
class EntityBulkResult(TypedDict):
|
|
71
|
+
identifier: str
|
|
72
|
+
index: int
|
|
73
|
+
created: bool
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class EntityBulkError(TypedDict):
|
|
77
|
+
identifier: str
|
|
78
|
+
index: int
|
|
79
|
+
statusCode: int
|
|
80
|
+
error: str
|
|
81
|
+
message: str
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class BulkUpsertResponse(TypedDict):
|
|
85
|
+
entities: list[EntityBulkResult]
|
|
86
|
+
errors: list[EntityBulkError]
|
|
87
|
+
|
|
88
|
+
|
|
70
89
|
class BlueprintRelation(BaseModel):
|
|
71
90
|
many: bool
|
|
72
91
|
required: bool
|