ingestr 0.13.29__tar.gz → 0.13.31__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 ingestr might be problematic. Click here for more details.
- {ingestr-0.13.29 → ingestr-0.13.31}/.gitignore +2 -1
- {ingestr-0.13.29 → ingestr-0.13.31}/Makefile +2 -2
- {ingestr-0.13.29 → ingestr-0.13.31}/PKG-INFO +2 -2
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/.vitepress/config.mjs +3 -0
- ingestr-0.13.31/docs/supported-sources/appsflyer.md +58 -0
- ingestr-0.13.31/ingestr/src/appsflyer/__init__.py +325 -0
- ingestr-0.13.31/ingestr/src/appsflyer/client.py +111 -0
- ingestr-0.13.31/ingestr/src/buildinfo.py +1 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/hubspot/__init__.py +18 -4
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/hubspot/helpers.py +17 -5
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/sources.py +33 -19
- {ingestr-0.13.29 → ingestr-0.13.31}/requirements.txt +1 -1
- ingestr-0.13.31/test.env.template +4 -0
- ingestr-0.13.29/docs/supported-sources/appsflyer.md +0 -33
- ingestr-0.13.29/ingestr/src/appsflyer/_init_.py +0 -24
- ingestr-0.13.29/ingestr/src/appsflyer/client.py +0 -106
- ingestr-0.13.29/ingestr/src/buildinfo.py +0 -1
- {ingestr-0.13.29 → ingestr-0.13.31}/.dockerignore +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/.githooks/pre-commit-hook.sh +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/.github/workflows/deploy-docs.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/.github/workflows/release.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/.github/workflows/secrets-scan.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/.github/workflows/tests.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/.gitleaksignore +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/.python-version +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/.vale.ini +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/Dockerfile +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/LICENSE.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/README.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/.vitepress/theme/custom.css +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/.vitepress/theme/index.js +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/commands/example-uris.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/commands/ingest.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/getting-started/core-concepts.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/getting-started/incremental-loading.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/getting-started/quickstart.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/getting-started/telemetry.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/index.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/media/applovin_max.png +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/media/athena.png +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/media/clickhouse_img.png +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/media/github.png +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/media/googleanalytics.png +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/media/kinesis.bigquery.png +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/media/linkedin_ads.png +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/media/personio.png +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/media/personio_duckdb.png +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/media/pipedrive.png +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/media/stripe_postgres.png +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/media/tiktok.png +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/adjust.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/airtable.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/applovin.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/applovin_max.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/appstore.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/asana.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/athena.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/bigquery.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/chess.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/clickhouse.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/csv.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/custom_queries.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/databricks.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/db2.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/duckdb.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/dynamodb.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/facebook-ads.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/frankfurter.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/gcs.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/github.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/google-ads.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/google_analytics.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/gorgias.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/gsheets.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/hubspot.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/kafka.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/kinesis.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/klaviyo.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/linkedin_ads.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/mongodb.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/mssql.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/mysql.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/notion.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/oracle.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/personio.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/pipedrive.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/postgres.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/redshift.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/s3.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/salesforce.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/sap-hana.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/shopify.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/slack.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/snowflake.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/sqlite.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/stripe.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/tiktok-ads.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/supported-sources/zendesk.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/tutorials/load-kinesis-bigquery.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/tutorials/load-personio-duckdb.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/docs/tutorials/load-stripe-postgres.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/conftest.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/main.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/.gitignore +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/adjust/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/adjust/adjust_helpers.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/airtable/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/applovin/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/applovin_max/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/appstore/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/appstore/client.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/appstore/errors.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/appstore/models.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/appstore/resources.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/arrow/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/asana_source/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/asana_source/helpers.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/asana_source/settings.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/blob.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/chess/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/chess/helpers.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/chess/settings.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/destinations.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/dynamodb/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/errors.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/facebook_ads/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/facebook_ads/exceptions.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/facebook_ads/helpers.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/facebook_ads/settings.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/factory.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/filesystem/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/filesystem/helpers.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/filesystem/readers.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/filters.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/frankfurter/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/frankfurter/helpers.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/github/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/github/helpers.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/github/queries.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/github/settings.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/google_ads/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/google_ads/field.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/google_ads/metrics.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/google_ads/predicates.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/google_ads/reports.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/google_analytics/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/google_analytics/helpers.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/google_sheets/README.md +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/google_sheets/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/google_sheets/helpers/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/google_sheets/helpers/api_calls.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/google_sheets/helpers/data_processing.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/gorgias/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/gorgias/helpers.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/hubspot/settings.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/kafka/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/kafka/helpers.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/kinesis/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/kinesis/helpers.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/klaviyo/_init_.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/klaviyo/client.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/klaviyo/helpers.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/linkedin_ads/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/linkedin_ads/dimension_time_enum.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/linkedin_ads/helpers.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/loader.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/mongodb/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/mongodb/helpers.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/notion/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/notion/helpers/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/notion/helpers/client.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/notion/helpers/database.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/notion/settings.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/partition.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/personio/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/personio/helpers.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/pipedrive/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/pipedrive/helpers/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/pipedrive/helpers/custom_fields_munger.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/pipedrive/helpers/pages.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/pipedrive/settings.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/pipedrive/typing.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/resource.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/salesforce/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/salesforce/helpers.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/shopify/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/shopify/exceptions.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/shopify/helpers.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/shopify/settings.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/slack/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/slack/helpers.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/slack/settings.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/sql_database/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/sql_database/callbacks.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/stripe_analytics/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/stripe_analytics/helpers.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/stripe_analytics/settings.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/table_definition.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/telemetry/event.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/testdata/fakebqcredentials.json +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/tiktok_ads/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/tiktok_ads/tiktok_helpers.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/time.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/version.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/zendesk/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/zendesk/helpers/__init__.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/zendesk/helpers/api_helpers.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/zendesk/helpers/credentials.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/zendesk/helpers/talk_api.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/src/zendesk/settings.py +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/testdata/.gitignore +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/testdata/create_replace.csv +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/testdata/delete_insert_expected.csv +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/testdata/delete_insert_part1.csv +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/testdata/delete_insert_part2.csv +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/testdata/merge_expected.csv +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/testdata/merge_part1.csv +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/ingestr/testdata/merge_part2.csv +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/package-lock.json +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/package.json +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/pyproject.toml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/requirements-dev.txt +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/requirements.in +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/requirements_arm64.txt +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/resources/demo.gif +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/resources/demo.tape +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/resources/ingestr.svg +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/AMPM.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/Acronyms.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/Colons.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/Contractions.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/DateFormat.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/Ellipses.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/EmDash.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/Exclamation.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/FirstPerson.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/Gender.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/GenderBias.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/HeadingPunctuation.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/Headings.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/Latin.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/LyHyphens.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/OptionalPlurals.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/Ordinal.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/OxfordComma.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/Parens.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/Passive.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/Periods.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/Quotes.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/Ranges.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/Semicolons.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/Slang.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/Spacing.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/Spelling.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/Units.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/We.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/Will.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/WordList.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/meta.json +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/Google/vocab.txt +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/bruin/Ingestr.yml +0 -0
- {ingestr-0.13.29 → ingestr-0.13.31}/styles/config/vocabularies/bruin/accept.txt +0 -0
|
@@ -22,13 +22,13 @@ deps-ci:
|
|
|
22
22
|
uv pip install --system -r requirements-dev.txt
|
|
23
23
|
|
|
24
24
|
test-ci:
|
|
25
|
-
TESTCONTAINERS_RYUK_DISABLED=true pytest -n auto -x -rP -vv --tb=short --durations=10 --cov=ingestr --no-cov-on-fail
|
|
25
|
+
set -a; source test.env; set +a; TESTCONTAINERS_RYUK_DISABLED=true pytest -n auto -x -rP -vv --tb=short --durations=10 --cov=ingestr --no-cov-on-fail
|
|
26
26
|
|
|
27
27
|
test : venv lock-deps
|
|
28
28
|
. venv/bin/activate; $(MAKE) test-ci
|
|
29
29
|
|
|
30
30
|
test-specific: venv lock-deps
|
|
31
|
-
. venv/bin/activate; TESTCONTAINERS_RYUK_DISABLED=true pytest -n auto -rP -vv --tb=short --capture=no -k $(test)
|
|
31
|
+
. venv/bin/activate; set -a; source test.env; set +a; TESTCONTAINERS_RYUK_DISABLED=true pytest -n auto -rP -vv --tb=short --capture=no -k $(test)
|
|
32
32
|
|
|
33
33
|
lint-ci:
|
|
34
34
|
ruff format ingestr && ruff check ingestr --fix
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ingestr
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.31
|
|
4
4
|
Summary: ingestr is a command-line application that ingests data from various sources and stores them in any database.
|
|
5
5
|
Project-URL: Homepage, https://github.com/bruin-data/ingestr
|
|
6
6
|
Project-URL: Issues, https://github.com/bruin-data/ingestr/issues
|
|
@@ -74,7 +74,7 @@ Requires-Dist: google-cloud-storage==3.1.0
|
|
|
74
74
|
Requires-Dist: google-crc32c==1.6.0
|
|
75
75
|
Requires-Dist: google-resumable-media==2.7.2
|
|
76
76
|
Requires-Dist: googleapis-common-protos==1.69.0
|
|
77
|
-
Requires-Dist: greenlet==3.2.
|
|
77
|
+
Requires-Dist: greenlet==3.2.1
|
|
78
78
|
Requires-Dist: grpcio-status==1.62.3
|
|
79
79
|
Requires-Dist: grpcio==1.70.0
|
|
80
80
|
Requires-Dist: hdbcli==2.23.27
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# AppsFlyer
|
|
2
|
+
|
|
3
|
+
[AppsFlyer](https://www.appsflyer.com/) is a mobile marketing analytics and attribution platform that helps businesses track, measure, and optimize their app marketing efforts across various channels.
|
|
4
|
+
|
|
5
|
+
ingestr supports AppsFlyer as a source.
|
|
6
|
+
|
|
7
|
+
> [!WARNING]
|
|
8
|
+
> AppsFlyer uses different names for input dimensions vs their name in the output schema. For instance, in order to obtain campaign information, you need to use the `c` dimension; however, in the output schema, the resulting column will be called `campaign`.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## URI Format
|
|
12
|
+
|
|
13
|
+
The URI format for AppsFlyer is as follows:
|
|
14
|
+
|
|
15
|
+
```plaintext
|
|
16
|
+
appsflyer://?api_key=<api-key>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
An API token is required to retrieve reports from the AppsFlyer API, please [follow the guide to obtain an API key](https://support.appsflyer.com/hc/en-us/articles/360004562377-Managing-AppsFlyer-tokens)
|
|
20
|
+
|
|
21
|
+
Let's say your API key is `ey123`, here's a sample command that will copy the data from AppsFlyer into a DuckDB database:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
ingestr ingest
|
|
25
|
+
--source-uri 'appsflyer://?api_key=ey123'
|
|
26
|
+
--source-table 'campaigns'
|
|
27
|
+
--dest-uri duckdb:///appsflyer.duckdb
|
|
28
|
+
--dest-table 'appsflyer.output'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The result of this command will be a table in the `appsflyer.duckdb` database.
|
|
32
|
+
|
|
33
|
+
## Supported Tables
|
|
34
|
+
|
|
35
|
+
ingestr integrates with the [Master Report API](https://dev.appsflyer.com/hc/reference/master_api_get) of AppsFlyer, which allows you to retrieve data for the following tables:
|
|
36
|
+
|
|
37
|
+
- `campaigns`: Retrieves data for campaigns, detailing the app's costs, loyal users, total installs, and revenue over multiple days.
|
|
38
|
+
- `creatives`: Retrieves data for a creative asset, including revenue and cost.
|
|
39
|
+
- `custom:<dimensions>:<metrics>`: Retrieves data for custom tables, which can be specified by the user.
|
|
40
|
+
|
|
41
|
+
Use these as `--source-table` parameter in the `ingestr ingest` command.
|
|
42
|
+
|
|
43
|
+
### Custom Tables
|
|
44
|
+
|
|
45
|
+
You can also ingest custom tables by providing a list of dimensions and metrics.
|
|
46
|
+
|
|
47
|
+
The table format is as follows:
|
|
48
|
+
|
|
49
|
+
```plaintext
|
|
50
|
+
custom:<dimension1>,<dimension2>,<metric1>,<metric2>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This will automatically generate a table with the dimensions and metrics you provided.
|
|
54
|
+
|
|
55
|
+
For custom tables, ingestr will use the given dimensions as the primary key to deduplicate the data.
|
|
56
|
+
|
|
57
|
+
> [!NOTE]
|
|
58
|
+
> ingestr will add `install_time` as the primary key to the table by default if it is not provided as one of the dimensions.
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
from typing import Iterable
|
|
2
|
+
|
|
3
|
+
import dlt
|
|
4
|
+
import pendulum
|
|
5
|
+
from dlt.common.typing import TDataItem
|
|
6
|
+
from dlt.sources import DltResource
|
|
7
|
+
|
|
8
|
+
from ingestr.src.appsflyer.client import AppsflyerClient
|
|
9
|
+
|
|
10
|
+
DIMENSION_RESPONSE_MAPPING = {
|
|
11
|
+
"c": "campaign",
|
|
12
|
+
"af_adset_id": "adset_id",
|
|
13
|
+
"af_adset": "adset",
|
|
14
|
+
"af_ad_id": "ad_id",
|
|
15
|
+
}
|
|
16
|
+
HINTS = {
|
|
17
|
+
"app_id": {
|
|
18
|
+
"data_type": "text",
|
|
19
|
+
"nullable": False,
|
|
20
|
+
},
|
|
21
|
+
"campaign": {
|
|
22
|
+
"data_type": "text",
|
|
23
|
+
"nullable": False,
|
|
24
|
+
},
|
|
25
|
+
"geo": {
|
|
26
|
+
"data_type": "text",
|
|
27
|
+
"nullable": False,
|
|
28
|
+
},
|
|
29
|
+
"cost": {
|
|
30
|
+
"data_type": "decimal",
|
|
31
|
+
"precision": 30,
|
|
32
|
+
"scale": 5,
|
|
33
|
+
"nullable": False,
|
|
34
|
+
},
|
|
35
|
+
"clicks": {
|
|
36
|
+
"data_type": "bigint",
|
|
37
|
+
"nullable": False,
|
|
38
|
+
},
|
|
39
|
+
"impressions": {
|
|
40
|
+
"data_type": "bigint",
|
|
41
|
+
"nullable": False,
|
|
42
|
+
},
|
|
43
|
+
"average_ecpi": {
|
|
44
|
+
"data_type": "decimal",
|
|
45
|
+
"precision": 30,
|
|
46
|
+
"scale": 5,
|
|
47
|
+
"nullable": False,
|
|
48
|
+
},
|
|
49
|
+
"installs": {
|
|
50
|
+
"data_type": "bigint",
|
|
51
|
+
"nullable": False,
|
|
52
|
+
},
|
|
53
|
+
"retention_day_7": {
|
|
54
|
+
"data_type": "decimal",
|
|
55
|
+
"precision": 30,
|
|
56
|
+
"scale": 5,
|
|
57
|
+
"nullable": False,
|
|
58
|
+
},
|
|
59
|
+
"retention_day_14": {
|
|
60
|
+
"data_type": "decimal",
|
|
61
|
+
"precision": 30,
|
|
62
|
+
"scale": 5,
|
|
63
|
+
"nullable": False,
|
|
64
|
+
},
|
|
65
|
+
"cohort_day_1_revenue_per_user": {
|
|
66
|
+
"data_type": "decimal",
|
|
67
|
+
"precision": 30,
|
|
68
|
+
"scale": 5,
|
|
69
|
+
"nullable": True,
|
|
70
|
+
},
|
|
71
|
+
"cohort_day_1_total_revenue_per_user": {
|
|
72
|
+
"data_type": "decimal",
|
|
73
|
+
"precision": 30,
|
|
74
|
+
"scale": 5,
|
|
75
|
+
"nullable": True,
|
|
76
|
+
},
|
|
77
|
+
"cohort_day_3_revenue_per_user": {
|
|
78
|
+
"data_type": "decimal",
|
|
79
|
+
"precision": 30,
|
|
80
|
+
"scale": 5,
|
|
81
|
+
"nullable": True,
|
|
82
|
+
},
|
|
83
|
+
"cohort_day_3_total_revenue_per_user": {
|
|
84
|
+
"data_type": "decimal",
|
|
85
|
+
"precision": 30,
|
|
86
|
+
"scale": 5,
|
|
87
|
+
"nullable": True,
|
|
88
|
+
},
|
|
89
|
+
"cohort_day_7_revenue_per_user": {
|
|
90
|
+
"data_type": "decimal",
|
|
91
|
+
"precision": 30,
|
|
92
|
+
"scale": 5,
|
|
93
|
+
"nullable": True,
|
|
94
|
+
},
|
|
95
|
+
"cohort_day_7_total_revenue_per_user": {
|
|
96
|
+
"data_type": "decimal",
|
|
97
|
+
"precision": 30,
|
|
98
|
+
"scale": 5,
|
|
99
|
+
"nullable": True,
|
|
100
|
+
},
|
|
101
|
+
"cohort_day_14_revenue_per_user": {
|
|
102
|
+
"data_type": "decimal",
|
|
103
|
+
"precision": 30,
|
|
104
|
+
"scale": 5,
|
|
105
|
+
"nullable": True,
|
|
106
|
+
},
|
|
107
|
+
"cohort_day_14_total_revenue_per_user": {
|
|
108
|
+
"data_type": "decimal",
|
|
109
|
+
"precision": 30,
|
|
110
|
+
"scale": 5,
|
|
111
|
+
"nullable": True,
|
|
112
|
+
},
|
|
113
|
+
"cohort_day_21_revenue_per_user": {
|
|
114
|
+
"data_type": "decimal",
|
|
115
|
+
"precision": 30,
|
|
116
|
+
"scale": 5,
|
|
117
|
+
"nullable": True,
|
|
118
|
+
},
|
|
119
|
+
"cohort_day_21_total_revenue_per_user": {
|
|
120
|
+
"data_type": "decimal",
|
|
121
|
+
"precision": 30,
|
|
122
|
+
"scale": 5,
|
|
123
|
+
"nullable": True,
|
|
124
|
+
},
|
|
125
|
+
"install_time": {
|
|
126
|
+
"data_type": "date",
|
|
127
|
+
"nullable": False,
|
|
128
|
+
},
|
|
129
|
+
"loyal_users": {
|
|
130
|
+
"data_type": "bigint",
|
|
131
|
+
"nullable": False,
|
|
132
|
+
},
|
|
133
|
+
"revenue": {
|
|
134
|
+
"data_type": "decimal",
|
|
135
|
+
"precision": 30,
|
|
136
|
+
"scale": 5,
|
|
137
|
+
"nullable": True,
|
|
138
|
+
},
|
|
139
|
+
"roi": {
|
|
140
|
+
"data_type": "decimal",
|
|
141
|
+
"precision": 30,
|
|
142
|
+
"scale": 5,
|
|
143
|
+
"nullable": True,
|
|
144
|
+
},
|
|
145
|
+
"uninstalls": {
|
|
146
|
+
"data_type": "bigint",
|
|
147
|
+
"nullable": True,
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
CAMPAIGNS_DIMENSIONS = ["c", "geo", "app_id", "install_time"]
|
|
152
|
+
CAMPAIGNS_METRICS = [
|
|
153
|
+
"average_ecpi",
|
|
154
|
+
"clicks",
|
|
155
|
+
"cohort_day_1_revenue_per_user",
|
|
156
|
+
"cohort_day_1_total_revenue_per_user",
|
|
157
|
+
"cohort_day_14_revenue_per_user",
|
|
158
|
+
"cohort_day_14_total_revenue_per_user",
|
|
159
|
+
"cohort_day_21_revenue_per_user",
|
|
160
|
+
"cohort_day_21_total_revenue_per_user",
|
|
161
|
+
"cohort_day_3_revenue_per_user",
|
|
162
|
+
"cohort_day_3_total_revenue_per_user",
|
|
163
|
+
"cohort_day_7_revenue_per_user",
|
|
164
|
+
"cohort_day_7_total_revenue_per_user",
|
|
165
|
+
"cost",
|
|
166
|
+
"impressions",
|
|
167
|
+
"installs",
|
|
168
|
+
"loyal_users",
|
|
169
|
+
"retention_day_7",
|
|
170
|
+
"revenue",
|
|
171
|
+
"roi",
|
|
172
|
+
"uninstalls",
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
CREATIVES_DIMENSIONS = [
|
|
176
|
+
"c",
|
|
177
|
+
"geo",
|
|
178
|
+
"app_id",
|
|
179
|
+
"install_time",
|
|
180
|
+
"af_adset_id",
|
|
181
|
+
"af_adset",
|
|
182
|
+
"af_ad_id",
|
|
183
|
+
]
|
|
184
|
+
CREATIVES_METRICS = [
|
|
185
|
+
"impressions",
|
|
186
|
+
"clicks",
|
|
187
|
+
"installs",
|
|
188
|
+
"cost",
|
|
189
|
+
"revenue",
|
|
190
|
+
"average_ecpi",
|
|
191
|
+
"loyal_users",
|
|
192
|
+
"uninstalls",
|
|
193
|
+
"roi",
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dlt.source(max_table_nesting=0)
|
|
198
|
+
def appsflyer_source(
|
|
199
|
+
api_key: str,
|
|
200
|
+
start_date: str,
|
|
201
|
+
end_date: str,
|
|
202
|
+
dimensions: list[str],
|
|
203
|
+
metrics: list[str],
|
|
204
|
+
) -> Iterable[DltResource]:
|
|
205
|
+
client = AppsflyerClient(api_key)
|
|
206
|
+
|
|
207
|
+
@dlt.resource(
|
|
208
|
+
write_disposition="merge",
|
|
209
|
+
merge_key="install_time",
|
|
210
|
+
columns=make_hints(CAMPAIGNS_DIMENSIONS, CAMPAIGNS_METRICS),
|
|
211
|
+
)
|
|
212
|
+
def campaigns(
|
|
213
|
+
datetime=dlt.sources.incremental(
|
|
214
|
+
"install_time",
|
|
215
|
+
initial_value=(
|
|
216
|
+
start_date
|
|
217
|
+
if start_date
|
|
218
|
+
else pendulum.today().subtract(days=30).format("YYYY-MM-DD")
|
|
219
|
+
),
|
|
220
|
+
end_value=end_date,
|
|
221
|
+
range_end="closed",
|
|
222
|
+
range_start="closed",
|
|
223
|
+
),
|
|
224
|
+
) -> Iterable[TDataItem]:
|
|
225
|
+
end = (
|
|
226
|
+
datetime.end_value
|
|
227
|
+
if datetime.end_value
|
|
228
|
+
else pendulum.now().format("YYYY-MM-DD")
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
yield from client._fetch_data(
|
|
232
|
+
from_date=datetime.last_value,
|
|
233
|
+
to_date=end,
|
|
234
|
+
dimensions=CAMPAIGNS_DIMENSIONS,
|
|
235
|
+
metrics=CAMPAIGNS_METRICS,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
@dlt.resource(
|
|
239
|
+
write_disposition="merge",
|
|
240
|
+
merge_key="install_time",
|
|
241
|
+
columns=make_hints(CREATIVES_DIMENSIONS, CREATIVES_METRICS),
|
|
242
|
+
)
|
|
243
|
+
def creatives(
|
|
244
|
+
datetime=dlt.sources.incremental(
|
|
245
|
+
"install_time",
|
|
246
|
+
initial_value=(
|
|
247
|
+
start_date
|
|
248
|
+
if start_date
|
|
249
|
+
else pendulum.today().subtract(days=30).format("YYYY-MM-DD")
|
|
250
|
+
),
|
|
251
|
+
end_value=end_date,
|
|
252
|
+
range_end="closed",
|
|
253
|
+
range_start="closed",
|
|
254
|
+
),
|
|
255
|
+
) -> Iterable[TDataItem]:
|
|
256
|
+
end = (
|
|
257
|
+
datetime.end_value
|
|
258
|
+
if datetime.end_value
|
|
259
|
+
else pendulum.now().format("YYYY-MM-DD")
|
|
260
|
+
)
|
|
261
|
+
yield from client._fetch_data(
|
|
262
|
+
datetime.last_value,
|
|
263
|
+
end,
|
|
264
|
+
dimensions=CREATIVES_DIMENSIONS,
|
|
265
|
+
metrics=CREATIVES_METRICS,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
primary_keys = []
|
|
269
|
+
if "install_time" not in dimensions:
|
|
270
|
+
dimensions.append("install_time")
|
|
271
|
+
primary_keys.append("install_time")
|
|
272
|
+
|
|
273
|
+
for dimension in dimensions:
|
|
274
|
+
if dimension in DIMENSION_RESPONSE_MAPPING:
|
|
275
|
+
primary_keys.append(DIMENSION_RESPONSE_MAPPING[dimension])
|
|
276
|
+
else:
|
|
277
|
+
primary_keys.append(dimension)
|
|
278
|
+
|
|
279
|
+
@dlt.resource(
|
|
280
|
+
write_disposition="merge",
|
|
281
|
+
primary_key=primary_keys,
|
|
282
|
+
columns=make_hints(dimensions, metrics),
|
|
283
|
+
)
|
|
284
|
+
def custom(
|
|
285
|
+
datetime=dlt.sources.incremental(
|
|
286
|
+
"install_time",
|
|
287
|
+
initial_value=(
|
|
288
|
+
start_date
|
|
289
|
+
if start_date
|
|
290
|
+
else pendulum.today().subtract(days=30).format("YYYY-MM-DD")
|
|
291
|
+
),
|
|
292
|
+
end_value=end_date,
|
|
293
|
+
),
|
|
294
|
+
):
|
|
295
|
+
end = (
|
|
296
|
+
datetime.end_value
|
|
297
|
+
if datetime.end_value
|
|
298
|
+
else pendulum.now().format("YYYY-MM-DD")
|
|
299
|
+
)
|
|
300
|
+
res = client._fetch_data(
|
|
301
|
+
from_date=datetime.last_value,
|
|
302
|
+
to_date=end,
|
|
303
|
+
dimensions=dimensions,
|
|
304
|
+
metrics=metrics,
|
|
305
|
+
)
|
|
306
|
+
yield from res
|
|
307
|
+
|
|
308
|
+
return campaigns, creatives, custom
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def make_hints(dimensions: list[str], metrics: list[str]):
|
|
312
|
+
campaign_hints = {}
|
|
313
|
+
for dimension in dimensions:
|
|
314
|
+
resp_key = dimension
|
|
315
|
+
if dimension in DIMENSION_RESPONSE_MAPPING:
|
|
316
|
+
resp_key = DIMENSION_RESPONSE_MAPPING[dimension]
|
|
317
|
+
|
|
318
|
+
if resp_key in HINTS:
|
|
319
|
+
campaign_hints[resp_key] = HINTS[resp_key]
|
|
320
|
+
|
|
321
|
+
for metric in metrics:
|
|
322
|
+
if metric in HINTS:
|
|
323
|
+
campaign_hints[metric] = HINTS[metric]
|
|
324
|
+
|
|
325
|
+
return campaign_hints
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
from dlt.sources.helpers.requests import Client
|
|
5
|
+
from requests.exceptions import HTTPError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AppsflyerClient:
|
|
9
|
+
def __init__(self, api_key: str):
|
|
10
|
+
self.api_key = api_key
|
|
11
|
+
self.uri = "https://hq1.appsflyer.com/api/master-agg-data/v4/app/all"
|
|
12
|
+
|
|
13
|
+
def __get_headers(self):
|
|
14
|
+
return {
|
|
15
|
+
"Authorization": f"{self.api_key}",
|
|
16
|
+
"accept": "text/json",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
def _fetch_data(
|
|
20
|
+
self,
|
|
21
|
+
from_date: str,
|
|
22
|
+
to_date: str,
|
|
23
|
+
dimensions: list[str],
|
|
24
|
+
metrics: list[str],
|
|
25
|
+
maximum_rows=1000000,
|
|
26
|
+
):
|
|
27
|
+
excluded_metrics = exclude_metrics_for_date_range(metrics, from_date, to_date)
|
|
28
|
+
included_metrics = [
|
|
29
|
+
metric for metric in metrics if metric not in excluded_metrics
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
params = {
|
|
33
|
+
"from": from_date,
|
|
34
|
+
"to": to_date,
|
|
35
|
+
"groupings": ",".join(dimensions),
|
|
36
|
+
"kpis": ",".join(included_metrics),
|
|
37
|
+
"format": "json",
|
|
38
|
+
"maximum_rows": maximum_rows,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def retry_on_limit(
|
|
42
|
+
response: Optional[requests.Response], exception: Optional[BaseException]
|
|
43
|
+
) -> bool:
|
|
44
|
+
return (
|
|
45
|
+
isinstance(response, requests.Response) and response.status_code == 429
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
request_client = Client(
|
|
49
|
+
request_timeout=10.0,
|
|
50
|
+
raise_for_status=False,
|
|
51
|
+
retry_condition=retry_on_limit,
|
|
52
|
+
request_max_attempts=12,
|
|
53
|
+
request_backoff_factor=2,
|
|
54
|
+
).session
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
response = request_client.get(
|
|
58
|
+
url=self.uri, headers=self.__get_headers(), params=params
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if response.status_code == 200:
|
|
62
|
+
result = response.json()
|
|
63
|
+
yield standardize_keys(result, excluded_metrics)
|
|
64
|
+
else:
|
|
65
|
+
raise HTTPError(
|
|
66
|
+
f"Request failed with status code: {response.status_code}: {response.text}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
except requests.RequestException as e:
|
|
70
|
+
raise HTTPError(f"Request failed: {e}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def standardize_keys(data: list[dict], excluded_metrics: list[str]) -> list[dict]:
|
|
74
|
+
def fix_key(key: str) -> str:
|
|
75
|
+
return key.lower().replace("-", "").replace(" ", "_").replace(" ", "_")
|
|
76
|
+
|
|
77
|
+
standardized = []
|
|
78
|
+
for item in data:
|
|
79
|
+
standardized_item = {}
|
|
80
|
+
for key, value in item.items():
|
|
81
|
+
standardized_item[fix_key(key)] = value
|
|
82
|
+
|
|
83
|
+
for metric in excluded_metrics:
|
|
84
|
+
if metric not in standardized_item:
|
|
85
|
+
standardized_item[fix_key(metric)] = None
|
|
86
|
+
|
|
87
|
+
standardized.append(standardized_item)
|
|
88
|
+
|
|
89
|
+
return standardized
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def exclude_metrics_for_date_range(
|
|
93
|
+
metrics: list[str], from_date: str, to_date: str
|
|
94
|
+
) -> list[str]:
|
|
95
|
+
"""
|
|
96
|
+
Some of the cohort metrics are not available if there hasn't been enough time to have data for that cohort.
|
|
97
|
+
This means if you request data for yesterday with cohort day 7 metrics, you will get an error because 7 days hasn't passed yet.
|
|
98
|
+
One would expect the API to handle this gracefully, but it doesn't.
|
|
99
|
+
|
|
100
|
+
This function will exclude the metrics that are not available for the given date range.
|
|
101
|
+
"""
|
|
102
|
+
import pendulum
|
|
103
|
+
|
|
104
|
+
excluded_metrics = []
|
|
105
|
+
days_between_today_and_end = (pendulum.now() - pendulum.parse(to_date)).days # type: ignore
|
|
106
|
+
for metric in metrics:
|
|
107
|
+
if "cohort_day_" in metric:
|
|
108
|
+
day_count = int(metric.split("_")[2])
|
|
109
|
+
if days_between_today_and_end <= day_count:
|
|
110
|
+
excluded_metrics.append(metric)
|
|
111
|
+
return excluded_metrics
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
version = "v0.13.31"
|
|
@@ -32,7 +32,12 @@ from dlt.common import pendulum
|
|
|
32
32
|
from dlt.common.typing import TDataItems
|
|
33
33
|
from dlt.sources import DltResource
|
|
34
34
|
|
|
35
|
-
from .helpers import
|
|
35
|
+
from .helpers import (
|
|
36
|
+
_get_property_names,
|
|
37
|
+
fetch_data,
|
|
38
|
+
fetch_data_raw,
|
|
39
|
+
fetch_property_history,
|
|
40
|
+
)
|
|
36
41
|
from .settings import (
|
|
37
42
|
ALL,
|
|
38
43
|
CRM_OBJECT_ENDPOINTS,
|
|
@@ -192,12 +197,21 @@ def hubspot(
|
|
|
192
197
|
api_key: str = api_key,
|
|
193
198
|
custom_object_name: str = custom_object,
|
|
194
199
|
) -> Iterator[TDataItems]:
|
|
195
|
-
get_custom_object =
|
|
200
|
+
get_custom_object = fetch_data_raw(CRM_SCHEMAS_ENDPOINT, api_key)
|
|
196
201
|
object_type_id = None
|
|
197
|
-
|
|
198
|
-
|
|
202
|
+
|
|
203
|
+
custom_object_lowercase = custom_object_name.lower()
|
|
204
|
+
for custom_object in get_custom_object["results"]:
|
|
205
|
+
if custom_object["name"].lower() == custom_object_lowercase:
|
|
199
206
|
object_type_id = custom_object["objectTypeId"]
|
|
200
207
|
break
|
|
208
|
+
|
|
209
|
+
# sometimes people use the plural name of the object type by accident, we should try to match that if we can
|
|
210
|
+
if "labels" in custom_object:
|
|
211
|
+
if custom_object_lowercase == custom_object["labels"]["plural"].lower():
|
|
212
|
+
object_type_id = custom_object["objectTypeId"]
|
|
213
|
+
break
|
|
214
|
+
|
|
201
215
|
if object_type_id is None:
|
|
202
216
|
raise ValueError(f"There is no such custom object as {custom_object_name}")
|
|
203
217
|
custom_object_properties = f"crm/v3/properties/{object_type_id}"
|
|
@@ -130,6 +130,7 @@ def fetch_data(
|
|
|
130
130
|
# Parse the API response and yield the properties of each result
|
|
131
131
|
# Parse the response JSON data
|
|
132
132
|
_data = r.json()
|
|
133
|
+
|
|
133
134
|
# Yield the properties of each result in the API response
|
|
134
135
|
while _data is not None:
|
|
135
136
|
if "results" in _data:
|
|
@@ -156,6 +157,7 @@ def fetch_data(
|
|
|
156
157
|
if "id" not in _obj and "id" in _result:
|
|
157
158
|
# Move id from properties to top level
|
|
158
159
|
_obj["id"] = _result["id"]
|
|
160
|
+
|
|
159
161
|
if "associations" in _result:
|
|
160
162
|
for association in _result["associations"]:
|
|
161
163
|
__values = [
|
|
@@ -168,12 +170,13 @@ def fetch_data(
|
|
|
168
170
|
]
|
|
169
171
|
]
|
|
170
172
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
# remove duplicates from list of dicts
|
|
174
|
+
__values = [
|
|
175
|
+
dict(t) for t in {tuple(d.items()) for d in __values}
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
_obj[association] = __values
|
|
175
179
|
|
|
176
|
-
_obj[association] = __values
|
|
177
180
|
_objects.append(_obj)
|
|
178
181
|
yield _objects
|
|
179
182
|
|
|
@@ -208,3 +211,12 @@ def _get_property_names(api_key: str, object_type: str) -> List[str]:
|
|
|
208
211
|
properties.extend([prop["name"] for prop in page])
|
|
209
212
|
|
|
210
213
|
return properties
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def fetch_data_raw(
|
|
217
|
+
endpoint: str, api_key: str, params: Optional[Dict[str, Any]] = None
|
|
218
|
+
) -> Iterator[List[Dict[str, Any]]]:
|
|
219
|
+
url = get_url(endpoint)
|
|
220
|
+
headers = _get_headers(api_key)
|
|
221
|
+
r = requests.get(url, headers=headers, params=params)
|
|
222
|
+
return r.json()
|