ingestr 0.12.6__tar.gz → 0.12.8__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.

Files changed (202) hide show
  1. {ingestr-0.12.6 → ingestr-0.12.8}/.githooks/pre-commit-hook.sh +2 -2
  2. {ingestr-0.12.6 → ingestr-0.12.8}/PKG-INFO +2 -1
  3. {ingestr-0.12.6 → ingestr-0.12.8}/docs/.vitepress/config.mjs +1 -0
  4. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/adjust.md +1 -0
  5. ingestr-0.12.8/docs/supported-sources/appstore.md +254 -0
  6. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/adjust/__init__.py +7 -2
  7. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/adjust/adjust_helpers.py +27 -15
  8. ingestr-0.12.8/ingestr/src/appstore/__init__.py +137 -0
  9. ingestr-0.12.8/ingestr/src/appstore/client.py +126 -0
  10. ingestr-0.12.8/ingestr/src/appstore/errors.py +15 -0
  11. ingestr-0.12.8/ingestr/src/appstore/models.py +117 -0
  12. ingestr-0.12.8/ingestr/src/appstore/resources.py +179 -0
  13. ingestr-0.12.8/ingestr/src/errors.py +10 -0
  14. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/factory.py +2 -0
  15. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/sources.py +80 -1
  16. ingestr-0.12.8/ingestr/src/version.py +1 -0
  17. {ingestr-0.12.6 → ingestr-0.12.8}/requirements.txt +8 -0
  18. ingestr-0.12.6/ingestr/src/version.py +0 -1
  19. {ingestr-0.12.6 → ingestr-0.12.8}/.dockerignore +0 -0
  20. {ingestr-0.12.6 → ingestr-0.12.8}/.github/workflows/deploy-docs.yml +0 -0
  21. {ingestr-0.12.6 → ingestr-0.12.8}/.github/workflows/secrets-scan.yml +0 -0
  22. {ingestr-0.12.6 → ingestr-0.12.8}/.github/workflows/tests.yml +0 -0
  23. {ingestr-0.12.6 → ingestr-0.12.8}/.gitignore +0 -0
  24. {ingestr-0.12.6 → ingestr-0.12.8}/.gitleaksignore +0 -0
  25. {ingestr-0.12.6 → ingestr-0.12.8}/.python-version +0 -0
  26. {ingestr-0.12.6 → ingestr-0.12.8}/.vale.ini +0 -0
  27. {ingestr-0.12.6 → ingestr-0.12.8}/Dockerfile +0 -0
  28. {ingestr-0.12.6 → ingestr-0.12.8}/LICENSE.md +0 -0
  29. {ingestr-0.12.6 → ingestr-0.12.8}/Makefile +0 -0
  30. {ingestr-0.12.6 → ingestr-0.12.8}/README.md +0 -0
  31. {ingestr-0.12.6 → ingestr-0.12.8}/docs/.vitepress/theme/custom.css +0 -0
  32. {ingestr-0.12.6 → ingestr-0.12.8}/docs/.vitepress/theme/index.js +0 -0
  33. {ingestr-0.12.6 → ingestr-0.12.8}/docs/commands/example-uris.md +0 -0
  34. {ingestr-0.12.6 → ingestr-0.12.8}/docs/commands/ingest.md +0 -0
  35. {ingestr-0.12.6 → ingestr-0.12.8}/docs/getting-started/core-concepts.md +0 -0
  36. {ingestr-0.12.6 → ingestr-0.12.8}/docs/getting-started/incremental-loading.md +0 -0
  37. {ingestr-0.12.6 → ingestr-0.12.8}/docs/getting-started/quickstart.md +0 -0
  38. {ingestr-0.12.6 → ingestr-0.12.8}/docs/getting-started/telemetry.md +0 -0
  39. {ingestr-0.12.6 → ingestr-0.12.8}/docs/index.md +0 -0
  40. {ingestr-0.12.6 → ingestr-0.12.8}/docs/media/athena.png +0 -0
  41. {ingestr-0.12.6 → ingestr-0.12.8}/docs/media/github.png +0 -0
  42. {ingestr-0.12.6 → ingestr-0.12.8}/docs/media/googleanalytics.png +0 -0
  43. {ingestr-0.12.6 → ingestr-0.12.8}/docs/media/tiktok.png +0 -0
  44. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/airtable.md +0 -0
  45. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/appsflyer.md +0 -0
  46. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/asana.md +0 -0
  47. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/athena.md +0 -0
  48. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/bigquery.md +0 -0
  49. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/chess.md +0 -0
  50. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/csv.md +0 -0
  51. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/custom_queries.md +0 -0
  52. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/databricks.md +0 -0
  53. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/duckdb.md +0 -0
  54. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/dynamodb.md +0 -0
  55. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/facebook-ads.md +0 -0
  56. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/github.md +0 -0
  57. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/google_analytics.md +0 -0
  58. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/gorgias.md +0 -0
  59. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/gsheets.md +0 -0
  60. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/hubspot.md +0 -0
  61. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/kafka.md +0 -0
  62. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/klaviyo.md +0 -0
  63. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/mongodb.md +0 -0
  64. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/mssql.md +0 -0
  65. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/mysql.md +0 -0
  66. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/notion.md +0 -0
  67. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/oracle.md +0 -0
  68. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/postgres.md +0 -0
  69. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/redshift.md +0 -0
  70. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/s3.md +0 -0
  71. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/sap-hana.md +0 -0
  72. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/shopify.md +0 -0
  73. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/slack.md +0 -0
  74. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/snowflake.md +0 -0
  75. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/sqlite.md +0 -0
  76. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/stripe.md +0 -0
  77. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/tiktok-ads.md +0 -0
  78. {ingestr-0.12.6 → ingestr-0.12.8}/docs/supported-sources/zendesk.md +0 -0
  79. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/main.py +0 -0
  80. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/.gitignore +0 -0
  81. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/airtable/__init__.py +0 -0
  82. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/appsflyer/_init_.py +0 -0
  83. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/appsflyer/client.py +0 -0
  84. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/arrow/__init__.py +0 -0
  85. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/asana_source/__init__.py +0 -0
  86. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/asana_source/helpers.py +0 -0
  87. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/asana_source/settings.py +0 -0
  88. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/chess/__init__.py +0 -0
  89. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/chess/helpers.py +0 -0
  90. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/chess/settings.py +0 -0
  91. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/destinations.py +0 -0
  92. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/dynamodb/__init__.py +0 -0
  93. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/facebook_ads/__init__.py +0 -0
  94. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/facebook_ads/exceptions.py +0 -0
  95. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/facebook_ads/helpers.py +0 -0
  96. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/facebook_ads/settings.py +0 -0
  97. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/filesystem/__init__.py +0 -0
  98. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/filesystem/helpers.py +0 -0
  99. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/filesystem/readers.py +0 -0
  100. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/filters.py +0 -0
  101. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/github/__init__.py +0 -0
  102. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/github/helpers.py +0 -0
  103. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/github/queries.py +0 -0
  104. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/github/settings.py +0 -0
  105. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/google_analytics/__init__.py +0 -0
  106. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/google_analytics/helpers.py +0 -0
  107. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/google_sheets/README.md +0 -0
  108. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/google_sheets/__init__.py +0 -0
  109. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/google_sheets/helpers/__init__.py +0 -0
  110. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/google_sheets/helpers/api_calls.py +0 -0
  111. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/google_sheets/helpers/data_processing.py +0 -0
  112. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/gorgias/__init__.py +0 -0
  113. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/gorgias/helpers.py +0 -0
  114. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/hubspot/__init__.py +0 -0
  115. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/hubspot/helpers.py +0 -0
  116. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/hubspot/settings.py +0 -0
  117. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/kafka/__init__.py +0 -0
  118. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/kafka/helpers.py +0 -0
  119. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/klaviyo/_init_.py +0 -0
  120. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/klaviyo/client.py +0 -0
  121. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/klaviyo/helpers.py +0 -0
  122. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/mongodb/__init__.py +0 -0
  123. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/mongodb/helpers.py +0 -0
  124. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/notion/__init__.py +0 -0
  125. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/notion/helpers/__init__.py +0 -0
  126. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/notion/helpers/client.py +0 -0
  127. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/notion/helpers/database.py +0 -0
  128. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/notion/settings.py +0 -0
  129. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/shopify/__init__.py +0 -0
  130. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/shopify/exceptions.py +0 -0
  131. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/shopify/helpers.py +0 -0
  132. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/shopify/settings.py +0 -0
  133. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/slack/__init__.py +0 -0
  134. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/slack/helpers.py +0 -0
  135. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/slack/settings.py +0 -0
  136. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/sql_database/__init__.py +0 -0
  137. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/sql_database/callbacks.py +0 -0
  138. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/stripe_analytics/__init__.py +0 -0
  139. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/stripe_analytics/helpers.py +0 -0
  140. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/stripe_analytics/settings.py +0 -0
  141. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/table_definition.py +0 -0
  142. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/telemetry/event.py +0 -0
  143. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/testdata/fakebqcredentials.json +0 -0
  144. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/tiktok_ads/__init__.py +0 -0
  145. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/tiktok_ads/tiktok_helpers.py +0 -0
  146. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/time.py +0 -0
  147. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/zendesk/__init__.py +0 -0
  148. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/zendesk/helpers/__init__.py +0 -0
  149. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/zendesk/helpers/api_helpers.py +0 -0
  150. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/zendesk/helpers/credentials.py +0 -0
  151. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/zendesk/helpers/talk_api.py +0 -0
  152. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/src/zendesk/settings.py +0 -0
  153. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/testdata/.gitignore +0 -0
  154. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/testdata/create_replace.csv +0 -0
  155. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/testdata/delete_insert_expected.csv +0 -0
  156. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/testdata/delete_insert_part1.csv +0 -0
  157. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/testdata/delete_insert_part2.csv +0 -0
  158. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/testdata/merge_expected.csv +0 -0
  159. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/testdata/merge_part1.csv +0 -0
  160. {ingestr-0.12.6 → ingestr-0.12.8}/ingestr/testdata/merge_part2.csv +0 -0
  161. {ingestr-0.12.6 → ingestr-0.12.8}/package-lock.json +0 -0
  162. {ingestr-0.12.6 → ingestr-0.12.8}/package.json +0 -0
  163. {ingestr-0.12.6 → ingestr-0.12.8}/pyproject.toml +0 -0
  164. {ingestr-0.12.6 → ingestr-0.12.8}/requirements-dev.txt +0 -0
  165. {ingestr-0.12.6 → ingestr-0.12.8}/resources/demo.gif +0 -0
  166. {ingestr-0.12.6 → ingestr-0.12.8}/resources/demo.tape +0 -0
  167. {ingestr-0.12.6 → ingestr-0.12.8}/resources/ingestr.svg +0 -0
  168. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/AMPM.yml +0 -0
  169. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/Acronyms.yml +0 -0
  170. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/Colons.yml +0 -0
  171. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/Contractions.yml +0 -0
  172. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/DateFormat.yml +0 -0
  173. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/Ellipses.yml +0 -0
  174. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/EmDash.yml +0 -0
  175. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/Exclamation.yml +0 -0
  176. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/FirstPerson.yml +0 -0
  177. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/Gender.yml +0 -0
  178. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/GenderBias.yml +0 -0
  179. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/HeadingPunctuation.yml +0 -0
  180. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/Headings.yml +0 -0
  181. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/Latin.yml +0 -0
  182. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/LyHyphens.yml +0 -0
  183. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/OptionalPlurals.yml +0 -0
  184. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/Ordinal.yml +0 -0
  185. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/OxfordComma.yml +0 -0
  186. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/Parens.yml +0 -0
  187. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/Passive.yml +0 -0
  188. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/Periods.yml +0 -0
  189. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/Quotes.yml +0 -0
  190. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/Ranges.yml +0 -0
  191. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/Semicolons.yml +0 -0
  192. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/Slang.yml +0 -0
  193. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/Spacing.yml +0 -0
  194. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/Spelling.yml +0 -0
  195. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/Units.yml +0 -0
  196. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/We.yml +0 -0
  197. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/Will.yml +0 -0
  198. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/WordList.yml +0 -0
  199. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/meta.json +0 -0
  200. {ingestr-0.12.6 → ingestr-0.12.8}/styles/Google/vocab.txt +0 -0
  201. {ingestr-0.12.6 → ingestr-0.12.8}/styles/bruin/Ingestr.yml +0 -0
  202. {ingestr-0.12.6 → ingestr-0.12.8}/styles/config/vocabularies/bruin/accept.txt +0 -0
@@ -13,11 +13,11 @@ secret_detected() {
13
13
 
14
14
  # use gitleaks binary if available
15
15
  # else fallback to using docker for running gitleaks
16
- CMD="gitleaks dir -v"
16
+ CMD="gitleaks protect --staged -v"
17
17
 
18
18
  if [[ ! `which gitleaks` ]]; then
19
19
  which docker > /dev/null || (echo "gitleaks or docker is required for running secrets scan." && exit 1)
20
- CMD="docker run -v $PWD:$WORK_DIR -w $WORK_DIR --rm ghcr.io/gitleaks/gitleaks:latest dir -v"
20
+ CMD="docker run -v $PWD:$WORK_DIR -w $WORK_DIR --rm ghcr.io/gitleaks/gitleaks:latest protect --staged -v"
21
21
  fi
22
22
 
23
23
  $CMD || secret_detected
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ingestr
3
- Version: 0.12.6
3
+ Version: 0.12.8
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
@@ -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" },
@@ -34,6 +34,7 @@ Adjust source allows ingesting data from various sources:
34
34
 
35
35
  - `campaigns`: Retrieves data for a campaign, showing the app's revenue and network costs over multiple days.
36
36
  - `creatives`: Retrieves data for a creative assets, detailing the app's revenue and network costs across multiple days.
37
+ - `events`: Retrieves data for [events](https://dev.adjust.com/en/api/rs-api/events/) and event slugs.
37
38
  - `custom`: Retrieves custom data based on the dimensions and metrics specified.
38
39
 
39
40
  ### Custom reports: `custom:<dimensions>:<metrics>[:<filters>]`
@@ -0,0 +1,254 @@
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`: optional, application ID of your app. You can specify `app_id` multiple times with different ids to ingest data for multiple apps.
21
+ * You can also define the app_id in the table name. For example, `app-downloads-detailed:12345,67890` will ingest data for app with id `12345` and `67890`.
22
+
23
+ ## Setting up Appstore Integration
24
+
25
+
26
+ ### Prerequisites
27
+ To generate an API key, you must have an Admin account in App Store Connect.
28
+
29
+ ### Generate an API Key
30
+
31
+ To generate a new API key to use with `ingestr`, log in to [App Store Connect](https://appstoreconnect.apple.com/) and:
32
+
33
+ 1. Select Users and Access, and then select the API Keys tab.
34
+ 2. Make sure the Team Keys tab is selected.
35
+ 3. Click Generate API Key or the Add (+) button.
36
+ 4. Enter a name for the key. The name is for your reference only and isn’t part of the key itself.
37
+ 5. Under Access, select the role as `FINANCE`.
38
+ 6. Click Generate.
39
+
40
+ The new key’s name, key ID, a download link, and other information appears on the page.
41
+
42
+ For more information, see [App Store Connect docs](https://developer.apple.com/documentation/appstoreconnectapi/creating-api-keys-for-app-store-connect-api)
43
+
44
+ ### Find your Apps ID
45
+
46
+ You can find the App ID of your app by:
47
+ 1. Opening the app entry in App Store Connect
48
+ 2. Looking for "General Information" in the App information tab
49
+ 3. Finding your App ID under the "Apple ID" entry
50
+
51
+ With this, you are ready to ingest data from App Store.
52
+
53
+ ### Request a Report.
54
+ 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.
55
+
56
+ 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).
57
+
58
+ > [!NOTE]
59
+ > 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.
60
+ ### Example: Loading App Downloads Analytics
61
+
62
+ For this example, we'll assume that:
63
+ * `key_id` is `key_0`
64
+ * `issuer_id` is `issue_0`
65
+ * `key` is stored in the current directory and is named `api.key`
66
+ * `app_id` is `12345`
67
+
68
+ We will run `ingestr` to save this data to a [duckdb](https://duckdb.org/) database called `analytics.db` under the name `public.app_downloads`.
69
+
70
+ ```sh
71
+ ingestr ingest \
72
+ --source-uri "appstore://app_id=12345&key_path=api.key&key_id=key_0&issuer_id=issue_0 \
73
+ --source-table "app-downloads-detailed" \
74
+ --dest-uri "duckdb:///analytics.db" \
75
+ --dest-table "public.app_downloads" \
76
+ ```
77
+
78
+ ### Example: Loading Data for multiple Apps
79
+
80
+ 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.
81
+ ```sh
82
+ ingestr ingest \
83
+ --source-uri "appstore://app_id=12345&app_id=67890&key_path=api.key&key_id=key_0&issuer_id=issue_0 \
84
+ --source-table "app-downloads-detailed" \
85
+ --dest-uri "duckdb:///analytics.db" \
86
+ --dest-table "public.app_downloads" \
87
+ ```
88
+
89
+
90
+ ### Example: Incremental Loading
91
+
92
+ `ingestr` supports incremental loading for all App Store tables.
93
+
94
+ 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)
95
+ ```sh
96
+ ingestr ingest \
97
+ --source-uri "appstore://app_id=12345&key_path=api.key&key_id=key_0&issuer_id=issue_0 \
98
+ --source-table "app-downloads-detailed" \
99
+ --dest-uri "duckdb:///analytics.db" \
100
+ --dest-table "public.app_downloads" \
101
+ --interval-end "2025-01-01"
102
+ ```
103
+
104
+ `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.
105
+
106
+ ```sh
107
+ ingestr ingest \
108
+ --source-uri "appstore://app_id=12345&key_path=api.key&key_id=key_0&issuer_id=issue_0 \
109
+ --source-table "app-downloads-detailed" \
110
+ --dest-uri "duckdb:///analytics.db" \
111
+ --dest-table "public.app_downloads" \
112
+ --incremental-strategy "merge"
113
+ ```
114
+
115
+ 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.
116
+
117
+ ## Tables
118
+
119
+ ### `app-downloads-detailed`
120
+ 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.
121
+
122
+ | **Column** | **Description** |
123
+ |--------------|-----------------|
124
+ | `date` | Date on which the event occurred. |
125
+ | `app_name` | The name of the app provided by you during app setup in App Store Connect.|
126
+ | `app_apple_identifier` | Your app’s Apple ID. |
127
+ | `download_type` | The type of download event that occured. |
128
+ | `app_version` | The app version being downloaded. |
129
+ | `device` | The device on which the app was downloaded.|
130
+ | `platform_version` | The OS version of the device on which the download occured.|
131
+ | `source_type` | The source from where the user discovered the app.|
132
+ | `source_info` | The app referrer or web referrer that led the user to discover the app.|
133
+ | `campaign` | The Campaign Token of the campaign created in App Analytics. Column available starting November 19, 2024.|
134
+ | `page_type` | The page type from where the app was downloaded. |
135
+ | `page_title` | The name of the product page or in-app event page that led the user to download the app.|
136
+ | `pre-order` | A flag indicating whether the download came from a pre-order.|
137
+ | `territory` | The App Store country or region where the download occured.|
138
+ | `counts` | The total number of downloads.|
139
+
140
+
141
+ ### `app-store-discovery-and-engagement-detailed`
142
+ 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.
143
+
144
+
145
+ | **Column** | **Description** |
146
+ |--------------|-----------------|
147
+ | `date` | Date on which the event occurred. |
148
+ | `app_name` | The name of the app provided by you during app setup in App Store Connect.|
149
+ | `app_apple_identifier` | Your app’s Apple ID. |
150
+ | `event` | The type of event that occurred.|
151
+ | `source_type` | The source from where the user discovered the app. |
152
+ | `source_info` | The app referrer or web referrer that led the user to discover the app.|
153
+ | `campaign` | The Campaign Token of the campaign created in App Analytics. Column available starting November 19, 2024.|
154
+ | `page_type` | The page type from where the app was downloaded. |
155
+ | `page_title` | The name of the product page or in-app event page that led the user to download the app.|
156
+ | `device` | The device on which the event occurred. |
157
+ | `engagement_type` | User action, if any, on the impression or page.|
158
+ | `platform_version` | The OS version of the device on which the event occurred.|
159
+ | `territory` | The App Store country or region where the download occured.|
160
+ | `counts` | The total number of events that occurred. |
161
+ | `unique_counts` | The total number of unique users that performed the event.|
162
+
163
+ ### `app-sessions-detailed`
164
+ App Session provides insights on how often people open your app, and how long they spend in your app.
165
+
166
+ | **Column** | **Description** |
167
+ |--------------|-----------------|
168
+ | `date` | Date on which the event occurred. |
169
+ | `app_name` | The name of the app provided by you during app setup in App Store Connect.|
170
+ | `app_apple_identifier` | Your app’s Apple ID. |
171
+ | `download_type` | The type of download event that occured. |
172
+ | `app_version` | The app version being downloaded. |
173
+ | `device` | The device on which the app was downloaded.|
174
+ | `platform_version` | The OS version of the device on which the download occured.|
175
+ | `source_type` | The source from where the user discovered the app.|
176
+ | `source_info` | The app referrer or web referrer that led the user to discover the app.|
177
+ | `campaign` | The Campaign Token of the campaign created in App Analytics. Column available starting November 19, 2024.|
178
+ | `page_type` | The page type from where the app was downloaded. |
179
+ | `page_title` | The name of the product page or in-app event page that led the user to download the app.|
180
+ | `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.|
181
+ | `territory` | The App Store country or region where the download occured.|
182
+ | `sessions` | The number of sessions. Based on users who have agreed to share their data with Apple and developers.|
183
+ | `total_session_duration` | The total duration, in seconds, of all sessions being reported.|
184
+ | `unique_devices` | The number of unique devices contributing to the total number of sessions being reported.|
185
+
186
+ ### `app-store-installation-and-deletion-detailed`
187
+ Use the data in App Store Installation and Deletion report to estimate the number of times people install and delete your App Store apps.
188
+
189
+
190
+ | **Column** | **Description** |
191
+ |--------------|-----------------|
192
+ | `date`| Date on which the event occurred.|
193
+ | `app_name`| The name of the app provided by you during app setup in App Store Connect.|
194
+ | `app_apple_identifier`| Date on which the event occurred.|
195
+ | `event`| The type of usage event that occurred. |
196
+ | `download_type`| The type of download event that occurred.|
197
+ | `app_version`| The version of the app being associated with the instalation or deletion.|
198
+ | `device`| The device on which the app was installed or deleted.|
199
+ | `platform_version`| The OS version of the device on which the app was installed or deleted.|
200
+ | `source_type`| Where the user discovered your app.|
201
+ | `source_info`| The app referrer or web referrer that led the user to discover your app.|
202
+ | `campaign`| The Campaign Token of the campaign created in App Analytics. Column available starting November 19, 2024.|
203
+ | `page_type`| The page type which led the user to discover your app.|
204
+ | `page_title`| The name of the product page or in-app event page that led the user to discover your app.|
205
+ | `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.|
206
+ | `territory`| The App Store country or region where the installation or deletion occurred.|
207
+ | `counts`| The total count of events, based on users who have agreed to share their data with Apple and developers.|
208
+ | `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.|
209
+
210
+ ### `app-store-purchases-detailed`
211
+ 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.
212
+
213
+ | **Column** | **Description** |
214
+ |--------------|-----------------|
215
+ | `date`| Date on which the event occurred.|
216
+ | `app_name`| The name of the app provided by you during app setup in App Store Connect.|
217
+ | `app_apple_identifier`| Your app’s Apple ID.|
218
+ | `purchase_type`| The type of purchase made by the user on the App Store.|
219
+ | `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.|
220
+ | `content_apple_identifier`| Your content’s Apple ID |
221
+ | `payment_method`| The payment type used to charge the customer.|
222
+ | `device`| The device on which the purchase occurred.|
223
+ | `platform_version`| The OS version of the device on which the app was installed or deleted.|
224
+ | `source_type`| Where the user discovered your app.|
225
+ | `source_info`| The app referrer or web referrer that led the user to discover your app.|
226
+ | `campaign`| The Campaign Token of the campaign created in App Analytics. Column available starting November 19, 2024.|
227
+ | `page_type`| The page type which led the user to discover your app.|
228
+ | `page_title`| The name of the product page or in-app event page that led the user to discover your app.|
229
+ | `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.|
230
+ | `pre-order`| Indicates whether the purchase originated from someone who pre-ordered the app.|
231
+ | `territory`| The App Store country or region in which the purchase occurred.|
232
+ | `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.|
233
+ | `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.|
234
+ | `sales_in_usd`| The estimated sales in USD from purchases of your app and in-app purchases.|
235
+ | `paying_users`| The number of unique users who paid for your app or in-app purchases. This metric is not summable across rows.|
236
+
237
+ ### `app-crashes-expanded`
238
+ Use this report to understand crashes for your App Store apps by app version and device type.
239
+
240
+ | **Column** | **Description** |
241
+ |--------------|-----------------|
242
+ | `date`| Date on which the event occurred.|
243
+ | `app_name`| The name of the app provided by you during app setup in App Store Connect.|
244
+ | `app_apple_identifier`| Your app’s Apple ID.|
245
+ | `app_version` | The app version being downloaded. |
246
+ | `device` | The device on which the app was downloaded.|
247
+ | `platform_version` | The OS version of the device on which the download occured.|
248
+ | `crashes` | The total number of crashes.|
249
+ | `unique_devices` | Number of unique devices where app crashed. |
250
+
251
+
252
+ Use these as `--source-table` parameter in the `ingestr ingest` command.
253
+
254
+ To know more about these reports and their dimensions, see [App Store Analytics docs](https://developer.apple.com/documentation/analytics-reports).
@@ -56,6 +56,11 @@ def adjust_source(
56
56
  filters=filters,
57
57
  )
58
58
 
59
+ @dlt.resource(write_disposition="replace", primary_key="id")
60
+ def events():
61
+ adjust_api = AdjustAPI(api_key=api_key)
62
+ yield adjust_api.fetch_events()
63
+
59
64
  @dlt.resource(write_disposition="merge", merge_key="day")
60
65
  def creatives():
61
66
  adjust_api = AdjustAPI(api_key=api_key)
@@ -68,7 +73,7 @@ def adjust_source(
68
73
  )
69
74
 
70
75
  if not dimensions:
71
- return campaigns, creatives
76
+ return campaigns, creatives, events
72
77
 
73
78
  merge_key = merge_key
74
79
  type_hints = {}
@@ -100,4 +105,4 @@ def adjust_source(
100
105
  filters=filters,
101
106
  )
102
107
 
103
- return campaigns, creatives, custom
108
+ return campaigns, creatives, custom, events
@@ -28,10 +28,20 @@ DEFAULT_METRICS = [
28
28
  ]
29
29
 
30
30
 
31
+ def retry_on_limit(response: requests.Response, exception: BaseException) -> bool:
32
+ return response.status_code == 429
33
+
34
+
31
35
  class AdjustAPI:
32
36
  def __init__(self, api_key):
33
37
  self.api_key = api_key
34
- self.uri = "https://automate.adjust.com/reports-service/report"
38
+ self.request_client = Client(
39
+ request_timeout=8.0,
40
+ raise_for_status=False,
41
+ retry_condition=retry_on_limit,
42
+ request_max_attempts=12,
43
+ request_backoff_factor=2,
44
+ ).session
35
45
 
36
46
  def fetch_report_data(
37
47
  self,
@@ -62,20 +72,11 @@ class AdjustAPI:
62
72
  f"Invalid date range: Start date ({start_date}) must be earlier than end date ({end_date})."
63
73
  )
64
74
 
65
- def retry_on_limit(
66
- response: requests.Response, exception: BaseException
67
- ) -> bool:
68
- return response.status_code == 429
69
-
70
- request_client = Client(
71
- request_timeout=8.0,
72
- raise_for_status=False,
73
- retry_condition=retry_on_limit,
74
- request_max_attempts=12,
75
- request_backoff_factor=2,
76
- ).session
77
-
78
- response = request_client.get(self.uri, headers=headers, params=params)
75
+ response = self.request_client.get(
76
+ "https://automate.adjust.com/reports-service/report",
77
+ headers=headers,
78
+ params=params,
79
+ )
79
80
  if response.status_code == 200:
80
81
  result = response.json()
81
82
  items = result.get("rows", [])
@@ -83,6 +84,17 @@ class AdjustAPI:
83
84
  else:
84
85
  raise HTTPError(f"Request failed with status code: {response.status_code}")
85
86
 
87
+ def fetch_events(self):
88
+ headers = {"Authorization": f"Bearer {self.api_key}"}
89
+ response = self.request_client.get(
90
+ "https://automate.adjust.com/reports-service/events", headers=headers
91
+ )
92
+ if response.status_code == 200:
93
+ result = response.json()
94
+ yield result
95
+ else:
96
+ raise HTTPError(f"Request failed with status code: {response.status_code}")
97
+
86
98
 
87
99
  def parse_filters(filters_raw: str) -> dict:
88
100
  # Parse filter string like "key1=value1,key2=value2,value3,value4"
@@ -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}")