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