ingestr 0.13.63__tar.gz → 0.13.65__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 (314) hide show
  1. {ingestr-0.13.63 → ingestr-0.13.65}/PKG-INFO +1 -1
  2. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/mssql.md +18 -0
  3. ingestr-0.13.65/docs/supported-sources/zoom.md +47 -0
  4. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/attio/__init__.py +10 -7
  5. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/attio/helpers.py +19 -8
  6. ingestr-0.13.65/ingestr/src/buildinfo.py +1 -0
  7. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/destinations.py +93 -1
  8. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/factory.py +1 -0
  9. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/linear/__init__.py +46 -34
  10. ingestr-0.13.65/ingestr/src/linear/helpers.py +60 -0
  11. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/sources.py +47 -2
  12. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/zoom/__init__.py +45 -1
  13. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/zoom/helpers.py +26 -0
  14. {ingestr-0.13.63 → ingestr-0.13.65}/styles/config/vocabularies/bruin/accept.txt +1 -0
  15. ingestr-0.13.63/docs/supported-sources/zoom.md +0 -34
  16. ingestr-0.13.63/ingestr/src/buildinfo.py +0 -1
  17. {ingestr-0.13.63 → ingestr-0.13.65}/.dockerignore +0 -0
  18. {ingestr-0.13.63 → ingestr-0.13.65}/.githooks/pre-commit-hook.sh +0 -0
  19. {ingestr-0.13.63 → ingestr-0.13.65}/.github/workflows/deploy-docs.yml +0 -0
  20. {ingestr-0.13.63 → ingestr-0.13.65}/.github/workflows/release.yml +0 -0
  21. {ingestr-0.13.63 → ingestr-0.13.65}/.github/workflows/secrets-scan.yml +0 -0
  22. {ingestr-0.13.63 → ingestr-0.13.65}/.github/workflows/tests.yml +0 -0
  23. {ingestr-0.13.63 → ingestr-0.13.65}/.gitignore +0 -0
  24. {ingestr-0.13.63 → ingestr-0.13.65}/.gitleaksignore +0 -0
  25. {ingestr-0.13.63 → ingestr-0.13.65}/.python-version +0 -0
  26. {ingestr-0.13.63 → ingestr-0.13.65}/.vale.ini +0 -0
  27. {ingestr-0.13.63 → ingestr-0.13.65}/Dockerfile +0 -0
  28. {ingestr-0.13.63 → ingestr-0.13.65}/LICENSE.md +0 -0
  29. {ingestr-0.13.63 → ingestr-0.13.65}/Makefile +0 -0
  30. {ingestr-0.13.63 → ingestr-0.13.65}/README.md +0 -0
  31. {ingestr-0.13.63 → ingestr-0.13.65}/docs/.vitepress/config.mjs +0 -0
  32. {ingestr-0.13.63 → ingestr-0.13.65}/docs/.vitepress/theme/custom.css +0 -0
  33. {ingestr-0.13.63 → ingestr-0.13.65}/docs/.vitepress/theme/index.js +0 -0
  34. {ingestr-0.13.63 → ingestr-0.13.65}/docs/commands/example-uris.md +0 -0
  35. {ingestr-0.13.63 → ingestr-0.13.65}/docs/commands/ingest.md +0 -0
  36. {ingestr-0.13.63 → ingestr-0.13.65}/docs/getting-started/core-concepts.md +0 -0
  37. {ingestr-0.13.63 → ingestr-0.13.65}/docs/getting-started/incremental-loading.md +0 -0
  38. {ingestr-0.13.63 → ingestr-0.13.65}/docs/getting-started/quickstart.md +0 -0
  39. {ingestr-0.13.63 → ingestr-0.13.65}/docs/getting-started/telemetry.md +0 -0
  40. {ingestr-0.13.63 → ingestr-0.13.65}/docs/index.md +0 -0
  41. {ingestr-0.13.63 → ingestr-0.13.65}/docs/media/applovin_max.png +0 -0
  42. {ingestr-0.13.63 → ingestr-0.13.65}/docs/media/athena.png +0 -0
  43. {ingestr-0.13.63 → ingestr-0.13.65}/docs/media/clickhouse_img.png +0 -0
  44. {ingestr-0.13.63 → ingestr-0.13.65}/docs/media/cratedb-destination.png +0 -0
  45. {ingestr-0.13.63 → ingestr-0.13.65}/docs/media/cratedb-source.png +0 -0
  46. {ingestr-0.13.63 → ingestr-0.13.65}/docs/media/freshdesk_ingestion.png +0 -0
  47. {ingestr-0.13.63 → ingestr-0.13.65}/docs/media/gcp_spanner_ingestion.png +0 -0
  48. {ingestr-0.13.63 → ingestr-0.13.65}/docs/media/github.png +0 -0
  49. {ingestr-0.13.63 → ingestr-0.13.65}/docs/media/google_analytics_realtime_report.png +0 -0
  50. {ingestr-0.13.63 → ingestr-0.13.65}/docs/media/googleanalytics.png +0 -0
  51. {ingestr-0.13.63 → ingestr-0.13.65}/docs/media/ingestion_elasticsearch_img.png +0 -0
  52. {ingestr-0.13.63 → ingestr-0.13.65}/docs/media/kinesis.bigquery.png +0 -0
  53. {ingestr-0.13.63 → ingestr-0.13.65}/docs/media/linear.png +0 -0
  54. {ingestr-0.13.63 → ingestr-0.13.65}/docs/media/linkedin_ads.png +0 -0
  55. {ingestr-0.13.63 → ingestr-0.13.65}/docs/media/mixpanel_ingestion.png +0 -0
  56. {ingestr-0.13.63 → ingestr-0.13.65}/docs/media/personio.png +0 -0
  57. {ingestr-0.13.63 → ingestr-0.13.65}/docs/media/personio_duckdb.png +0 -0
  58. {ingestr-0.13.63 → ingestr-0.13.65}/docs/media/phantombuster.png +0 -0
  59. {ingestr-0.13.63 → ingestr-0.13.65}/docs/media/pipedrive.png +0 -0
  60. {ingestr-0.13.63 → ingestr-0.13.65}/docs/media/quickbook_ingestion.png +0 -0
  61. {ingestr-0.13.63 → ingestr-0.13.65}/docs/media/sftp.png +0 -0
  62. {ingestr-0.13.63 → ingestr-0.13.65}/docs/media/stripe_postgres.png +0 -0
  63. {ingestr-0.13.63 → ingestr-0.13.65}/docs/media/tiktok.png +0 -0
  64. {ingestr-0.13.63 → ingestr-0.13.65}/docs/media/zoom_ingestion.png +0 -0
  65. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/adjust.md +0 -0
  66. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/airtable.md +0 -0
  67. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/applovin.md +0 -0
  68. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/applovin_max.md +0 -0
  69. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/appsflyer.md +0 -0
  70. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/appstore.md +0 -0
  71. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/asana.md +0 -0
  72. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/athena.md +0 -0
  73. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/attio.md +0 -0
  74. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/bigquery.md +0 -0
  75. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/chess.md +0 -0
  76. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/clickhouse.md +0 -0
  77. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/cratedb.md +0 -0
  78. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/csv.md +0 -0
  79. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/custom_queries.md +0 -0
  80. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/databricks.md +0 -0
  81. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/db2.md +0 -0
  82. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/duckdb.md +0 -0
  83. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/dynamodb.md +0 -0
  84. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/elasticsearch.md +0 -0
  85. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/facebook-ads.md +0 -0
  86. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/frankfurter.md +0 -0
  87. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/freshdesk.md +0 -0
  88. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/gcs.md +0 -0
  89. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/github.md +0 -0
  90. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/google-ads.md +0 -0
  91. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/google_analytics.md +0 -0
  92. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/gorgias.md +0 -0
  93. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/gsheets.md +0 -0
  94. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/hubspot.md +0 -0
  95. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/isoc-pulse.md +0 -0
  96. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/kafka.md +0 -0
  97. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/kinesis.md +0 -0
  98. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/klaviyo.md +0 -0
  99. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/linear.md +0 -0
  100. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/linkedin_ads.md +0 -0
  101. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/mixpanel.md +0 -0
  102. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/mongodb.md +0 -0
  103. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/mysql.md +0 -0
  104. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/notion.md +0 -0
  105. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/oracle.md +0 -0
  106. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/personio.md +0 -0
  107. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/phantombuster.md +0 -0
  108. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/pinterest.md +0 -0
  109. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/pipedrive.md +0 -0
  110. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/postgres.md +0 -0
  111. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/quickbooks.md +0 -0
  112. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/redshift.md +0 -0
  113. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/s3.md +0 -0
  114. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/salesforce.md +0 -0
  115. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/sap-hana.md +0 -0
  116. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/sftp.md +0 -0
  117. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/shopify.md +0 -0
  118. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/slack.md +0 -0
  119. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/smartsheets.md +0 -0
  120. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/snowflake.md +0 -0
  121. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/solidgate.md +0 -0
  122. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/spanner.md +0 -0
  123. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/sqlite.md +0 -0
  124. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/stripe.md +0 -0
  125. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/tiktok-ads.md +0 -0
  126. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/trustpilot.md +0 -0
  127. {ingestr-0.13.63 → ingestr-0.13.65}/docs/supported-sources/zendesk.md +0 -0
  128. {ingestr-0.13.63 → ingestr-0.13.65}/docs/tutorials/load-kinesis-bigquery.md +0 -0
  129. {ingestr-0.13.63 → ingestr-0.13.65}/docs/tutorials/load-personio-duckdb.md +0 -0
  130. {ingestr-0.13.63 → ingestr-0.13.65}/docs/tutorials/load-stripe-postgres.md +0 -0
  131. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/conftest.py +0 -0
  132. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/main.py +0 -0
  133. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/.gitignore +0 -0
  134. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/adjust/__init__.py +0 -0
  135. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/adjust/adjust_helpers.py +0 -0
  136. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/airtable/__init__.py +0 -0
  137. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/applovin/__init__.py +0 -0
  138. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/applovin_max/__init__.py +0 -0
  139. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/appsflyer/__init__.py +0 -0
  140. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/appsflyer/client.py +0 -0
  141. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/appstore/__init__.py +0 -0
  142. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/appstore/client.py +0 -0
  143. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/appstore/errors.py +0 -0
  144. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/appstore/models.py +0 -0
  145. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/appstore/resources.py +0 -0
  146. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/arrow/__init__.py +0 -0
  147. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/asana_source/__init__.py +0 -0
  148. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/asana_source/helpers.py +0 -0
  149. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/asana_source/settings.py +0 -0
  150. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/blob.py +0 -0
  151. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/chess/__init__.py +0 -0
  152. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/chess/helpers.py +0 -0
  153. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/chess/settings.py +0 -0
  154. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/collector/spinner.py +0 -0
  155. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/dynamodb/__init__.py +0 -0
  156. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/elasticsearch/__init__.py +0 -0
  157. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/errors.py +0 -0
  158. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/facebook_ads/__init__.py +0 -0
  159. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/facebook_ads/exceptions.py +0 -0
  160. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/facebook_ads/helpers.py +0 -0
  161. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/facebook_ads/settings.py +0 -0
  162. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/facebook_ads/utils.py +0 -0
  163. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/filesystem/__init__.py +0 -0
  164. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/filesystem/helpers.py +0 -0
  165. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/filesystem/readers.py +0 -0
  166. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/filters.py +0 -0
  167. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/frankfurter/__init__.py +0 -0
  168. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/frankfurter/helpers.py +0 -0
  169. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/freshdesk/__init__.py +0 -0
  170. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/freshdesk/freshdesk_client.py +0 -0
  171. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/freshdesk/settings.py +0 -0
  172. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/github/__init__.py +0 -0
  173. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/github/helpers.py +0 -0
  174. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/github/queries.py +0 -0
  175. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/github/settings.py +0 -0
  176. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/google_ads/__init__.py +0 -0
  177. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/google_ads/field.py +0 -0
  178. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/google_ads/metrics.py +0 -0
  179. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/google_ads/predicates.py +0 -0
  180. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/google_ads/reports.py +0 -0
  181. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/google_analytics/__init__.py +0 -0
  182. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/google_analytics/helpers.py +0 -0
  183. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/google_sheets/README.md +0 -0
  184. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/google_sheets/__init__.py +0 -0
  185. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/google_sheets/helpers/__init__.py +0 -0
  186. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/google_sheets/helpers/api_calls.py +0 -0
  187. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/google_sheets/helpers/data_processing.py +0 -0
  188. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/gorgias/__init__.py +0 -0
  189. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/gorgias/helpers.py +0 -0
  190. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/http_client.py +0 -0
  191. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/hubspot/__init__.py +0 -0
  192. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/hubspot/helpers.py +0 -0
  193. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/hubspot/settings.py +0 -0
  194. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/isoc_pulse/__init__.py +0 -0
  195. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/kafka/__init__.py +0 -0
  196. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/kafka/helpers.py +0 -0
  197. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/kinesis/__init__.py +0 -0
  198. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/kinesis/helpers.py +0 -0
  199. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/klaviyo/__init__.py +0 -0
  200. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/klaviyo/client.py +0 -0
  201. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/klaviyo/helpers.py +0 -0
  202. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/linkedin_ads/__init__.py +0 -0
  203. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/linkedin_ads/dimension_time_enum.py +0 -0
  204. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/linkedin_ads/helpers.py +0 -0
  205. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/loader.py +0 -0
  206. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/mixpanel/__init__.py +0 -0
  207. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/mixpanel/client.py +0 -0
  208. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/mongodb/__init__.py +0 -0
  209. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/mongodb/helpers.py +0 -0
  210. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/notion/__init__.py +0 -0
  211. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/notion/helpers/__init__.py +0 -0
  212. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/notion/helpers/client.py +0 -0
  213. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/notion/helpers/database.py +0 -0
  214. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/notion/settings.py +0 -0
  215. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/partition.py +0 -0
  216. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/personio/__init__.py +0 -0
  217. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/personio/helpers.py +0 -0
  218. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/phantombuster/__init__.py +0 -0
  219. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/phantombuster/client.py +0 -0
  220. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/pinterest/__init__.py +0 -0
  221. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/pipedrive/__init__.py +0 -0
  222. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/pipedrive/helpers/__init__.py +0 -0
  223. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/pipedrive/helpers/custom_fields_munger.py +0 -0
  224. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/pipedrive/helpers/pages.py +0 -0
  225. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/pipedrive/settings.py +0 -0
  226. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/pipedrive/typing.py +0 -0
  227. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/quickbooks/__init__.py +0 -0
  228. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/resource.py +0 -0
  229. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/salesforce/__init__.py +0 -0
  230. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/salesforce/helpers.py +0 -0
  231. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/shopify/__init__.py +0 -0
  232. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/shopify/exceptions.py +0 -0
  233. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/shopify/helpers.py +0 -0
  234. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/shopify/settings.py +0 -0
  235. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/slack/__init__.py +0 -0
  236. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/slack/helpers.py +0 -0
  237. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/slack/settings.py +0 -0
  238. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/smartsheets/__init__.py +0 -0
  239. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/solidgate/__init__.py +0 -0
  240. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/solidgate/helpers.py +0 -0
  241. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/sql_database/__init__.py +0 -0
  242. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/sql_database/callbacks.py +0 -0
  243. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/stripe_analytics/__init__.py +0 -0
  244. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/stripe_analytics/helpers.py +0 -0
  245. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/stripe_analytics/settings.py +0 -0
  246. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/table_definition.py +0 -0
  247. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/telemetry/event.py +0 -0
  248. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/testdata/fakebqcredentials.json +0 -0
  249. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/tiktok_ads/__init__.py +0 -0
  250. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/tiktok_ads/tiktok_helpers.py +0 -0
  251. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/time.py +0 -0
  252. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/trustpilot/__init__.py +0 -0
  253. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/trustpilot/client.py +0 -0
  254. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/version.py +0 -0
  255. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/zendesk/__init__.py +0 -0
  256. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/zendesk/helpers/__init__.py +0 -0
  257. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/zendesk/helpers/api_helpers.py +0 -0
  258. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/zendesk/helpers/credentials.py +0 -0
  259. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/zendesk/helpers/talk_api.py +0 -0
  260. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/src/zendesk/settings.py +0 -0
  261. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/testdata/.gitignore +0 -0
  262. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/testdata/create_replace.csv +0 -0
  263. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/testdata/delete_insert_expected.csv +0 -0
  264. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/testdata/delete_insert_part1.csv +0 -0
  265. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/testdata/delete_insert_part2.csv +0 -0
  266. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/testdata/merge_expected.csv +0 -0
  267. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/testdata/merge_part1.csv +0 -0
  268. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/testdata/merge_part2.csv +0 -0
  269. {ingestr-0.13.63 → ingestr-0.13.65}/ingestr/tests/unit/test_smartsheets.py +0 -0
  270. {ingestr-0.13.63 → ingestr-0.13.65}/package-lock.json +0 -0
  271. {ingestr-0.13.63 → ingestr-0.13.65}/package.json +0 -0
  272. {ingestr-0.13.63 → ingestr-0.13.65}/pyproject.toml +0 -0
  273. {ingestr-0.13.63 → ingestr-0.13.65}/requirements-dev.txt +0 -0
  274. {ingestr-0.13.63 → ingestr-0.13.65}/requirements.in +0 -0
  275. {ingestr-0.13.63 → ingestr-0.13.65}/requirements.txt +0 -0
  276. {ingestr-0.13.63 → ingestr-0.13.65}/requirements_arm64.txt +0 -0
  277. {ingestr-0.13.63 → ingestr-0.13.65}/resources/demo.gif +0 -0
  278. {ingestr-0.13.63 → ingestr-0.13.65}/resources/demo.tape +0 -0
  279. {ingestr-0.13.63 → ingestr-0.13.65}/resources/ingestr.svg +0 -0
  280. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/AMPM.yml +0 -0
  281. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/Acronyms.yml +0 -0
  282. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/Colons.yml +0 -0
  283. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/Contractions.yml +0 -0
  284. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/DateFormat.yml +0 -0
  285. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/Ellipses.yml +0 -0
  286. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/EmDash.yml +0 -0
  287. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/Exclamation.yml +0 -0
  288. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/FirstPerson.yml +0 -0
  289. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/Gender.yml +0 -0
  290. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/GenderBias.yml +0 -0
  291. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/HeadingPunctuation.yml +0 -0
  292. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/Headings.yml +0 -0
  293. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/Latin.yml +0 -0
  294. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/LyHyphens.yml +0 -0
  295. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/OptionalPlurals.yml +0 -0
  296. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/Ordinal.yml +0 -0
  297. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/OxfordComma.yml +0 -0
  298. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/Parens.yml +0 -0
  299. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/Passive.yml +0 -0
  300. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/Periods.yml +0 -0
  301. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/Quotes.yml +0 -0
  302. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/Ranges.yml +0 -0
  303. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/Semicolons.yml +0 -0
  304. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/Slang.yml +0 -0
  305. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/Spacing.yml +0 -0
  306. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/Spelling.yml +0 -0
  307. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/Units.yml +0 -0
  308. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/We.yml +0 -0
  309. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/Will.yml +0 -0
  310. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/WordList.yml +0 -0
  311. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/meta.json +0 -0
  312. {ingestr-0.13.63 → ingestr-0.13.65}/styles/Google/vocab.txt +0 -0
  313. {ingestr-0.13.63 → ingestr-0.13.65}/styles/bruin/Ingestr.yml +0 -0
  314. {ingestr-0.13.63 → ingestr-0.13.65}/test.env.template +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ingestr
3
- Version: 0.13.63
3
+ Version: 0.13.65
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
@@ -28,3 +28,21 @@ URI parameters:
28
28
  - `TrustServerCertificate`: whether to trust the server certificate
29
29
 
30
30
  The same URI structure can be used both for sources and destinations. You can read more about SQLAlchemy's SQL Server dialect [here](https://docs.sqlalchemy.org/en/20/core/engines.html#microsoft-sql-server).
31
+
32
+ ## Tips & Tricks
33
+
34
+ If you're using Azure SQL Server, you can use `az cli` to generate access tokens to connect to SQL server.
35
+
36
+ Set the password to your token and the `Authentication` parameter to `ActiveDirectoryAccessToken`
37
+ ::: code-group
38
+
39
+ ```sh [token-auth-example.sh]
40
+ USER=$(az account show --query user.name -o tsv)
41
+ TOKEN=$(az account get-access-token --resource https://database.windows.net/ --query accessToken -o tsv)
42
+ ingestr ingest \
43
+ --source-uri "mssql://$USER:$TOKEN@<server>.database.windows.net/<database>?Authentication=ActiveDirectoryAccessToken" \
44
+ --source-table "dbo.example" \
45
+ --dest-uri "duckdb:///example.db" \
46
+ --dest-table "dbo.example" \
47
+ ```
48
+ :::
@@ -0,0 +1,47 @@
1
+ # Zoom
2
+
3
+ [Zoom](https://www.zoom.com/) is a video conferencing and collaboration platform.
4
+
5
+ ingestr supports Zoom as a source.
6
+
7
+ ## Prerequisites
8
+ - A [Zoom Server-to-Server OAuth App](https://developers.zoom.us/docs/internal-apps/s2s-oauth/)
9
+ - Appropriate permissions related to meetings, users and participants must be added in the app's scopes
10
+ `user:read,user:write,user:read:admin,user:write:admin`
11
+ - Obtain the `client_id`, `client_secret` and `account_id` credentials from the app
12
+
13
+ ## URI format
14
+ ```plaintext
15
+ zoom://?client_id=<client_id>&client_secret=<client_secret>&account_id=<account_id>
16
+ ```
17
+
18
+ This command copies meetings data from the Zoom source to DuckDB.
19
+
20
+ ```sh
21
+ ingestr ingest \
22
+ --source-uri 'zoom://?client_id=abc&client_secret=xyz&account_id=123' \
23
+ --source-table 'meetings' \
24
+ --dest-uri duckdb:///zoom.duckdb \
25
+ --dest-table 'dest.meetings'
26
+ ```
27
+
28
+ <img alt="zoom" src="../media/zoom_ingestion.png"/>
29
+ ## Tables
30
+
31
+ Zoom source allows ingesting the following tables:
32
+ - `meetings`: Retrieve all valid previous meetings, live meetings, and upcoming scheduled meetings for all users in the given Zoom account.
33
+ - Permissions required: meeting:read:admin,meeting:read
34
+ - Granular permissions: meeting:read:list_meetings,meeting:read:list_meetings:admin
35
+
36
+ - `users`: Retrieve a list of users in your account.
37
+ - Permissions required: user:read, user:write, user:read:admin, user:write:admin.
38
+ - Granular permissions: user:read:list_users:admin.
39
+ - Prerequisites: A Pro or higher plan.
40
+
41
+ - `participants`: Return a report of a past meeting that had participants, including the host.
42
+ It only returns data for meetings within the last 6 months.
43
+ - Permissions required: report:read:admin.
44
+ - Granular permissions: report:read:list_meeting_participants:admin.
45
+ - Prerequisites: A Pro or higher plan.
46
+
47
+ Use these as the `--source-table` parameter in the `ingestr ingest` command.
@@ -20,13 +20,15 @@ def attio_source(
20
20
  "created_at": {"data_type": "timestamp", "partition": True},
21
21
  },
22
22
  )
23
+ # https://docs.attio.com/rest-api/endpoint-reference/objects/list-objects - does not support pagination
23
24
  def fetch_objects() -> Iterator[dict]:
24
25
  if len(params) != 0:
25
26
  raise ValueError("Objects table must be in the format `objects`")
26
27
 
27
28
  path = "objects"
28
- yield attio_client.fetch_data(path, "get")
29
+ yield attio_client.fetch_all(path, "get")
29
30
 
31
+ # https://docs.attio.com/rest-api/endpoint-reference/records/list-records
30
32
  @dlt.resource(
31
33
  name="records",
32
34
  write_disposition="replace",
@@ -39,12 +41,12 @@ def attio_source(
39
41
  raise ValueError(
40
42
  "Records table must be in the format `records:{object_api_slug}`"
41
43
  )
42
-
43
44
  object_id = params[0]
44
45
  path = f"objects/{object_id}/records/query"
45
46
 
46
- yield attio_client.fetch_data(path, "post")
47
+ yield attio_client.fetch_paginated(path, "post")
47
48
 
49
+ # https://docs.attio.com/rest-api/endpoint-reference/lists/list-all-lists -- does not support pagination
48
50
  @dlt.resource(
49
51
  name="lists",
50
52
  write_disposition="replace",
@@ -54,8 +56,9 @@ def attio_source(
54
56
  )
55
57
  def fetch_lists() -> Iterator[dict]:
56
58
  path = "lists"
57
- yield attio_client.fetch_data(path, "get")
59
+ yield attio_client.fetch_all(path, "get")
58
60
 
61
+ # https://docs.attio.com/rest-api/endpoint-reference/entries/list-entries
59
62
  @dlt.resource(
60
63
  name="list_entries",
61
64
  write_disposition="replace",
@@ -70,7 +73,7 @@ def attio_source(
70
73
  )
71
74
  path = f"lists/{params[0]}/entries/query"
72
75
 
73
- yield attio_client.fetch_data(path, "post")
76
+ yield attio_client.fetch_paginated(path, "post")
74
77
 
75
78
  @dlt.resource(
76
79
  name="all_list_entries",
@@ -85,10 +88,10 @@ def attio_source(
85
88
  "All list entries table must be in the format `all_list_entries:{object_api_slug}`"
86
89
  )
87
90
  path = "lists"
88
- for lst in attio_client.fetch_data(path, "get"):
91
+ for lst in attio_client.fetch_all(path, "get"):
89
92
  if params[0] in lst["parent_object"]:
90
93
  path = f"lists/{lst['id']['list_id']}/entries/query"
91
- yield from attio_client.fetch_data(path, "post")
94
+ yield from attio_client.fetch_paginated(path, "post")
92
95
 
93
96
  return (
94
97
  fetch_objects,
@@ -10,42 +10,53 @@ class AttioClient:
10
10
  }
11
11
  self.client = create_client()
12
12
 
13
- def fetch_data(self, path: str, method: str, limit: int = 1000, params=None):
13
+ def fetch_paginated(self, path: str, method: str, limit: int = 1000, params=None):
14
14
  url = f"{self.base_url}/{path}"
15
15
  if params is None:
16
16
  params = {}
17
17
  offset = 0
18
18
  while True:
19
- query_params = {**params, "limit": limit, "offset": offset}
19
+ query_params = {"limit": limit, "offset": offset, **params}
20
20
  if method == "get":
21
21
  response = self.client.get(
22
22
  url, headers=self.headers, params=query_params
23
23
  )
24
24
  else:
25
- response = self.client.post(
26
- url, headers=self.headers, params=query_params
27
- )
25
+ json_body = {**params, "limit": limit, "offset": offset}
26
+ response = self.client.post(url, headers=self.headers, json=json_body)
28
27
 
29
28
  if response.status_code != 200:
30
29
  raise Exception(f"HTTP {response.status_code} error: {response.text}")
31
30
 
32
31
  response_data = response.json()
33
32
  if "data" not in response_data:
34
- print(f"API Response: {response_data}")
35
33
  raise Exception(
36
34
  "Attio API returned a response without the expected data"
37
35
  )
38
36
 
39
37
  data = response_data["data"]
40
-
41
38
  for item in data:
42
39
  flat_item = flatten_item(item)
43
40
  yield flat_item
44
-
45
41
  if len(data) < limit:
46
42
  break
43
+
47
44
  offset += limit
48
45
 
46
+ def fetch_all(self, path: str, method: str = "get", params=None):
47
+ url = f"{self.base_url}/{path}"
48
+ params = params or {}
49
+
50
+ if method == "get":
51
+ response = self.client.get(url, headers=self.headers, params=params)
52
+ else:
53
+ response = self.client.post(url, headers=self.headers, json=params)
54
+
55
+ response.raise_for_status()
56
+ data = response.json().get("data", [])
57
+ for item in data:
58
+ yield flatten_item(item)
59
+
49
60
 
50
61
  def flatten_item(item: dict) -> dict:
51
62
  if "id" in item:
@@ -0,0 +1 @@
1
+ version = "v0.13.65"
@@ -1,19 +1,31 @@
1
1
  import abc
2
2
  import base64
3
3
  import csv
4
+ import datetime
4
5
  import json
5
6
  import os
6
7
  import shutil
8
+ import struct
7
9
  import tempfile
8
10
  from urllib.parse import parse_qs, quote, urlparse
9
11
 
10
12
  import dlt
11
13
  import dlt.destinations.impl.filesystem.filesystem
12
14
  from dlt.common.configuration.specs import AwsCredentials
15
+ from dlt.common.destination.capabilities import DestinationCapabilitiesContext
16
+ from dlt.common.schema import Schema
13
17
  from dlt.common.storages.configuration import FileSystemCredentials
14
18
  from dlt.destinations.impl.clickhouse.configuration import (
15
19
  ClickHouseCredentials,
16
20
  )
21
+ from dlt.destinations.impl.mssql.configuration import MsSqlClientConfiguration
22
+ from dlt.destinations.impl.mssql.mssql import (
23
+ HINT_TO_MSSQL_ATTR,
24
+ MsSqlJobClient,
25
+ )
26
+ from dlt.destinations.impl.mssql.sql_client import (
27
+ PyOdbcMsSqlClient,
28
+ )
17
29
 
18
30
  from ingestr.src.errors import MissingValueError
19
31
  from ingestr.src.loader import load_dlt_file
@@ -143,9 +155,89 @@ class DuckDBDestination(GenericSqlDestination):
143
155
  return dlt.destinations.duckdb(uri, **kwargs)
144
156
 
145
157
 
158
+ def handle_datetimeoffset(dto_value: bytes) -> datetime.datetime:
159
+ # ref: https://github.com/mkleehammer/pyodbc/issues/134#issuecomment-281739794
160
+ tup = struct.unpack(
161
+ "<6hI2h", dto_value
162
+ ) # e.g., (2017, 3, 16, 10, 35, 18, 500000000, -6, 0)
163
+ return datetime.datetime(
164
+ tup[0],
165
+ tup[1],
166
+ tup[2],
167
+ tup[3],
168
+ tup[4],
169
+ tup[5],
170
+ tup[6] // 1000,
171
+ datetime.timezone(datetime.timedelta(hours=tup[7], minutes=tup[8])),
172
+ )
173
+
174
+
175
+ class OdbcMsSqlClient(PyOdbcMsSqlClient):
176
+ SQL_COPT_SS_ACCESS_TOKEN = 1256
177
+ SKIP_CREDENTIALS = {"PWD", "AUTHENTICATION", "UID"}
178
+
179
+ def open_connection(self):
180
+ cfg = self.credentials._get_odbc_dsn_dict()
181
+ if (
182
+ cfg.get("AUTHENTICATION", "").strip().lower()
183
+ != "activedirectoryaccesstoken"
184
+ ):
185
+ return super().open_connection()
186
+
187
+ import pyodbc # type: ignore
188
+
189
+ dsn = ";".join(
190
+ [f"{k}={v}" for k, v in cfg.items() if k not in self.SKIP_CREDENTIALS]
191
+ )
192
+
193
+ self._conn = pyodbc.connect(
194
+ dsn,
195
+ timeout=self.credentials.connect_timeout,
196
+ attrs_before={
197
+ self.SQL_COPT_SS_ACCESS_TOKEN: self.serialize_token(cfg["PWD"]),
198
+ },
199
+ )
200
+
201
+ # https://github.com/mkleehammer/pyodbc/wiki/Using-an-Output-Converter-function
202
+ self._conn.add_output_converter(-155, handle_datetimeoffset)
203
+ self._conn.autocommit = True
204
+ return self._conn
205
+
206
+ def serialize_token(self, token):
207
+ # https://github.com/mkleehammer/pyodbc/issues/228#issuecomment-494773723
208
+ encoded = token.encode("utf_16_le")
209
+ return struct.pack("<i", len(encoded)) + encoded
210
+
211
+
212
+ class MsSqlClient(MsSqlJobClient):
213
+ def __init__(
214
+ self,
215
+ schema: Schema,
216
+ config: MsSqlClientConfiguration,
217
+ capabilities: DestinationCapabilitiesContext,
218
+ ) -> None:
219
+ sql_client = OdbcMsSqlClient(
220
+ config.normalize_dataset_name(schema),
221
+ config.normalize_staging_dataset_name(schema),
222
+ config.credentials,
223
+ capabilities,
224
+ )
225
+ super(MsSqlJobClient, self).__init__(schema, config, sql_client)
226
+ self.config: MsSqlClientConfiguration = config
227
+ self.sql_client = sql_client
228
+ self.active_hints = HINT_TO_MSSQL_ATTR if self.config.create_indexes else {}
229
+ self.type_mapper = capabilities.get_type_mapper()
230
+
231
+
232
+ class MsSqlDestImpl(dlt.destinations.mssql):
233
+ @property
234
+ def client_class(self):
235
+ return MsSqlClient
236
+
237
+
146
238
  class MsSQLDestination(GenericSqlDestination):
147
239
  def dlt_dest(self, uri: str, **kwargs):
148
- return dlt.destinations.mssql(credentials=uri, **kwargs)
240
+ return MsSqlDestImpl(credentials=uri, **kwargs)
149
241
 
150
242
 
151
243
  class DatabricksDestination(GenericSqlDestination):
@@ -79,6 +79,7 @@ SQL_SOURCE_SCHEMES = [
79
79
  "crate",
80
80
  "duckdb",
81
81
  "mssql",
82
+ "mssql+pyodbc",
82
83
  "mysql",
83
84
  "mysql+pymysql",
84
85
  "mysql+mysqlconnector",
@@ -1,38 +1,9 @@
1
- from typing import Any, Dict, Iterable, Iterator, Optional
1
+ from typing import Any, Dict, Iterable, Iterator
2
2
 
3
3
  import dlt
4
4
  import pendulum
5
- import requests
6
-
7
- LINEAR_GRAPHQL_ENDPOINT = "https://api.linear.app/graphql"
8
-
9
-
10
- def _graphql(
11
- api_key: str, query: str, variables: Optional[Dict[str, Any]] = None
12
- ) -> Dict[str, Any]:
13
- headers = {"Authorization": api_key, "Content-Type": "application/json"}
14
- response = requests.post(
15
- LINEAR_GRAPHQL_ENDPOINT,
16
- json={"query": query, "variables": variables or {}},
17
- headers=headers,
18
- )
19
- response.raise_for_status()
20
- payload = response.json()
21
- if "errors" in payload:
22
- raise ValueError(str(payload["errors"]))
23
- return payload["data"]
24
-
25
-
26
- def _paginate(api_key: str, query: str, root: str) -> Iterator[Dict[str, Any]]:
27
- cursor: Optional[str] = None
28
- while True:
29
- data = _graphql(api_key, query, {"cursor": cursor})[root]
30
- for item in data["nodes"]:
31
- yield item
32
- if not data["pageInfo"]["hasNextPage"]:
33
- break
34
- cursor = data["pageInfo"]["endCursor"]
35
5
 
6
+ from .helpers import _paginate, _normalize_issue, _normalize_team
36
7
 
37
8
  ISSUES_QUERY = """
38
9
  query Issues($cursor: String) {
@@ -43,6 +14,17 @@ query Issues($cursor: String) {
43
14
  description
44
15
  createdAt
45
16
  updatedAt
17
+ creator { id }
18
+ assignee { id}
19
+ state { id}
20
+ labels { nodes { id } }
21
+ cycle { id}
22
+ project { id }
23
+ subtasks: children { nodes { id title } }
24
+ comments(first: 250) { nodes { id body } }
25
+ priority
26
+ attachments { nodes { id } }
27
+ subscribers { nodes { id } }
46
28
  }
47
29
  pageInfo { hasNextPage endCursor }
48
30
  }
@@ -58,6 +40,10 @@ query Projects($cursor: String) {
58
40
  description
59
41
  createdAt
60
42
  updatedAt
43
+ health
44
+ priority
45
+ targetDate
46
+ lead { id }
61
47
  }
62
48
  pageInfo { hasNextPage endCursor }
63
49
  }
@@ -72,6 +58,11 @@ query Teams($cursor: String) {
72
58
  name
73
59
  key
74
60
  description
61
+ updatedAt
62
+ createdAt
63
+ memberships { nodes { id } }
64
+ members { nodes { id } }
65
+ projects { nodes { id } }
75
66
  }
76
67
  pageInfo { hasNextPage endCursor }
77
68
  }
@@ -124,7 +115,7 @@ def linear_source(
124
115
  for item in _paginate(api_key, ISSUES_QUERY, "issues"):
125
116
  if pendulum.parse(item["updatedAt"]) >= current_start_date:
126
117
  if pendulum.parse(item["updatedAt"]) <= current_end_date:
127
- yield item
118
+ yield _normalize_issue(item)
128
119
 
129
120
  @dlt.resource(name="projects", primary_key="id", write_disposition="merge")
130
121
  def projects(
@@ -152,8 +143,29 @@ def linear_source(
152
143
  yield item
153
144
 
154
145
  @dlt.resource(name="teams", primary_key="id", write_disposition="merge")
155
- def teams() -> Iterator[Dict[str, Any]]:
156
- yield from _paginate(api_key, TEAMS_QUERY, "teams")
146
+ def teams( updated_at: dlt.sources.incremental[str] = dlt.sources.incremental(
147
+ "updatedAt",
148
+ initial_value=start_date.isoformat(),
149
+ end_value=end_date.isoformat() if end_date else None,
150
+ range_start="closed",
151
+ range_end="closed",
152
+ ),) -> Iterator[Dict[str, Any]]:
153
+ print(start_date)
154
+ if updated_at.last_value:
155
+ current_start_date = pendulum.parse(updated_at.last_value)
156
+ else:
157
+ current_start_date = pendulum.parse(start_date)
158
+ print(current_start_date)
159
+
160
+ if updated_at.end_value:
161
+ current_end_date = pendulum.parse(updated_at.end_value)
162
+ else:
163
+ current_end_date = pendulum.now(tz="UTC")
164
+
165
+ for item in _paginate(api_key, TEAMS_QUERY, "teams"):
166
+ if pendulum.parse(item["updatedAt"]) >= current_start_date:
167
+ if pendulum.parse(item["updatedAt"]) <= current_end_date:
168
+ yield _normalize_team(item)
157
169
 
158
170
  @dlt.resource(name="users", primary_key="id", write_disposition="merge")
159
171
  def users(
@@ -0,0 +1,60 @@
1
+ import json
2
+ from typing import Any, Dict, Iterator, Optional
3
+
4
+ import requests
5
+
6
+ LINEAR_GRAPHQL_ENDPOINT = "https://api.linear.app/graphql"
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
+ def _paginate(api_key: str, query: str, root: str) -> Iterator[Dict[str, Any]]:
24
+ cursor: Optional[str] = None
25
+ while True:
26
+ data = _graphql(api_key, query, {"cursor": cursor})[root]
27
+ for item in data["nodes"]:
28
+ yield item
29
+ if not data["pageInfo"]["hasNextPage"]:
30
+ break
31
+ cursor = data["pageInfo"]["endCursor"]
32
+
33
+ def _normalize_issue(item: Dict[str, Any]) -> Dict[str, Any]:
34
+ field_mapping = {
35
+ "assignee": "assignee_id",
36
+ "creator": "creator_id",
37
+ "state": "state_id",
38
+ "cycle": "cycle_id",
39
+ "project": "project_id",
40
+ }
41
+ for key, value in field_mapping.items():
42
+ if item.get(key):
43
+ item[value] = item[key]["id"]
44
+ del item[key]
45
+ else:
46
+ item[value] = None
47
+ del item[key]
48
+ json_fields = ["comments", "subscribers", "attachments", "labels", "subtasks","projects", "memberships", "members"]
49
+ for field in json_fields:
50
+ if item.get(field):
51
+ item[f"{field}"] = item[field].get("nodes", [])
52
+
53
+ return item
54
+
55
+ def _normalize_team(item: Dict[str, Any]) -> Dict[str, Any]:
56
+ json_fields = ["memberships", "members", "projects"]
57
+ for field in json_fields:
58
+ if item.get(field):
59
+ item[f"{field}"] = item[field].get("nodes", [])
60
+ return item
@@ -258,8 +258,53 @@ class SqlSource:
258
258
  # override the query adapters, the only one we want is the one here in the case of custom queries
259
259
  query_adapters = [custom_query_variable_subsitution(query_value, kwargs)]
260
260
 
261
+ credentials = ConnectionStringCredentials(uri)
262
+ if uri.startswith("mssql://"):
263
+ parsed_uri = urlparse(uri)
264
+ params = parse_qs(parsed_uri.query)
265
+ params = {k.lower(): v for k, v in params.items()}
266
+ if params.get("authentication") == ["ActiveDirectoryAccessToken"]:
267
+ import pyodbc # type: ignore
268
+ from sqlalchemy import create_engine
269
+
270
+ from ingestr.src.destinations import (
271
+ OdbcMsSqlClient,
272
+ handle_datetimeoffset,
273
+ )
274
+
275
+ cfg = {
276
+ "DRIVER": params.get("driver", ["ODBC Driver 18 for SQL Server"])[
277
+ 0
278
+ ],
279
+ "SERVER": f"{parsed_uri.hostname},{parsed_uri.port or 1433}",
280
+ "DATABASE": parsed_uri.path.lstrip("/"),
281
+ }
282
+ for k, v in params.items():
283
+ if k.lower() not in ["driver", "authentication", "connect_timeout"]:
284
+ cfg[k.upper()] = v[0]
285
+
286
+ token = OdbcMsSqlClient.serialize_token(None, parsed_uri.password) # type: ignore[arg-type]
287
+ dsn = ";".join([f"{k}={v}" for k, v in cfg.items()])
288
+
289
+ def creator():
290
+ connection = pyodbc.connect(
291
+ dsn,
292
+ autocommit=True,
293
+ timeout=kwargs.get("connect_timeout", 30),
294
+ attrs_before={
295
+ OdbcMsSqlClient.SQL_COPT_SS_ACCESS_TOKEN: token,
296
+ },
297
+ )
298
+ connection.add_output_converter(-155, handle_datetimeoffset)
299
+ return connection
300
+
301
+ credentials = create_engine(
302
+ "mssql+pyodbc://",
303
+ creator=creator,
304
+ )
305
+
261
306
  builder_res = self.table_builder(
262
- credentials=ConnectionStringCredentials(uri),
307
+ credentials=credentials,
263
308
  schema=table_fields.dataset,
264
309
  table=table_fields.table,
265
310
  incremental=incremental,
@@ -2915,7 +2960,7 @@ class ZoomSource:
2915
2960
 
2916
2961
  from ingestr.src.zoom import zoom_source
2917
2962
 
2918
- if table not in {"meetings"}:
2963
+ if table not in {"meetings", "users", "participants"}:
2919
2964
  raise UnsupportedResourceError(table, "Zoom")
2920
2965
 
2921
2966
  return zoom_source(
@@ -42,14 +42,58 @@ def zoom_source(
42
42
  end_dt = pendulum.now("UTC")
43
43
  else:
44
44
  end_dt = pendulum.parse(datetime.end_value)
45
+
45
46
  base_params: Dict[str, Any] = {
46
47
  "type": "scheduled",
47
48
  "page_size": 300,
48
49
  "from": start_dt.to_date_string(),
49
50
  "to": end_dt.to_date_string(),
50
51
  }
52
+
51
53
  for user in client.get_users():
52
54
  user_id = user["id"]
53
55
  yield from client.get_meetings(user_id, base_params)
54
56
 
55
- return meetings
57
+ @dlt.resource(write_disposition="merge", primary_key="id")
58
+ def users() -> Iterable[TDataItem]:
59
+ yield from client.get_users()
60
+
61
+ @dlt.resource(write_disposition="merge", primary_key="id")
62
+ def participants(
63
+ datetime: dlt.sources.incremental[TAnyDateTime] = dlt.sources.incremental(
64
+ "join_time",
65
+ initial_value=start_date.isoformat(),
66
+ end_value=end_date.isoformat() if end_date is not None else None,
67
+ range_start="closed",
68
+ range_end="closed",
69
+ ),
70
+ ) -> Iterable[TDataItem]:
71
+ if datetime.last_value:
72
+ start_dt = pendulum.parse(datetime.last_value)
73
+ else:
74
+ start_dt = pendulum.parse(start_date)
75
+
76
+ if end_date is None:
77
+ end_dt = pendulum.now("UTC")
78
+ else:
79
+ end_dt = pendulum.parse(datetime.end_value)
80
+
81
+ participant_params: Dict[str, Any] = {
82
+ "page_size": 300,
83
+ }
84
+ meeting_params = {
85
+ "type": "previous_meetings",
86
+ "page_size": 300,
87
+ }
88
+ for user in client.get_users():
89
+ user_id = user["id"]
90
+ for meeting in client.get_meetings(user_id=user_id, params=meeting_params):
91
+ meeting_id = meeting["id"]
92
+ yield from client.get_participants(
93
+ meeting_id=meeting_id,
94
+ params=participant_params,
95
+ start_date=start_dt,
96
+ end_date=end_dt,
97
+ )
98
+
99
+ return meetings, users, participants