ingestr 0.13.79__tar.gz → 0.13.81__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 (323) hide show
  1. {ingestr-0.13.79 → ingestr-0.13.81}/PKG-INFO +1 -1
  2. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/facebook-ads.md +63 -5
  3. ingestr-0.13.81/ingestr/src/buildinfo.py +1 -0
  4. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/facebook_ads/__init__.py +15 -16
  5. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/facebook_ads/helpers.py +47 -1
  6. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/linear/__init__.py +62 -42
  7. ingestr-0.13.81/ingestr/src/linear/helpers.py +53 -0
  8. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/sources.py +110 -20
  9. ingestr-0.13.79/ingestr/src/buildinfo.py +0 -1
  10. ingestr-0.13.79/ingestr/src/linear/helpers.py +0 -72
  11. {ingestr-0.13.79 → ingestr-0.13.81}/.dlt/config.toml +0 -0
  12. {ingestr-0.13.79 → ingestr-0.13.81}/.dockerignore +0 -0
  13. {ingestr-0.13.79 → ingestr-0.13.81}/.githooks/pre-commit-hook.sh +0 -0
  14. {ingestr-0.13.79 → ingestr-0.13.81}/.github/workflows/deploy-docs.yml +0 -0
  15. {ingestr-0.13.79 → ingestr-0.13.81}/.github/workflows/release.yml +0 -0
  16. {ingestr-0.13.79 → ingestr-0.13.81}/.github/workflows/secrets-scan.yml +0 -0
  17. {ingestr-0.13.79 → ingestr-0.13.81}/.github/workflows/tests.yml +0 -0
  18. {ingestr-0.13.79 → ingestr-0.13.81}/.gitignore +0 -0
  19. {ingestr-0.13.79 → ingestr-0.13.81}/.gitleaksignore +0 -0
  20. {ingestr-0.13.79 → ingestr-0.13.81}/.python-version +0 -0
  21. {ingestr-0.13.79 → ingestr-0.13.81}/.vale.ini +0 -0
  22. {ingestr-0.13.79 → ingestr-0.13.81}/Dockerfile +0 -0
  23. {ingestr-0.13.79 → ingestr-0.13.81}/LICENSE.md +0 -0
  24. {ingestr-0.13.79 → ingestr-0.13.81}/Makefile +0 -0
  25. {ingestr-0.13.79 → ingestr-0.13.81}/README.md +0 -0
  26. {ingestr-0.13.79 → ingestr-0.13.81}/docs/.vitepress/config.mjs +0 -0
  27. {ingestr-0.13.79 → ingestr-0.13.81}/docs/.vitepress/theme/custom.css +0 -0
  28. {ingestr-0.13.79 → ingestr-0.13.81}/docs/.vitepress/theme/index.js +0 -0
  29. {ingestr-0.13.79 → ingestr-0.13.81}/docs/commands/example-uris.md +0 -0
  30. {ingestr-0.13.79 → ingestr-0.13.81}/docs/commands/ingest.md +0 -0
  31. {ingestr-0.13.79 → ingestr-0.13.81}/docs/getting-started/core-concepts.md +0 -0
  32. {ingestr-0.13.79 → ingestr-0.13.81}/docs/getting-started/incremental-loading.md +0 -0
  33. {ingestr-0.13.79 → ingestr-0.13.81}/docs/getting-started/quickstart.md +0 -0
  34. {ingestr-0.13.79 → ingestr-0.13.81}/docs/getting-started/telemetry.md +0 -0
  35. {ingestr-0.13.79 → ingestr-0.13.81}/docs/index.md +0 -0
  36. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/applovin_max.png +0 -0
  37. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/athena.png +0 -0
  38. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/clickhouse_img.png +0 -0
  39. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/clickup_ingestion.png +0 -0
  40. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/cratedb-destination.png +0 -0
  41. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/cratedb-source.png +0 -0
  42. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/freshdesk_ingestion.png +0 -0
  43. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/gcp_spanner_ingestion.png +0 -0
  44. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/github.png +0 -0
  45. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/google_analytics_realtime_report.png +0 -0
  46. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/googleanalytics.png +0 -0
  47. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/ingestion_elasticsearch_img.png +0 -0
  48. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/kinesis.bigquery.png +0 -0
  49. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/linear.png +0 -0
  50. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/linkedin_ads.png +0 -0
  51. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/mixpanel_ingestion.png +0 -0
  52. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/personio.png +0 -0
  53. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/personio_duckdb.png +0 -0
  54. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/phantombuster.png +0 -0
  55. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/pipedrive.png +0 -0
  56. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/quickbook_ingestion.png +0 -0
  57. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/sftp.png +0 -0
  58. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/stripe_postgres.png +0 -0
  59. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/tiktok.png +0 -0
  60. {ingestr-0.13.79 → ingestr-0.13.81}/docs/media/zoom_ingestion.png +0 -0
  61. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/adjust.md +0 -0
  62. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/airtable.md +0 -0
  63. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/applovin.md +0 -0
  64. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/applovin_max.md +0 -0
  65. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/appsflyer.md +0 -0
  66. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/appstore.md +0 -0
  67. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/asana.md +0 -0
  68. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/athena.md +0 -0
  69. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/attio.md +0 -0
  70. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/bigquery.md +0 -0
  71. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/chess.md +0 -0
  72. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/clickhouse.md +0 -0
  73. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/clickup.md +0 -0
  74. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/cratedb.md +0 -0
  75. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/csv.md +0 -0
  76. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/custom_queries.md +0 -0
  77. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/databricks.md +0 -0
  78. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/db2.md +0 -0
  79. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/duckdb.md +0 -0
  80. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/dynamodb.md +0 -0
  81. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/elasticsearch.md +0 -0
  82. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/frankfurter.md +0 -0
  83. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/freshdesk.md +0 -0
  84. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/gcs.md +0 -0
  85. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/github.md +0 -0
  86. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/google-ads.md +0 -0
  87. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/google_analytics.md +0 -0
  88. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/gorgias.md +0 -0
  89. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/gsheets.md +0 -0
  90. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/hubspot.md +0 -0
  91. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/influxdb.md +0 -0
  92. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/isoc-pulse.md +0 -0
  93. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/kafka.md +0 -0
  94. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/kinesis.md +0 -0
  95. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/klaviyo.md +0 -0
  96. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/linear.md +0 -0
  97. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/linkedin_ads.md +0 -0
  98. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/mixpanel.md +0 -0
  99. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/mongodb.md +0 -0
  100. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/motherduck.md +0 -0
  101. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/mssql.md +0 -0
  102. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/mysql.md +0 -0
  103. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/notion.md +0 -0
  104. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/oracle.md +0 -0
  105. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/personio.md +0 -0
  106. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/phantombuster.md +0 -0
  107. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/pinterest.md +0 -0
  108. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/pipedrive.md +0 -0
  109. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/postgres.md +0 -0
  110. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/quickbooks.md +0 -0
  111. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/redshift.md +0 -0
  112. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/s3.md +0 -0
  113. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/salesforce.md +0 -0
  114. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/sap-hana.md +0 -0
  115. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/sftp.md +0 -0
  116. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/shopify.md +0 -0
  117. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/slack.md +0 -0
  118. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/smartsheets.md +0 -0
  119. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/snowflake.md +0 -0
  120. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/solidgate.md +0 -0
  121. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/spanner.md +0 -0
  122. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/sqlite.md +0 -0
  123. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/stripe.md +0 -0
  124. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/tiktok-ads.md +0 -0
  125. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/trustpilot.md +0 -0
  126. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/zendesk.md +0 -0
  127. {ingestr-0.13.79 → ingestr-0.13.81}/docs/supported-sources/zoom.md +0 -0
  128. {ingestr-0.13.79 → ingestr-0.13.81}/docs/tutorials/load-kinesis-bigquery.md +0 -0
  129. {ingestr-0.13.79 → ingestr-0.13.81}/docs/tutorials/load-personio-duckdb.md +0 -0
  130. {ingestr-0.13.79 → ingestr-0.13.81}/docs/tutorials/load-stripe-postgres.md +0 -0
  131. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/conftest.py +0 -0
  132. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/main.py +0 -0
  133. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/.gitignore +0 -0
  134. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/adjust/__init__.py +0 -0
  135. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/adjust/adjust_helpers.py +0 -0
  136. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/airtable/__init__.py +0 -0
  137. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/applovin/__init__.py +0 -0
  138. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/applovin_max/__init__.py +0 -0
  139. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/appsflyer/__init__.py +0 -0
  140. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/appsflyer/client.py +0 -0
  141. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/appstore/__init__.py +0 -0
  142. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/appstore/client.py +0 -0
  143. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/appstore/errors.py +0 -0
  144. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/appstore/models.py +0 -0
  145. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/appstore/resources.py +0 -0
  146. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/arrow/__init__.py +0 -0
  147. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/asana_source/__init__.py +0 -0
  148. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/asana_source/helpers.py +0 -0
  149. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/asana_source/settings.py +0 -0
  150. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/attio/__init__.py +0 -0
  151. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/attio/helpers.py +0 -0
  152. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/blob.py +0 -0
  153. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/chess/__init__.py +0 -0
  154. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/chess/helpers.py +0 -0
  155. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/chess/settings.py +0 -0
  156. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/clickup/__init__.py +0 -0
  157. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/clickup/helpers.py +0 -0
  158. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/collector/spinner.py +0 -0
  159. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/destinations.py +0 -0
  160. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/dynamodb/__init__.py +0 -0
  161. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/elasticsearch/__init__.py +0 -0
  162. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/errors.py +0 -0
  163. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/facebook_ads/exceptions.py +0 -0
  164. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/facebook_ads/settings.py +0 -0
  165. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/facebook_ads/utils.py +0 -0
  166. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/factory.py +0 -0
  167. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/filesystem/__init__.py +0 -0
  168. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/filesystem/helpers.py +0 -0
  169. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/filesystem/readers.py +0 -0
  170. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/filters.py +0 -0
  171. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/frankfurter/__init__.py +0 -0
  172. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/frankfurter/helpers.py +0 -0
  173. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/freshdesk/__init__.py +0 -0
  174. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/freshdesk/freshdesk_client.py +0 -0
  175. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/freshdesk/settings.py +0 -0
  176. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/github/__init__.py +0 -0
  177. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/github/helpers.py +0 -0
  178. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/github/queries.py +0 -0
  179. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/github/settings.py +0 -0
  180. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/google_ads/__init__.py +0 -0
  181. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/google_ads/field.py +0 -0
  182. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/google_ads/metrics.py +0 -0
  183. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/google_ads/predicates.py +0 -0
  184. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/google_ads/reports.py +0 -0
  185. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/google_analytics/__init__.py +0 -0
  186. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/google_analytics/helpers.py +0 -0
  187. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/google_sheets/README.md +0 -0
  188. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/google_sheets/__init__.py +0 -0
  189. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/google_sheets/helpers/__init__.py +0 -0
  190. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/google_sheets/helpers/api_calls.py +0 -0
  191. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/google_sheets/helpers/data_processing.py +0 -0
  192. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/gorgias/__init__.py +0 -0
  193. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/gorgias/helpers.py +0 -0
  194. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/http_client.py +0 -0
  195. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/hubspot/__init__.py +0 -0
  196. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/hubspot/helpers.py +0 -0
  197. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/hubspot/settings.py +0 -0
  198. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/influxdb/__init__.py +0 -0
  199. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/influxdb/client.py +0 -0
  200. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/isoc_pulse/__init__.py +0 -0
  201. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/kafka/__init__.py +0 -0
  202. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/kafka/helpers.py +0 -0
  203. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/kinesis/__init__.py +0 -0
  204. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/kinesis/helpers.py +0 -0
  205. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/klaviyo/__init__.py +0 -0
  206. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/klaviyo/client.py +0 -0
  207. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/klaviyo/helpers.py +0 -0
  208. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/linkedin_ads/__init__.py +0 -0
  209. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/linkedin_ads/dimension_time_enum.py +0 -0
  210. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/linkedin_ads/helpers.py +0 -0
  211. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/loader.py +0 -0
  212. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/mixpanel/__init__.py +0 -0
  213. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/mixpanel/client.py +0 -0
  214. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/mongodb/__init__.py +0 -0
  215. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/mongodb/helpers.py +0 -0
  216. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/notion/__init__.py +0 -0
  217. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/notion/helpers/__init__.py +0 -0
  218. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/notion/helpers/client.py +0 -0
  219. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/notion/helpers/database.py +0 -0
  220. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/notion/settings.py +0 -0
  221. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/partition.py +0 -0
  222. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/personio/__init__.py +0 -0
  223. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/personio/helpers.py +0 -0
  224. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/phantombuster/__init__.py +0 -0
  225. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/phantombuster/client.py +0 -0
  226. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/pinterest/__init__.py +0 -0
  227. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/pipedrive/__init__.py +0 -0
  228. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/pipedrive/helpers/__init__.py +0 -0
  229. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/pipedrive/helpers/custom_fields_munger.py +0 -0
  230. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/pipedrive/helpers/pages.py +0 -0
  231. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/pipedrive/settings.py +0 -0
  232. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/pipedrive/typing.py +0 -0
  233. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/quickbooks/__init__.py +0 -0
  234. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/resource.py +0 -0
  235. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/salesforce/__init__.py +0 -0
  236. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/salesforce/helpers.py +0 -0
  237. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/shopify/__init__.py +0 -0
  238. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/shopify/exceptions.py +0 -0
  239. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/shopify/helpers.py +0 -0
  240. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/shopify/settings.py +0 -0
  241. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/slack/__init__.py +0 -0
  242. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/slack/helpers.py +0 -0
  243. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/slack/settings.py +0 -0
  244. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/smartsheets/__init__.py +0 -0
  245. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/solidgate/__init__.py +0 -0
  246. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/solidgate/helpers.py +0 -0
  247. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/sql_database/__init__.py +0 -0
  248. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/sql_database/callbacks.py +0 -0
  249. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/stripe_analytics/__init__.py +0 -0
  250. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/stripe_analytics/helpers.py +0 -0
  251. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/stripe_analytics/settings.py +0 -0
  252. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/table_definition.py +0 -0
  253. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/telemetry/event.py +0 -0
  254. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/testdata/fakebqcredentials.json +0 -0
  255. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/tiktok_ads/__init__.py +0 -0
  256. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/tiktok_ads/tiktok_helpers.py +0 -0
  257. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/time.py +0 -0
  258. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/trustpilot/__init__.py +0 -0
  259. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/trustpilot/client.py +0 -0
  260. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/version.py +0 -0
  261. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/zendesk/__init__.py +0 -0
  262. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/zendesk/helpers/__init__.py +0 -0
  263. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/zendesk/helpers/api_helpers.py +0 -0
  264. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/zendesk/helpers/credentials.py +0 -0
  265. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/zendesk/helpers/talk_api.py +0 -0
  266. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/zendesk/settings.py +0 -0
  267. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/zoom/__init__.py +0 -0
  268. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/src/zoom/helpers.py +0 -0
  269. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/testdata/.gitignore +0 -0
  270. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/testdata/create_replace.csv +0 -0
  271. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/testdata/delete_insert_expected.csv +0 -0
  272. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/testdata/delete_insert_part1.csv +0 -0
  273. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/testdata/delete_insert_part2.csv +0 -0
  274. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/testdata/merge_expected.csv +0 -0
  275. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/testdata/merge_part1.csv +0 -0
  276. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/testdata/merge_part2.csv +0 -0
  277. {ingestr-0.13.79 → ingestr-0.13.81}/ingestr/tests/unit/test_smartsheets.py +0 -0
  278. {ingestr-0.13.79 → ingestr-0.13.81}/package-lock.json +0 -0
  279. {ingestr-0.13.79 → ingestr-0.13.81}/package.json +0 -0
  280. {ingestr-0.13.79 → ingestr-0.13.81}/pyproject.toml +0 -0
  281. {ingestr-0.13.79 → ingestr-0.13.81}/requirements-dev.txt +0 -0
  282. {ingestr-0.13.79 → ingestr-0.13.81}/requirements.in +0 -0
  283. {ingestr-0.13.79 → ingestr-0.13.81}/requirements.txt +0 -0
  284. {ingestr-0.13.79 → ingestr-0.13.81}/requirements_arm64.txt +0 -0
  285. {ingestr-0.13.79 → ingestr-0.13.81}/resources/demo.gif +0 -0
  286. {ingestr-0.13.79 → ingestr-0.13.81}/resources/demo.tape +0 -0
  287. {ingestr-0.13.79 → ingestr-0.13.81}/resources/ingestr.svg +0 -0
  288. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/AMPM.yml +0 -0
  289. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/Acronyms.yml +0 -0
  290. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/Colons.yml +0 -0
  291. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/Contractions.yml +0 -0
  292. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/DateFormat.yml +0 -0
  293. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/Ellipses.yml +0 -0
  294. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/EmDash.yml +0 -0
  295. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/Exclamation.yml +0 -0
  296. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/FirstPerson.yml +0 -0
  297. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/Gender.yml +0 -0
  298. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/GenderBias.yml +0 -0
  299. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/HeadingPunctuation.yml +0 -0
  300. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/Headings.yml +0 -0
  301. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/Latin.yml +0 -0
  302. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/LyHyphens.yml +0 -0
  303. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/OptionalPlurals.yml +0 -0
  304. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/Ordinal.yml +0 -0
  305. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/OxfordComma.yml +0 -0
  306. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/Parens.yml +0 -0
  307. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/Passive.yml +0 -0
  308. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/Periods.yml +0 -0
  309. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/Quotes.yml +0 -0
  310. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/Ranges.yml +0 -0
  311. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/Semicolons.yml +0 -0
  312. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/Slang.yml +0 -0
  313. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/Spacing.yml +0 -0
  314. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/Spelling.yml +0 -0
  315. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/Units.yml +0 -0
  316. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/We.yml +0 -0
  317. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/Will.yml +0 -0
  318. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/WordList.yml +0 -0
  319. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/meta.json +0 -0
  320. {ingestr-0.13.79 → ingestr-0.13.81}/styles/Google/vocab.txt +0 -0
  321. {ingestr-0.13.79 → ingestr-0.13.81}/styles/bruin/Ingestr.yml +0 -0
  322. {ingestr-0.13.79 → ingestr-0.13.81}/styles/config/vocabularies/bruin/accept.txt +0 -0
  323. {ingestr-0.13.79 → ingestr-0.13.81}/test.env.template +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ingestr
3
- Version: 0.13.79
3
+ Version: 0.13.81
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
@@ -160,13 +160,19 @@ The `facebook_insights` table supports advanced configuration for breakdowns and
160
160
  1. **Default usage**: `facebook_insights`
161
161
  - Uses default breakdown and default fields
162
162
 
163
- 2. **Custom breakdown**: `facebook_insights:breakdown_type`
164
- - Uses specified breakdown with default fields
163
+ 2. **Predefined breakdown**: `facebook_insights:breakdown_type`
164
+ - Uses specified predefined breakdown with default fields
165
165
 
166
- 3. **Custom breakdown + metrics**: `facebook_insights:breakdown_type:metric1,metric2,metric3`
167
- - Uses specified breakdown with custom metrics
166
+ 3. **Predefined breakdown + custom metrics**: `facebook_insights:breakdown_type:metric1,metric2,metric3`
167
+ - Uses specified predefined breakdown with custom metrics
168
168
 
169
- #### Available Breakdown Types
169
+ 4. **Custom dimensions + metrics**: `facebook_insights:dimension1,dimension2:metric1,metric2,metric3`
170
+ - Uses custom dimensions with custom metrics
171
+
172
+ 5. **Level + dimensions + metrics**: `facebook_insights:level,dimension1,dimension2:metric1,metric2,metric3`
173
+ - Uses specified level with custom dimensions and metrics
174
+
175
+ #### Available Predefined Breakdown Types
170
176
 
171
177
  - `ads_insights` (default)
172
178
  - `ads_insights_age_and_gender`
@@ -176,6 +182,30 @@ The `facebook_insights` table supports advanced configuration for breakdowns and
176
182
  - `ads_insights_dma`
177
183
  - `ads_insights_hourly_advertiser`
178
184
 
185
+ #### Available Levels
186
+
187
+ When using custom dimensions, you can specify one of these levels:
188
+ - `account` - Account level insights
189
+ - `campaign` - Campaign level insights
190
+ - `adset` - Ad set level insights
191
+ - `ad` - Ad level insights
192
+
193
+ Note: If multiple levels are specified in the dimensions list, the last valid level will be used and removed from the dimensions list.
194
+
195
+ #### Common Dimensions
196
+
197
+ You can use any valid Facebook Ads dimension in your custom configurations. Some commonly used dimensions include:
198
+ - `age` - Age ranges
199
+ - `gender` - Gender breakdown
200
+ - `country` - Country breakdown
201
+ - `region` - Region breakdown
202
+ - `platform_position` - Platform position
203
+ - `publisher_platform` - Publisher platform
204
+ - `impression_device` - Device type
205
+ - `placement` - Ad placement
206
+
207
+ Note: Not all dimension combinations are valid according to Facebook's API. Refer to [Facebook's Marketing API](https://developers.facebook.com/docs/marketing-api/insights/breakdowns/) documentation for valid dimension combinations.
208
+
179
209
  #### Examples
180
210
 
181
211
  ```sh
@@ -199,4 +229,32 @@ ingestr ingest \
199
229
  --source-table 'facebook_insights:ads_insights_country:impressions,clicks,spend,reach,cpm,ctr' \
200
230
  --dest-uri 'duckdb:///facebook.duckdb' \
201
231
  --dest-table 'dest.insights_by_country'
232
+
233
+ # Custom dimensions (age and gender) with custom metrics
234
+ ingestr ingest \
235
+ --source-uri 'facebookads://?access_token=easdyh&account_id=1234' \
236
+ --source-table 'facebook_insights:age,gender:impressions,clicks,spend' \
237
+ --dest-uri 'duckdb:///facebook.duckdb' \
238
+ --dest-table 'dest.insights_custom_dimensions'
239
+
240
+ # Campaign level with custom dimensions and metrics
241
+ ingestr ingest \
242
+ --source-uri 'facebookads://?access_token=easdyh&account_id=1234' \
243
+ --source-table 'facebook_insights:campaign,age,gender:impressions,clicks,spend,reach' \
244
+ --dest-uri 'duckdb:///facebook.duckdb' \
245
+ --dest-table 'dest.campaign_insights_demographics'
246
+
247
+ # Ad level with geographic dimensions
248
+ ingestr ingest \
249
+ --source-uri 'facebookads://?access_token=easdyh&account_id=1234' \
250
+ --source-table 'facebook_insights:ad,country,region:clicks,impressions,spend' \
251
+ --dest-uri 'duckdb:///facebook.duckdb' \
252
+ --dest-table 'dest.ad_insights_geographic'
253
+
254
+ # Account level insights only (no additional dimensions)
255
+ ingestr ingest \
256
+ --source-uri 'facebookads://?access_token=easdyh&account_id=1234' \
257
+ --source-table 'facebook_insights:account:impressions,clicks,spend,reach' \
258
+ --dest-uri 'duckdb:///facebook.duckdb' \
259
+ --dest-table 'dest.account_level_insights'
202
260
  ```
@@ -0,0 +1 @@
1
+ version = "v0.13.81"
@@ -22,12 +22,8 @@ from .settings import (
22
22
  DEFAULT_ADCREATIVE_FIELDS,
23
23
  DEFAULT_ADSET_FIELDS,
24
24
  DEFAULT_CAMPAIGN_FIELDS,
25
- DEFAULT_INSIGHT_FIELDS,
26
25
  DEFAULT_LEAD_FIELDS,
27
26
  INSIGHT_FIELDS_TYPES,
28
- INSIGHTS_BREAKDOWNS_OPTIONS,
29
- INVALID_INSIGHTS_FIELDS,
30
- TInsightsBreakdownOptions,
31
27
  TInsightsLevels,
32
28
  )
33
29
 
@@ -105,10 +101,9 @@ def facebook_insights_source(
105
101
  account_id: str = dlt.config.value,
106
102
  access_token: str = dlt.secrets.value,
107
103
  initial_load_past_days: int = 1,
108
- fields: Sequence[str] = DEFAULT_INSIGHT_FIELDS,
109
- attribution_window_days_lag: int = 7,
104
+ dimensions: Sequence[str] = None,
105
+ fields: Sequence[str] = None,
110
106
  time_increment_days: int = 1,
111
- breakdowns: TInsightsBreakdownOptions = "ads_insights",
112
107
  action_breakdowns: Sequence[str] = ALL_ACTION_BREAKDOWNS,
113
108
  level: TInsightsLevels = "ad",
114
109
  action_attribution_windows: Sequence[str] = ALL_ACTION_ATTRIBUTION_WINDOWS,
@@ -117,6 +112,9 @@ def facebook_insights_source(
117
112
  app_api_version: str = None,
118
113
  start_date: pendulum.DateTime | None = None,
119
114
  end_date: pendulum.DateTime | None = None,
115
+ insights_max_wait_to_finish_seconds: int = 60 * 60 * 4,
116
+ insights_max_wait_to_start_seconds: int = 60 * 30,
117
+ insights_max_async_sleep_seconds: int = 20,
120
118
  ) -> DltResource:
121
119
  """Incrementally loads insight reports with defined granularity level, fields, breakdowns etc.
122
120
 
@@ -152,6 +150,11 @@ def facebook_insights_source(
152
150
  if start_date is None:
153
151
  start_date = pendulum.today().subtract(days=initial_load_past_days)
154
152
 
153
+ if dimensions is None:
154
+ dimensions = []
155
+ if fields is None:
156
+ fields = []
157
+
155
158
  columns = {}
156
159
  for field in fields:
157
160
  if field in INSIGHT_FIELDS_TYPES:
@@ -184,15 +187,9 @@ def facebook_insights_source(
184
187
  query = {
185
188
  "level": level,
186
189
  "action_breakdowns": list(action_breakdowns),
187
- "breakdowns": list(
188
- INSIGHTS_BREAKDOWNS_OPTIONS[breakdowns]["breakdowns"]
189
- ),
190
+ "breakdowns": dimensions,
190
191
  "limit": batch_size,
191
- "fields": list(
192
- set(fields)
193
- .union(INSIGHTS_BREAKDOWNS_OPTIONS[breakdowns]["fields"])
194
- .difference(INVALID_INSIGHTS_FIELDS)
195
- ),
192
+ "fields": fields,
196
193
  "time_increment": time_increment_days,
197
194
  "action_attribution_windows": list(action_attribution_windows),
198
195
  "time_ranges": [
@@ -206,7 +203,9 @@ def facebook_insights_source(
206
203
  }
207
204
  job = execute_job(
208
205
  account.get_insights(params=query, is_async=True),
209
- insights_max_async_sleep_seconds=20,
206
+ insights_max_async_sleep_seconds=insights_max_async_sleep_seconds,
207
+ insights_max_wait_to_finish_seconds=insights_max_wait_to_finish_seconds,
208
+ insights_max_wait_to_start_seconds=insights_max_wait_to_start_seconds,
210
209
  )
211
210
  output = list(map(process_report_item, job.get_result()))
212
211
  yield output
@@ -144,7 +144,7 @@ def execute_job(
144
144
  raise InsightsJobTimeout(
145
145
  "facebook_insights",
146
146
  pretty_error_message.format(
147
- job_id, insights_max_wait_to_finish_seconds // 60
147
+ job_id, insights_max_wait_to_finish_seconds
148
148
  ),
149
149
  )
150
150
 
@@ -229,3 +229,49 @@ def notify_on_token_expiration(access_token_expires_at: int = None) -> None:
229
229
  logger.error(
230
230
  f"Access Token expires in {humanize.precisedelta(pendulum.now() - expires_at)}. Replace the token now!"
231
231
  )
232
+
233
+
234
+ def parse_insights_table_to_source_kwargs(table: str) -> DictStrAny:
235
+ import typing
236
+
237
+ from ingestr.src.facebook_ads.settings import (
238
+ INSIGHTS_BREAKDOWNS_OPTIONS,
239
+ TInsightsBreakdownOptions,
240
+ TInsightsLevels,
241
+ )
242
+
243
+ parts = table.split(":")
244
+
245
+ source_kwargs = {}
246
+
247
+ breakdown_type = parts[1]
248
+
249
+ valid_breakdowns = list(typing.get_args(TInsightsBreakdownOptions))
250
+ if breakdown_type in valid_breakdowns:
251
+ dimensions = INSIGHTS_BREAKDOWNS_OPTIONS[breakdown_type]["breakdowns"]
252
+ fields = INSIGHTS_BREAKDOWNS_OPTIONS[breakdown_type]["fields"]
253
+ source_kwargs["dimensions"] = dimensions
254
+ source_kwargs["fields"] = fields
255
+ else:
256
+ dimensions = breakdown_type.split(",")
257
+ valid_levels = list(typing.get_args(TInsightsLevels))
258
+ level = None
259
+ for valid_level in reversed(valid_levels):
260
+ if valid_level in dimensions:
261
+ level = valid_level
262
+ dimensions.remove(valid_level)
263
+ break
264
+
265
+ source_kwargs["level"] = level
266
+ source_kwargs["dimensions"] = dimensions
267
+
268
+ # If custom metrics are provided, parse them
269
+ if len(parts) == 3:
270
+ fields = [f.strip() for f in parts[2].split(",") if f.strip()]
271
+ if not fields:
272
+ raise ValueError(
273
+ "Custom metrics must be provided after the second colon in format: facebook_insights:breakdown_type:metric1,metric2..."
274
+ )
275
+ source_kwargs["fields"] = fields
276
+
277
+ return source_kwargs
@@ -3,7 +3,23 @@ from typing import Any, Dict, Iterable, Iterator
3
3
  import dlt
4
4
  import pendulum
5
5
 
6
- from .helpers import _normalize_issue, _normalize_team, _paginate
6
+ from .helpers import _paginate, normalize_dictionaries
7
+
8
+
9
+ def _get_date_range(updated_at, start_date):
10
+ """Extract current start and end dates from incremental state."""
11
+ if updated_at.last_value:
12
+ current_start_date = pendulum.parse(updated_at.last_value)
13
+ else:
14
+ current_start_date = pendulum.parse(start_date)
15
+
16
+ if updated_at.end_value:
17
+ current_end_date = pendulum.parse(updated_at.end_value)
18
+ else:
19
+ current_end_date = pendulum.now(tz="UTC")
20
+
21
+ return current_start_date, current_end_date
22
+
7
23
 
8
24
  ISSUES_QUERY = """
9
25
  query Issues($cursor: String) {
@@ -84,6 +100,25 @@ query Users($cursor: String) {
84
100
  }
85
101
  }
86
102
  """
103
+ WORKFLOW_STATES_QUERY = """
104
+ query WorkflowStates($cursor: String) {
105
+ workflowStates(first: 50, after: $cursor) {
106
+ nodes {
107
+ archivedAt
108
+ color
109
+ createdAt
110
+ id
111
+ inheritedFrom { id }
112
+ name
113
+ position
114
+ team { id }
115
+ type
116
+ updatedAt
117
+ }
118
+ pageInfo { hasNextPage endCursor }
119
+ }
120
+ }
121
+ """
87
122
 
88
123
 
89
124
  @dlt.source(name="linear", max_table_nesting=0)
@@ -102,20 +137,12 @@ def linear_source(
102
137
  range_end="closed",
103
138
  ),
104
139
  ) -> Iterator[Dict[str, Any]]:
105
- if updated_at.last_value:
106
- current_start_date = pendulum.parse(updated_at.last_value)
107
- else:
108
- current_start_date = pendulum.parse(start_date)
109
-
110
- if updated_at.end_value:
111
- current_end_date = pendulum.parse(updated_at.end_value)
112
- else:
113
- current_end_date = pendulum.now(tz="UTC")
140
+ current_start_date, current_end_date = _get_date_range(updated_at, start_date)
114
141
 
115
142
  for item in _paginate(api_key, ISSUES_QUERY, "issues"):
116
143
  if pendulum.parse(item["updatedAt"]) >= current_start_date:
117
144
  if pendulum.parse(item["updatedAt"]) <= current_end_date:
118
- yield _normalize_issue(item)
145
+ yield normalize_dictionaries(item)
119
146
 
120
147
  @dlt.resource(name="projects", primary_key="id", write_disposition="merge")
121
148
  def projects(
@@ -127,20 +154,12 @@ def linear_source(
127
154
  range_end="closed",
128
155
  ),
129
156
  ) -> Iterator[Dict[str, Any]]:
130
- if updated_at.last_value:
131
- current_start_date = pendulum.parse(updated_at.last_value)
132
- else:
133
- current_start_date = pendulum.parse(start_date)
134
-
135
- if updated_at.end_value:
136
- current_end_date = pendulum.parse(updated_at.end_value)
137
- else:
138
- current_end_date = pendulum.now(tz="UTC")
157
+ current_start_date, current_end_date = _get_date_range(updated_at, start_date)
139
158
 
140
159
  for item in _paginate(api_key, PROJECTS_QUERY, "projects"):
141
160
  if pendulum.parse(item["updatedAt"]) >= current_start_date:
142
161
  if pendulum.parse(item["updatedAt"]) <= current_end_date:
143
- yield item
162
+ yield normalize_dictionaries(item)
144
163
 
145
164
  @dlt.resource(name="teams", primary_key="id", write_disposition="merge")
146
165
  def teams(
@@ -153,21 +172,13 @@ def linear_source(
153
172
  ),
154
173
  ) -> Iterator[Dict[str, Any]]:
155
174
  print(start_date)
156
- if updated_at.last_value:
157
- current_start_date = pendulum.parse(updated_at.last_value)
158
- else:
159
- current_start_date = pendulum.parse(start_date)
175
+ current_start_date, current_end_date = _get_date_range(updated_at, start_date)
160
176
  print(current_start_date)
161
177
 
162
- if updated_at.end_value:
163
- current_end_date = pendulum.parse(updated_at.end_value)
164
- else:
165
- current_end_date = pendulum.now(tz="UTC")
166
-
167
178
  for item in _paginate(api_key, TEAMS_QUERY, "teams"):
168
179
  if pendulum.parse(item["updatedAt"]) >= current_start_date:
169
180
  if pendulum.parse(item["updatedAt"]) <= current_end_date:
170
- yield _normalize_team(item)
181
+ yield normalize_dictionaries(item)
171
182
 
172
183
  @dlt.resource(name="users", primary_key="id", write_disposition="merge")
173
184
  def users(
@@ -179,19 +190,28 @@ def linear_source(
179
190
  range_end="closed",
180
191
  ),
181
192
  ) -> Iterator[Dict[str, Any]]:
182
- if updated_at.last_value:
183
- current_start_date = pendulum.parse(updated_at.last_value)
184
- else:
185
- current_start_date = pendulum.parse(start_date)
186
-
187
- if updated_at.end_value:
188
- current_end_date = pendulum.parse(updated_at.end_value)
189
- else:
190
- current_end_date = pendulum.now(tz="UTC")
193
+ current_start_date, current_end_date = _get_date_range(updated_at, start_date)
191
194
 
192
195
  for item in _paginate(api_key, USERS_QUERY, "users"):
193
196
  if pendulum.parse(item["updatedAt"]) >= current_start_date:
194
197
  if pendulum.parse(item["updatedAt"]) <= current_end_date:
195
- yield item
198
+ yield normalize_dictionaries(item)
199
+
200
+ @dlt.resource(name="workflow_states", primary_key="id", write_disposition="merge")
201
+ def workflow_states(
202
+ updated_at: dlt.sources.incremental[str] = dlt.sources.incremental(
203
+ "updatedAt",
204
+ initial_value=start_date.isoformat(),
205
+ end_value=end_date.isoformat() if end_date else None,
206
+ range_start="closed",
207
+ range_end="closed",
208
+ ),
209
+ ) -> Iterator[Dict[str, Any]]:
210
+ current_start_date, current_end_date = _get_date_range(updated_at, start_date)
211
+
212
+ for item in _paginate(api_key, WORKFLOW_STATES_QUERY, "workflowStates"):
213
+ if pendulum.parse(item["updatedAt"]) >= current_start_date:
214
+ if pendulum.parse(item["updatedAt"]) <= current_end_date:
215
+ yield normalize_dictionaries(item)
196
216
 
197
- return issues, projects, teams, users
217
+ return [issues, projects, teams, users, workflow_states]
@@ -0,0 +1,53 @@
1
+ from typing import Any, Dict, Iterator, Optional
2
+
3
+ import requests
4
+
5
+ LINEAR_GRAPHQL_ENDPOINT = "https://api.linear.app/graphql"
6
+
7
+
8
+ def _graphql(
9
+ api_key: str, query: str, variables: Optional[Dict[str, Any]] = None
10
+ ) -> Dict[str, Any]:
11
+ headers = {"Authorization": api_key, "Content-Type": "application/json"}
12
+ response = requests.post(
13
+ LINEAR_GRAPHQL_ENDPOINT,
14
+ json={"query": query, "variables": variables or {}},
15
+ headers=headers,
16
+ )
17
+ response.raise_for_status()
18
+ payload = response.json()
19
+ if "errors" in payload:
20
+ raise ValueError(str(payload["errors"]))
21
+ return payload["data"]
22
+
23
+
24
+ def _paginate(api_key: str, query: str, root: str) -> Iterator[Dict[str, Any]]:
25
+ cursor: Optional[str] = None
26
+ while True:
27
+ data = _graphql(api_key, query, {"cursor": cursor})[root]
28
+ for item in data["nodes"]:
29
+ yield item
30
+ if not data["pageInfo"]["hasNextPage"]:
31
+ break
32
+ cursor = data["pageInfo"]["endCursor"]
33
+
34
+
35
+ def normalize_dictionaries(item: Dict[str, Any]) -> Dict[str, Any]:
36
+ """
37
+ Automatically normalize dictionary fields by detecting their structure:
38
+ - Convert nested objects with 'id' field to {field_name}_id
39
+ - Convert objects with 'nodes' field to arrays
40
+ """
41
+ normalized_item = item.copy()
42
+
43
+ for key, value in list(normalized_item.items()):
44
+ if isinstance(value, dict):
45
+ # If the dict has an 'id' field, replace with {key}_id
46
+ if "id" in value:
47
+ normalized_item[f"{key}_id"] = value["id"]
48
+ del normalized_item[key]
49
+ # If the dict has 'nodes' field, extract the nodes array
50
+ elif "nodes" in value:
51
+ normalized_item[key] = value["nodes"]
52
+
53
+ return normalized_item