ingestr 0.12.5__tar.gz → 0.12.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 ingestr might be problematic. Click here for more details.
- {ingestr-0.12.5 → ingestr-0.12.7}/Makefile +1 -1
- {ingestr-0.12.5 → ingestr-0.12.7}/PKG-INFO +3 -1
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/.vitepress/config.mjs +1 -0
- ingestr-0.12.7/docs/supported-sources/appstore.md +253 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/github.md +3 -3
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/main.py +1 -1
- ingestr-0.12.7/ingestr/src/appstore/__init__.py +137 -0
- ingestr-0.12.7/ingestr/src/appstore/client.py +126 -0
- ingestr-0.12.7/ingestr/src/appstore/errors.py +15 -0
- ingestr-0.12.7/ingestr/src/appstore/models.py +117 -0
- ingestr-0.12.7/ingestr/src/appstore/resources.py +179 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/asana_source/__init__.py +4 -1
- ingestr-0.12.7/ingestr/src/errors.py +10 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/facebook_ads/__init__.py +4 -1
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/factory.py +2 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/filesystem/__init__.py +3 -1
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/github/__init__.py +7 -3
- ingestr-0.12.7/ingestr/src/google_analytics/__init__.py +106 -0
- ingestr-0.12.5/ingestr/src/google_analytics/helpers/data_processing.py → ingestr-0.12.7/ingestr/src/google_analytics/helpers.py +29 -33
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/gorgias/__init__.py +12 -4
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/hubspot/__init__.py +8 -1
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/klaviyo/_init_.py +78 -13
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/shopify/__init__.py +14 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/slack/__init__.py +4 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/sources.py +99 -10
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/stripe_analytics/__init__.py +4 -1
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/tiktok_ads/__init__.py +6 -1
- ingestr-0.12.7/ingestr/src/version.py +1 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/zendesk/__init__.py +6 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/requirements-dev.txt +1 -1
- {ingestr-0.12.5 → ingestr-0.12.7}/requirements.txt +12 -3
- ingestr-0.12.5/ingestr/src/google_analytics/__init__.py +0 -70
- ingestr-0.12.5/ingestr/src/google_analytics/helpers/__init__.py +0 -70
- ingestr-0.12.5/ingestr/src/version.py +0 -1
- {ingestr-0.12.5 → ingestr-0.12.7}/.dockerignore +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/.githooks/pre-commit-hook.sh +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/.github/workflows/deploy-docs.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/.github/workflows/secrets-scan.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/.github/workflows/tests.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/.gitignore +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/.gitleaksignore +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/.python-version +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/.vale.ini +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/Dockerfile +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/LICENSE.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/README.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/.vitepress/theme/custom.css +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/.vitepress/theme/index.js +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/commands/example-uris.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/commands/ingest.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/getting-started/core-concepts.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/getting-started/incremental-loading.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/getting-started/quickstart.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/getting-started/telemetry.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/index.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/media/athena.png +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/media/github.png +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/media/googleanalytics.png +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/media/tiktok.png +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/adjust.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/airtable.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/appsflyer.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/asana.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/athena.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/bigquery.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/chess.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/csv.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/custom_queries.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/databricks.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/duckdb.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/dynamodb.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/facebook-ads.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/google_analytics.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/gorgias.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/gsheets.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/hubspot.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/kafka.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/klaviyo.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/mongodb.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/mssql.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/mysql.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/notion.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/oracle.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/postgres.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/redshift.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/s3.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/sap-hana.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/shopify.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/slack.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/snowflake.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/sqlite.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/stripe.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/tiktok-ads.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/docs/supported-sources/zendesk.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/.gitignore +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/adjust/__init__.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/adjust/adjust_helpers.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/airtable/__init__.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/appsflyer/_init_.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/appsflyer/client.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/arrow/__init__.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/asana_source/helpers.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/asana_source/settings.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/chess/__init__.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/chess/helpers.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/chess/settings.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/destinations.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/dynamodb/__init__.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/facebook_ads/exceptions.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/facebook_ads/helpers.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/facebook_ads/settings.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/filesystem/helpers.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/filesystem/readers.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/filters.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/github/helpers.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/github/queries.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/github/settings.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/google_sheets/README.md +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/google_sheets/__init__.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/google_sheets/helpers/__init__.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/google_sheets/helpers/api_calls.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/google_sheets/helpers/data_processing.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/gorgias/helpers.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/hubspot/helpers.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/hubspot/settings.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/kafka/__init__.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/kafka/helpers.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/klaviyo/client.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/klaviyo/helpers.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/mongodb/__init__.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/mongodb/helpers.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/notion/__init__.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/notion/helpers/__init__.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/notion/helpers/client.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/notion/helpers/database.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/notion/settings.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/shopify/exceptions.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/shopify/helpers.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/shopify/settings.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/slack/helpers.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/slack/settings.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/sql_database/__init__.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/sql_database/callbacks.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/stripe_analytics/helpers.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/stripe_analytics/settings.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/table_definition.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/telemetry/event.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/testdata/fakebqcredentials.json +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/tiktok_ads/tiktok_helpers.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/time.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/zendesk/helpers/__init__.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/zendesk/helpers/api_helpers.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/zendesk/helpers/credentials.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/zendesk/helpers/talk_api.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/src/zendesk/settings.py +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/testdata/.gitignore +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/testdata/create_replace.csv +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/testdata/delete_insert_expected.csv +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/testdata/delete_insert_part1.csv +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/testdata/delete_insert_part2.csv +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/testdata/merge_expected.csv +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/testdata/merge_part1.csv +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/ingestr/testdata/merge_part2.csv +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/package-lock.json +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/package.json +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/pyproject.toml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/resources/demo.gif +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/resources/demo.tape +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/resources/ingestr.svg +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/AMPM.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/Acronyms.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/Colons.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/Contractions.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/DateFormat.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/Ellipses.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/EmDash.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/Exclamation.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/FirstPerson.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/Gender.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/GenderBias.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/HeadingPunctuation.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/Headings.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/Latin.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/LyHyphens.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/OptionalPlurals.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/Ordinal.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/OxfordComma.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/Parens.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/Passive.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/Periods.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/Quotes.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/Ranges.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/Semicolons.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/Slang.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/Spacing.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/Spelling.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/Units.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/We.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/Will.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/WordList.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/meta.json +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/Google/vocab.txt +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/bruin/Ingestr.yml +0 -0
- {ingestr-0.12.5 → ingestr-0.12.7}/styles/config/vocabularies/bruin/accept.txt +0 -0
|
@@ -24,7 +24,7 @@ test-specific: venv
|
|
|
24
24
|
. venv/bin/activate; pytest -rP -vv --tb=short --capture=no -k $(test)
|
|
25
25
|
|
|
26
26
|
lint-ci:
|
|
27
|
-
ruff
|
|
27
|
+
ruff format ingestr && ruff check ingestr --fix
|
|
28
28
|
mypy --config-file pyproject.toml --explicit-package-bases ingestr
|
|
29
29
|
|
|
30
30
|
lint: venv
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ingestr
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.7
|
|
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
|
|
@@ -17,6 +17,7 @@ Requires-Python: >=3.9
|
|
|
17
17
|
Requires-Dist: asana==3.2.3
|
|
18
18
|
Requires-Dist: confluent-kafka>=2.6.1
|
|
19
19
|
Requires-Dist: databricks-sql-connector==2.9.3
|
|
20
|
+
Requires-Dist: dataclasses-json==0.6.7
|
|
20
21
|
Requires-Dist: dlt==1.5.0
|
|
21
22
|
Requires-Dist: duckdb-engine==0.13.5
|
|
22
23
|
Requires-Dist: duckdb==1.1.3
|
|
@@ -26,6 +27,7 @@ Requires-Dist: google-api-python-client==2.130.0
|
|
|
26
27
|
Requires-Dist: google-cloud-bigquery-storage==2.24.0
|
|
27
28
|
Requires-Dist: mysql-connector-python==9.1.0
|
|
28
29
|
Requires-Dist: pendulum==3.0.0
|
|
30
|
+
Requires-Dist: psutil==6.1.1
|
|
29
31
|
Requires-Dist: psycopg2-binary==2.9.10
|
|
30
32
|
Requires-Dist: py-machineid==0.6.0
|
|
31
33
|
Requires-Dist: pyairtable==2.3.3
|
|
@@ -94,6 +94,7 @@ export default defineConfig({
|
|
|
94
94
|
{ text: "Adjust", link: "/supported-sources/adjust.md" },
|
|
95
95
|
{ text: "Airtable", link: "/supported-sources/airtable.md" },
|
|
96
96
|
{ text: "AppsFlyer", link: "/supported-sources/appsflyer.md" },
|
|
97
|
+
{ text: "Apple App Store", link: "/supported-sources/appstore.md"},
|
|
97
98
|
{ text: "Asana", link: "/supported-sources/asana.md" },
|
|
98
99
|
{ text: "Chess.com", link: "/supported-sources/chess.md" },
|
|
99
100
|
{ text: "DynamoDB", link: "/supported-sources/dynamodb.md" },
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# Apple App Store
|
|
2
|
+
The [App Store](https://appstore.com/) is an app marketplace developed and maintained by Apple, for mobile apps on its iOS and iPadOS operating systems. The store allows users to browse and download approved apps developed within Apple's iOS SDK. Apps can be downloaded on the iPhone, iPod Touch, or iPad, and some can be transferred to the Apple Watch smartwatch or 4th-generation or newer Apple TVs as extensions of iPhone apps.
|
|
3
|
+
|
|
4
|
+
`ingestr` allows you to ingest analytics, sales and performance data using the [Apple App Store Connect API](https://developer.apple.com/documentation/appstoreconnectapi)
|
|
5
|
+
|
|
6
|
+
> [!NOTE]
|
|
7
|
+
> Sometimes, the data in App Store Analytics reports isn’t fully complete when first provided. This happens because some information takes longer to process and appears in the reports later. For example, certain usage or sales details might be updated after the initial report is generated to correct errors or include missing data. This means that the report for a certain date may include data points from older dates. `ingestr` takes care of updating these rows to show the updated values. However, caution should be exercised when analysing current date's data, as it maybe subject to change in the future.
|
|
8
|
+
> see [Data Completeness and Corrections](https://developer.apple.com/documentation/analytics-reports/data-completeness-corrections) for more information.
|
|
9
|
+
## URI Format
|
|
10
|
+
|
|
11
|
+
The URI format for App Store is as follows:
|
|
12
|
+
```
|
|
13
|
+
appstore://?key_path=</path/to/key>&key_id=<key_id>&issuer_id=<issuer_id>&app_id=<app_id>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
URI Parameters:
|
|
17
|
+
* `key_path`: path to API private key
|
|
18
|
+
* `key_id`: ID of the generated key
|
|
19
|
+
* `issuer_id`: Issuer ID of the generated key
|
|
20
|
+
* `app_id`: application ID of your app. You can specify `app_id` multiple times with different ids to ingest data for multiple apps.
|
|
21
|
+
|
|
22
|
+
## Setting up Appstore Integration
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Prerequisites
|
|
26
|
+
To generate an API key, you must have an Admin account in App Store Connect.
|
|
27
|
+
|
|
28
|
+
### Generate an API Key
|
|
29
|
+
|
|
30
|
+
To generate a new API key to use with `ingestr`, log in to [App Store Connect](https://appstoreconnect.apple.com/) and:
|
|
31
|
+
|
|
32
|
+
1. Select Users and Access, and then select the API Keys tab.
|
|
33
|
+
2. Make sure the Team Keys tab is selected.
|
|
34
|
+
3. Click Generate API Key or the Add (+) button.
|
|
35
|
+
4. Enter a name for the key. The name is for your reference only and isn’t part of the key itself.
|
|
36
|
+
5. Under Access, select the role as `FINANCE`.
|
|
37
|
+
6. Click Generate.
|
|
38
|
+
|
|
39
|
+
The new key’s name, key ID, a download link, and other information appears on the page.
|
|
40
|
+
|
|
41
|
+
For more information, see [App Store Connect docs](https://developer.apple.com/documentation/appstoreconnectapi/creating-api-keys-for-app-store-connect-api)
|
|
42
|
+
|
|
43
|
+
### Find your Apps ID
|
|
44
|
+
|
|
45
|
+
You can find the App ID of your app by:
|
|
46
|
+
1. Opening the app entry in App Store Connect
|
|
47
|
+
2. Looking for "General Information" in the App information tab
|
|
48
|
+
3. Finding your App ID under the "Apple ID" entry
|
|
49
|
+
|
|
50
|
+
With this, you are ready to ingest data from App Store.
|
|
51
|
+
|
|
52
|
+
### Request a Report.
|
|
53
|
+
Before you can ingest analytics data from App Store, you need to submit a [Report Request](https://developer.apple.com/documentation/appstoreconnectapi/post-v1-analyticsreportrequests). See [App Store Connect docs](https://developer.apple.com/documentation/appstoreconnectapi/downloading-analytics-reports) for more information on how to Request a Report.
|
|
54
|
+
|
|
55
|
+
We recommend using `ONGOING` access-type for reports. Please note that it may take upto 48 hours after submitting a Report Request for the data to become available. For more information, see [Request analytics report](https://developer.apple.com/documentation/appstoreconnectapi/downloading-analytics-reports#Request-analytics-reports).
|
|
56
|
+
|
|
57
|
+
> [!NOTE]
|
|
58
|
+
> you have to create a Report Request for each individual App that you want to ingest data for. You can use [list apps](https://developer.apple.com/documentation/appstoreconnectapi/get-v1-apps) API to get the list of all apps in your Apple Account.
|
|
59
|
+
### Example: Loading App Downloads Analytics
|
|
60
|
+
|
|
61
|
+
For this example, we'll assume that:
|
|
62
|
+
* `key_id` is `key_0`
|
|
63
|
+
* `issuer_id` is `issue_0`
|
|
64
|
+
* `key` is stored in the current directory and is named `api.key`
|
|
65
|
+
* `app_id` is `12345`
|
|
66
|
+
|
|
67
|
+
We will run `ingestr` to save this data to a [duckdb](https://duckdb.org/) database called `analytics.db` under the name `public.app_downloads`.
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
ingestr ingest \
|
|
71
|
+
--source-uri "appstore://app_id=12345&key_path=api.key&key_id=key_0&issuer_id=issue_0 \
|
|
72
|
+
--source-table "app-downloads-detailed" \
|
|
73
|
+
--dest-uri "duckdb:///analytics.db" \
|
|
74
|
+
--dest-table "public.app_downloads" \
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Example: Loading Data for multiple Apps
|
|
78
|
+
|
|
79
|
+
We will extend the prior example with another app with ID `67890`. To achieve this, simply add another `app_id` query parameter to the URI.
|
|
80
|
+
```sh
|
|
81
|
+
ingestr ingest \
|
|
82
|
+
--source-uri "appstore://app_id=12345&app_id=67890&key_path=api.key&key_id=key_0&issuer_id=issue_0 \
|
|
83
|
+
--source-table "app-downloads-detailed" \
|
|
84
|
+
--dest-uri "duckdb:///analytics.db" \
|
|
85
|
+
--dest-table "public.app_downloads" \
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
### Example: Incremental Loading
|
|
90
|
+
|
|
91
|
+
`ingestr` supports incremental loading for all App Store tables.
|
|
92
|
+
|
|
93
|
+
To begin, we will first load all data till `2025-01-01` by specifying the `--interval-end` flag. We'll assume the same credentials from our [first example](#example-loading-app-downloads-analytics)
|
|
94
|
+
```sh
|
|
95
|
+
ingestr ingest \
|
|
96
|
+
--source-uri "appstore://app_id=12345&key_path=api.key&key_id=key_0&issuer_id=issue_0 \
|
|
97
|
+
--source-table "app-downloads-detailed" \
|
|
98
|
+
--dest-uri "duckdb:///analytics.db" \
|
|
99
|
+
--dest-table "public.app_downloads" \
|
|
100
|
+
--interval-end "2025-01-01"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`ingestr` will load all data available till `2025-01-01`. Now we will run `ingestr` again, but this time, we'll let `ingestr` pickup from where it left off by specifying the `--incremental-strategy` flag.
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
ingestr ingest \
|
|
107
|
+
--source-uri "appstore://app_id=12345&key_path=api.key&key_id=key_0&issuer_id=issue_0 \
|
|
108
|
+
--source-table "app-downloads-detailed" \
|
|
109
|
+
--dest-uri "duckdb:///analytics.db" \
|
|
110
|
+
--dest-table "public.app_downloads" \
|
|
111
|
+
--incremental-strategy "merge"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Notice how we didn't specify a date parameter? `ingestr` will automatically use the metadata from last load and continue loading data from that point on.
|
|
115
|
+
|
|
116
|
+
## Tables
|
|
117
|
+
|
|
118
|
+
### `app-downloads-detailed`
|
|
119
|
+
The App Downloads Report includes download data generated on the App Store. You can use this report to understand your total number of downloads, including first-time downloads, redownloads, updates, and more.
|
|
120
|
+
|
|
121
|
+
| **Column** | **Description** |
|
|
122
|
+
|--------------|-----------------|
|
|
123
|
+
| `date` | Date on which the event occurred. |
|
|
124
|
+
| `app_name` | The name of the app provided by you during app setup in App Store Connect.|
|
|
125
|
+
| `app_apple_identifier` | Your app’s Apple ID. |
|
|
126
|
+
| `download_type` | The type of download event that occured. |
|
|
127
|
+
| `app_version` | The app version being downloaded. |
|
|
128
|
+
| `device` | The device on which the app was downloaded.|
|
|
129
|
+
| `platform_version` | The OS version of the device on which the download occured.|
|
|
130
|
+
| `source_type` | The source from where the user discovered the app.|
|
|
131
|
+
| `source_info` | The app referrer or web referrer that led the user to discover the app.|
|
|
132
|
+
| `campaign` | The Campaign Token of the campaign created in App Analytics. Column available starting November 19, 2024.|
|
|
133
|
+
| `page_type` | The page type from where the app was downloaded. |
|
|
134
|
+
| `page_title` | The name of the product page or in-app event page that led the user to download the app.|
|
|
135
|
+
| `pre-order` | A flag indicating whether the download came from a pre-order.|
|
|
136
|
+
| `territory` | The App Store country or region where the download occured.|
|
|
137
|
+
| `counts` | The total number of downloads.|
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
### `app-store-discovery-and-engagement-detailed`
|
|
141
|
+
The App Store Discovery and Engagement report provides details about how users engage with your apps on the App Store itself. This includes data about user engagement with your app’s icons, product pages, in-app event pages, and other install sheets.
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
| **Column** | **Description** |
|
|
145
|
+
|--------------|-----------------|
|
|
146
|
+
| `date` | Date on which the event occurred. |
|
|
147
|
+
| `app_name` | The name of the app provided by you during app setup in App Store Connect.|
|
|
148
|
+
| `app_apple_identifier` | Your app’s Apple ID. |
|
|
149
|
+
| `event` | The type of event that occurred.|
|
|
150
|
+
| `source_type` | The source from where the user discovered the app. |
|
|
151
|
+
| `source_info` | The app referrer or web referrer that led the user to discover the app.|
|
|
152
|
+
| `campaign` | The Campaign Token of the campaign created in App Analytics. Column available starting November 19, 2024.|
|
|
153
|
+
| `page_type` | The page type from where the app was downloaded. |
|
|
154
|
+
| `page_title` | The name of the product page or in-app event page that led the user to download the app.|
|
|
155
|
+
| `device` | The device on which the event occurred. |
|
|
156
|
+
| `engagement_type` | User action, if any, on the impression or page.|
|
|
157
|
+
| `platform_version` | The OS version of the device on which the event occurred.|
|
|
158
|
+
| `territory` | The App Store country or region where the download occured.|
|
|
159
|
+
| `counts` | The total number of events that occurred. |
|
|
160
|
+
| `unique_counts` | The total number of unique users that performed the event.|
|
|
161
|
+
|
|
162
|
+
### `app-sessions-detailed`
|
|
163
|
+
App Session provides insights on how often people open your app, and how long they spend in your app.
|
|
164
|
+
|
|
165
|
+
| **Column** | **Description** |
|
|
166
|
+
|--------------|-----------------|
|
|
167
|
+
| `date` | Date on which the event occurred. |
|
|
168
|
+
| `app_name` | The name of the app provided by you during app setup in App Store Connect.|
|
|
169
|
+
| `app_apple_identifier` | Your app’s Apple ID. |
|
|
170
|
+
| `download_type` | The type of download event that occured. |
|
|
171
|
+
| `app_version` | The app version being downloaded. |
|
|
172
|
+
| `device` | The device on which the app was downloaded.|
|
|
173
|
+
| `platform_version` | The OS version of the device on which the download occured.|
|
|
174
|
+
| `source_type` | The source from where the user discovered the app.|
|
|
175
|
+
| `source_info` | The app referrer or web referrer that led the user to discover the app.|
|
|
176
|
+
| `campaign` | The Campaign Token of the campaign created in App Analytics. Column available starting November 19, 2024.|
|
|
177
|
+
| `page_type` | The page type from where the app was downloaded. |
|
|
178
|
+
| `page_title` | The name of the product page or in-app event page that led the user to download the app.|
|
|
179
|
+
| `app_download_date` | The date on which the app was downloaded onto the device. This field is only populated if the download occurred in the previous 30 days, otherwise it is null.|
|
|
180
|
+
| `territory` | The App Store country or region where the download occured.|
|
|
181
|
+
| `sessions` | The number of sessions. Based on users who have agreed to share their data with Apple and developers.|
|
|
182
|
+
| `total_session_duration` | The total duration, in seconds, of all sessions being reported.|
|
|
183
|
+
| `unique_devices` | The number of unique devices contributing to the total number of sessions being reported.|
|
|
184
|
+
|
|
185
|
+
### `app-store-installation-and-deletion-detailed`
|
|
186
|
+
Use the data in App Store Installation and Deletion report to estimate the number of times people install and delete your App Store apps.
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
| **Column** | **Description** |
|
|
190
|
+
|--------------|-----------------|
|
|
191
|
+
| `date`| Date on which the event occurred.|
|
|
192
|
+
| `app_name`| The name of the app provided by you during app setup in App Store Connect.|
|
|
193
|
+
| `app_apple_identifier`| Date on which the event occurred.|
|
|
194
|
+
| `event`| The type of usage event that occurred. |
|
|
195
|
+
| `download_type`| The type of download event that occurred.|
|
|
196
|
+
| `app_version`| The version of the app being associated with the instalation or deletion.|
|
|
197
|
+
| `device`| The device on which the app was installed or deleted.|
|
|
198
|
+
| `platform_version`| The OS version of the device on which the app was installed or deleted.|
|
|
199
|
+
| `source_type`| Where the user discovered your app.|
|
|
200
|
+
| `source_info`| The app referrer or web referrer that led the user to discover your app.|
|
|
201
|
+
| `campaign`| The Campaign Token of the campaign created in App Analytics. Column available starting November 19, 2024.|
|
|
202
|
+
| `page_type`| The page type which led the user to discover your app.|
|
|
203
|
+
| `page_title`| The name of the product page or in-app event page that led the user to discover your app.|
|
|
204
|
+
| `app_download_date`| The date on which the app was downloaded onto the device. This field is only populated if the download occurred in the previous 30 days, otherwise it is null.|
|
|
205
|
+
| `territory`| The App Store country or region where the installation or deletion occurred.|
|
|
206
|
+
| `counts`| The total count of events, based on users who have agreed to share their data with Apple and developers.|
|
|
207
|
+
| `unique_devices`| The number of unique devices on which events were generated, based on users who have agreed to share their data with Apple and developers.|
|
|
208
|
+
|
|
209
|
+
### `app-store-purchases-detailed`
|
|
210
|
+
The App Store Purchases Report includes App Store paid app and in-app purchase data. Using the data in this report, you can measure your total revenue generated on the App Store, attribute sales to download sources and page types, and measure how many paying users you have for each individual row. Paying user counts are not summable across rows, because the same user can exist in multiple rows.
|
|
211
|
+
|
|
212
|
+
| **Column** | **Description** |
|
|
213
|
+
|--------------|-----------------|
|
|
214
|
+
| `date`| Date on which the event occurred.|
|
|
215
|
+
| `app_name`| The name of the app provided by you during app setup in App Store Connect.|
|
|
216
|
+
| `app_apple_identifier`| Your app’s Apple ID.|
|
|
217
|
+
| `purchase_type`| The type of purchase made by the user on the App Store.|
|
|
218
|
+
| `content_name`| The name of the content being purchased. For paid apps, the field will populate the name of app as set in App Store. For in-app purchases, the field will populate the name of the SKU as set in App Store Connect.|
|
|
219
|
+
| `content_apple_identifier`| Your content’s Apple ID |
|
|
220
|
+
| `payment_method`| The payment type used to charge the customer.|
|
|
221
|
+
| `device`| The device on which the purchase occurred.|
|
|
222
|
+
| `platform_version`| The OS version of the device on which the app was installed or deleted.|
|
|
223
|
+
| `source_type`| Where the user discovered your app.|
|
|
224
|
+
| `source_info`| The app referrer or web referrer that led the user to discover your app.|
|
|
225
|
+
| `campaign`| The Campaign Token of the campaign created in App Analytics. Column available starting November 19, 2024.|
|
|
226
|
+
| `page_type`| The page type which led the user to discover your app.|
|
|
227
|
+
| `page_title`| The name of the product page or in-app event page that led the user to discover your app.|
|
|
228
|
+
| `app_download_date`| The date on which the app was downloaded onto the device. This field is only populated if the download occurred in the previous 30 days, otherwise it is null.|
|
|
229
|
+
| `pre-order`| Indicates whether the purchase originated from someone who pre-ordered the app.|
|
|
230
|
+
| `territory`| The App Store country or region in which the purchase occurred.|
|
|
231
|
+
| `purchases`| Aggregated count of purchases made. Negative value indicates refunds. If purchases count is 0 and proceeds, and sales are negative, it indicates partial refunds.|
|
|
232
|
+
| `proceeds_in_usd`| The estimated proceeds in USD from purchases of your app and in-app purchases. This is the Customer Price minus applicable taxes and Apple’s commission, per Schedule 2 of the Paid Apps Agreement.|
|
|
233
|
+
| `sales_in_usd`| The estimated sales in USD from purchases of your app and in-app purchases.|
|
|
234
|
+
| `paying_users`| The number of unique users who paid for your app or in-app purchases. This metric is not summable across rows.|
|
|
235
|
+
|
|
236
|
+
### `app-crashes-expanded`
|
|
237
|
+
Use this report to understand crashes for your App Store apps by app version and device type.
|
|
238
|
+
|
|
239
|
+
| **Column** | **Description** |
|
|
240
|
+
|--------------|-----------------|
|
|
241
|
+
| `date`| Date on which the event occurred.|
|
|
242
|
+
| `app_name`| The name of the app provided by you during app setup in App Store Connect.|
|
|
243
|
+
| `app_apple_identifier`| Your app’s Apple ID.|
|
|
244
|
+
| `app_version` | The app version being downloaded. |
|
|
245
|
+
| `device` | The device on which the app was downloaded.|
|
|
246
|
+
| `platform_version` | The OS version of the device on which the download occured.|
|
|
247
|
+
| `crashes` | The total number of crashes.|
|
|
248
|
+
| `unique_devices` | Number of unique devices where app crashed. |
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
Use these as `--source-table` parameter in the `ingestr ingest` command.
|
|
252
|
+
|
|
253
|
+
To know more about these reports and their dimensions, see [App Store Analytics docs](https://developer.apple.com/documentation/analytics-reports).
|
|
@@ -14,9 +14,9 @@ github://?access_token=<access_token>&owner=<owner>&repo=<repo>
|
|
|
14
14
|
|
|
15
15
|
URI parameters:
|
|
16
16
|
|
|
17
|
-
- `access_token
|
|
18
|
-
- `owner
|
|
19
|
-
- `repo
|
|
17
|
+
- `access_token` (optional): Access Token used for authentication with the GitHub API
|
|
18
|
+
- `owner` (required): Refers to the owner of the repository
|
|
19
|
+
- `repo` (required): Refers to the name of the repository
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
## Setting up a GitHub Integration
|
|
@@ -444,7 +444,7 @@ def ingest(
|
|
|
444
444
|
|
|
445
445
|
progressInstance: Collector = SpinnerCollector()
|
|
446
446
|
if progress == Progress.log:
|
|
447
|
-
progressInstance = LogCollector(
|
|
447
|
+
progressInstance = LogCollector()
|
|
448
448
|
|
|
449
449
|
is_pipelines_dir_temp = False
|
|
450
450
|
if pipelines_dir is None:
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import gzip
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Iterable, List, Optional
|
|
8
|
+
|
|
9
|
+
import dlt
|
|
10
|
+
import requests
|
|
11
|
+
from dlt.common.typing import TDataItem
|
|
12
|
+
from dlt.sources import DltResource
|
|
13
|
+
|
|
14
|
+
from .client import AppStoreConnectClientInterface
|
|
15
|
+
from .errors import (
|
|
16
|
+
NoOngoingReportRequestsFoundError,
|
|
17
|
+
NoReportsFoundError,
|
|
18
|
+
NoSuchReportError,
|
|
19
|
+
)
|
|
20
|
+
from .models import AnalyticsReportInstancesResponse
|
|
21
|
+
from .resources import RESOURCES
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dlt.source
|
|
25
|
+
def app_store(
|
|
26
|
+
client: AppStoreConnectClientInterface,
|
|
27
|
+
app_ids: List[str],
|
|
28
|
+
start_date: Optional[datetime] = None,
|
|
29
|
+
end_date: Optional[datetime] = None,
|
|
30
|
+
) -> Iterable[DltResource]:
|
|
31
|
+
for resource in RESOURCES:
|
|
32
|
+
yield dlt.resource(
|
|
33
|
+
get_analytics_reports,
|
|
34
|
+
name=resource.name,
|
|
35
|
+
primary_key=resource.primary_key,
|
|
36
|
+
columns=resource.columns,
|
|
37
|
+
)(client, app_ids, resource.report_name, start_date, end_date)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def filter_instances_by_date(
|
|
41
|
+
instances: AnalyticsReportInstancesResponse,
|
|
42
|
+
start_date: Optional[datetime],
|
|
43
|
+
end_date: Optional[datetime],
|
|
44
|
+
) -> AnalyticsReportInstancesResponse:
|
|
45
|
+
instances = deepcopy(instances)
|
|
46
|
+
if start_date is not None:
|
|
47
|
+
instances.data = list(
|
|
48
|
+
filter(
|
|
49
|
+
lambda x: datetime.fromisoformat(x.attributes.processingDate)
|
|
50
|
+
>= start_date,
|
|
51
|
+
instances.data,
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
if end_date is not None:
|
|
55
|
+
instances.data = list(
|
|
56
|
+
filter(
|
|
57
|
+
lambda x: datetime.fromisoformat(x.attributes.processingDate)
|
|
58
|
+
<= end_date,
|
|
59
|
+
instances.data,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return instances
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_analytics_reports(
|
|
67
|
+
client: AppStoreConnectClientInterface,
|
|
68
|
+
app_ids: List[str],
|
|
69
|
+
report_name: str,
|
|
70
|
+
start_date: Optional[datetime],
|
|
71
|
+
end_date: Optional[datetime],
|
|
72
|
+
last_processing_date=dlt.sources.incremental("processing_date"),
|
|
73
|
+
) -> Iterable[TDataItem]:
|
|
74
|
+
if last_processing_date.last_value:
|
|
75
|
+
start_date = datetime.fromisoformat(last_processing_date.last_value)
|
|
76
|
+
for app_id in app_ids:
|
|
77
|
+
yield from get_report(client, app_id, report_name, start_date, end_date)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_report(
|
|
81
|
+
client: AppStoreConnectClientInterface,
|
|
82
|
+
app_id: str,
|
|
83
|
+
report_name: str,
|
|
84
|
+
start_date: Optional[datetime],
|
|
85
|
+
end_date: Optional[datetime],
|
|
86
|
+
) -> Iterable[TDataItem]:
|
|
87
|
+
report_requests = client.list_analytics_report_requests(app_id)
|
|
88
|
+
ongoing_requests = list(
|
|
89
|
+
filter(
|
|
90
|
+
lambda x: x.attributes.accessType == "ONGOING"
|
|
91
|
+
and not x.attributes.stoppedDueToInactivity,
|
|
92
|
+
report_requests.data,
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if len(ongoing_requests) == 0:
|
|
97
|
+
raise NoOngoingReportRequestsFoundError()
|
|
98
|
+
|
|
99
|
+
reports = client.list_analytics_reports(ongoing_requests[0].id, report_name)
|
|
100
|
+
if len(reports.data) == 0:
|
|
101
|
+
raise NoSuchReportError(report_name)
|
|
102
|
+
|
|
103
|
+
for report in reports.data:
|
|
104
|
+
instances = client.list_report_instances(report.id)
|
|
105
|
+
|
|
106
|
+
instances = filter_instances_by_date(instances, start_date, end_date)
|
|
107
|
+
|
|
108
|
+
if len(instances.data) == 0:
|
|
109
|
+
raise NoReportsFoundError()
|
|
110
|
+
|
|
111
|
+
for instance in instances.data:
|
|
112
|
+
segments = client.list_report_segments(instance.id)
|
|
113
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
114
|
+
files = []
|
|
115
|
+
for segment in segments.data:
|
|
116
|
+
payload = requests.get(segment.attributes.url, stream=True)
|
|
117
|
+
payload.raise_for_status()
|
|
118
|
+
|
|
119
|
+
csv_path = os.path.join(
|
|
120
|
+
temp_dir, f"{segment.attributes.checksum}.csv"
|
|
121
|
+
)
|
|
122
|
+
with open(csv_path, "wb") as f:
|
|
123
|
+
for chunk in payload.iter_content(chunk_size=8192):
|
|
124
|
+
f.write(chunk)
|
|
125
|
+
files.append(csv_path)
|
|
126
|
+
for file in files:
|
|
127
|
+
with gzip.open(file, "rt") as f:
|
|
128
|
+
# TODO: infer delimiter from the file itself
|
|
129
|
+
delimiter = (
|
|
130
|
+
"," if report_name == "App Crashes Expanded" else "\t"
|
|
131
|
+
)
|
|
132
|
+
reader = csv.DictReader(f, delimiter=delimiter)
|
|
133
|
+
for row in reader:
|
|
134
|
+
yield {
|
|
135
|
+
"processing_date": instance.attributes.processingDate,
|
|
136
|
+
**row,
|
|
137
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import time
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import jwt
|
|
6
|
+
import requests
|
|
7
|
+
from requests.models import PreparedRequest
|
|
8
|
+
|
|
9
|
+
from .models import (
|
|
10
|
+
AnalyticsReportInstancesResponse,
|
|
11
|
+
AnalyticsReportRequestsResponse,
|
|
12
|
+
AnalyticsReportResponse,
|
|
13
|
+
AnalyticsReportSegmentsResponse,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AppStoreConnectClientInterface(abc.ABC):
|
|
18
|
+
@abc.abstractmethod
|
|
19
|
+
def list_analytics_report_requests(self, app_id) -> AnalyticsReportRequestsResponse:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
@abc.abstractmethod
|
|
23
|
+
def list_analytics_reports(
|
|
24
|
+
self, req_id: str, report_name: str
|
|
25
|
+
) -> AnalyticsReportResponse:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@abc.abstractmethod
|
|
29
|
+
def list_report_instances(
|
|
30
|
+
self,
|
|
31
|
+
report_id: str,
|
|
32
|
+
granularity: str = "DAILY",
|
|
33
|
+
) -> AnalyticsReportInstancesResponse:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
@abc.abstractmethod
|
|
37
|
+
def list_report_segments(self, instance_id: str) -> AnalyticsReportSegmentsResponse:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AppStoreConnectClient(AppStoreConnectClientInterface):
|
|
42
|
+
def __init__(self, key: bytes, key_id: str, issuer_id: str):
|
|
43
|
+
self.__key = key
|
|
44
|
+
self.__key_id = key_id
|
|
45
|
+
self.__issuer_id = issuer_id
|
|
46
|
+
|
|
47
|
+
def list_analytics_report_requests(self, app_id) -> AnalyticsReportRequestsResponse:
|
|
48
|
+
res = requests.get(
|
|
49
|
+
f"https://api.appstoreconnect.apple.com/v1/apps/{app_id}/analyticsReportRequests",
|
|
50
|
+
auth=self.auth,
|
|
51
|
+
)
|
|
52
|
+
res.raise_for_status()
|
|
53
|
+
|
|
54
|
+
return AnalyticsReportRequestsResponse.from_json(res.text) # type: ignore
|
|
55
|
+
|
|
56
|
+
def list_analytics_reports(
|
|
57
|
+
self, req_id: str, report_name: str
|
|
58
|
+
) -> AnalyticsReportResponse:
|
|
59
|
+
params = {"filter[name]": report_name}
|
|
60
|
+
res = requests.get(
|
|
61
|
+
f"https://api.appstoreconnect.apple.com/v1/analyticsReportRequests/{req_id}/reports",
|
|
62
|
+
auth=self.auth,
|
|
63
|
+
params=params,
|
|
64
|
+
)
|
|
65
|
+
res.raise_for_status()
|
|
66
|
+
return AnalyticsReportResponse.from_json(res.text) # type: ignore
|
|
67
|
+
|
|
68
|
+
def list_report_instances(
|
|
69
|
+
self,
|
|
70
|
+
report_id: str,
|
|
71
|
+
granularity: str = "DAILY",
|
|
72
|
+
) -> AnalyticsReportInstancesResponse:
|
|
73
|
+
data = []
|
|
74
|
+
url = f"https://api.appstoreconnect.apple.com/v1/analyticsReports/{report_id}/instances"
|
|
75
|
+
params: Optional[dict] = {"filter[granularity]": granularity}
|
|
76
|
+
|
|
77
|
+
while url:
|
|
78
|
+
res = requests.get(url, auth=self.auth, params=params)
|
|
79
|
+
res.raise_for_status()
|
|
80
|
+
|
|
81
|
+
response_data = AnalyticsReportInstancesResponse.from_json(res.text) # type: ignore
|
|
82
|
+
data.extend(response_data.data)
|
|
83
|
+
|
|
84
|
+
url = response_data.links.next
|
|
85
|
+
params = None # Clear params for subsequent requests
|
|
86
|
+
|
|
87
|
+
return AnalyticsReportInstancesResponse(
|
|
88
|
+
data=data,
|
|
89
|
+
links=response_data.links,
|
|
90
|
+
meta=response_data.meta,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def list_report_segments(self, instance_id: str) -> AnalyticsReportSegmentsResponse:
|
|
94
|
+
segments = []
|
|
95
|
+
url = f"https://api.appstoreconnect.apple.com/v1/analyticsReportInstances/{instance_id}/segments"
|
|
96
|
+
|
|
97
|
+
while url:
|
|
98
|
+
res = requests.get(url, auth=self.auth)
|
|
99
|
+
res.raise_for_status()
|
|
100
|
+
|
|
101
|
+
response_data = AnalyticsReportSegmentsResponse.from_json(res.text) # type: ignore
|
|
102
|
+
segments.extend(response_data.data)
|
|
103
|
+
|
|
104
|
+
url = response_data.links.next
|
|
105
|
+
|
|
106
|
+
return AnalyticsReportSegmentsResponse(
|
|
107
|
+
data=segments, links=response_data.links, meta=response_data.meta
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def auth(self, req: PreparedRequest) -> PreparedRequest:
|
|
111
|
+
headers = {
|
|
112
|
+
"alg": "ES256",
|
|
113
|
+
"kid": self.__key_id,
|
|
114
|
+
}
|
|
115
|
+
payload = {
|
|
116
|
+
"iss": self.__issuer_id,
|
|
117
|
+
"exp": int(time.time()) + 600,
|
|
118
|
+
"aud": "appstoreconnect-v1",
|
|
119
|
+
}
|
|
120
|
+
req.headers["Authorization"] = jwt.encode(
|
|
121
|
+
payload,
|
|
122
|
+
self.__key,
|
|
123
|
+
algorithm="ES256",
|
|
124
|
+
headers=headers,
|
|
125
|
+
)
|
|
126
|
+
return req
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class NoReportsFoundError(Exception):
|
|
2
|
+
def __init__(self):
|
|
3
|
+
super().__init__("No Report instances found for the given date range")
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NoOngoingReportRequestsFoundError(Exception):
|
|
7
|
+
def __init__(self):
|
|
8
|
+
super().__init__(
|
|
9
|
+
"No ONGOING report requests found (or they're stopped due to inactivity)"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NoSuchReportError(Exception):
|
|
14
|
+
def __init__(self, report_name):
|
|
15
|
+
super().__init__(f"No such report found: {report_name}")
|