ingestr 0.13.23__tar.gz → 0.13.25__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 (258) hide show
  1. {ingestr-0.13.23 → ingestr-0.13.25}/.gitignore +2 -1
  2. {ingestr-0.13.23 → ingestr-0.13.25}/Makefile +1 -1
  3. {ingestr-0.13.23 → ingestr-0.13.25}/PKG-INFO +5 -4
  4. {ingestr-0.13.23 → ingestr-0.13.25}/docs/.vitepress/config.mjs +1 -0
  5. ingestr-0.13.25/docs/supported-sources/frankfurter.md +167 -0
  6. ingestr-0.13.25/ingestr/conftest.py +63 -0
  7. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/main.py +4 -2
  8. ingestr-0.13.25/ingestr/src/buildinfo.py +1 -0
  9. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/factory.py +2 -0
  10. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/filters.py +21 -0
  11. ingestr-0.13.25/ingestr/src/frankfurter/__init__.py +142 -0
  12. ingestr-0.13.25/ingestr/src/frankfurter/helpers.py +32 -0
  13. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/github/helpers.py +5 -5
  14. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/google_sheets/__init__.py +4 -4
  15. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/google_sheets/helpers/data_processing.py +2 -2
  16. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/sources.py +34 -0
  17. {ingestr-0.13.23 → ingestr-0.13.25}/requirements-dev.txt +4 -3
  18. {ingestr-0.13.23 → ingestr-0.13.25}/requirements.in +3 -3
  19. {ingestr-0.13.23 → ingestr-0.13.25}/requirements.txt +5 -3
  20. {ingestr-0.13.23 → ingestr-0.13.25}/requirements_arm64.txt +5 -3
  21. ingestr-0.13.23/ingestr/src/buildinfo.py +0 -1
  22. {ingestr-0.13.23 → ingestr-0.13.25}/.dockerignore +0 -0
  23. {ingestr-0.13.23 → ingestr-0.13.25}/.githooks/pre-commit-hook.sh +0 -0
  24. {ingestr-0.13.23 → ingestr-0.13.25}/.github/workflows/deploy-docs.yml +0 -0
  25. {ingestr-0.13.23 → ingestr-0.13.25}/.github/workflows/release.yml +0 -0
  26. {ingestr-0.13.23 → ingestr-0.13.25}/.github/workflows/secrets-scan.yml +0 -0
  27. {ingestr-0.13.23 → ingestr-0.13.25}/.github/workflows/tests.yml +0 -0
  28. {ingestr-0.13.23 → ingestr-0.13.25}/.gitleaksignore +0 -0
  29. {ingestr-0.13.23 → ingestr-0.13.25}/.python-version +0 -0
  30. {ingestr-0.13.23 → ingestr-0.13.25}/.vale.ini +0 -0
  31. {ingestr-0.13.23 → ingestr-0.13.25}/Dockerfile +0 -0
  32. {ingestr-0.13.23 → ingestr-0.13.25}/LICENSE.md +0 -0
  33. {ingestr-0.13.23 → ingestr-0.13.25}/README.md +0 -0
  34. {ingestr-0.13.23 → ingestr-0.13.25}/docs/.vitepress/theme/custom.css +0 -0
  35. {ingestr-0.13.23 → ingestr-0.13.25}/docs/.vitepress/theme/index.js +0 -0
  36. {ingestr-0.13.23 → ingestr-0.13.25}/docs/commands/example-uris.md +0 -0
  37. {ingestr-0.13.23 → ingestr-0.13.25}/docs/commands/ingest.md +0 -0
  38. {ingestr-0.13.23 → ingestr-0.13.25}/docs/getting-started/core-concepts.md +0 -0
  39. {ingestr-0.13.23 → ingestr-0.13.25}/docs/getting-started/incremental-loading.md +0 -0
  40. {ingestr-0.13.23 → ingestr-0.13.25}/docs/getting-started/quickstart.md +0 -0
  41. {ingestr-0.13.23 → ingestr-0.13.25}/docs/getting-started/telemetry.md +0 -0
  42. {ingestr-0.13.23 → ingestr-0.13.25}/docs/index.md +0 -0
  43. {ingestr-0.13.23 → ingestr-0.13.25}/docs/media/applovin_max.png +0 -0
  44. {ingestr-0.13.23 → ingestr-0.13.25}/docs/media/athena.png +0 -0
  45. {ingestr-0.13.23 → ingestr-0.13.25}/docs/media/clickhouse_img.png +0 -0
  46. {ingestr-0.13.23 → ingestr-0.13.25}/docs/media/github.png +0 -0
  47. {ingestr-0.13.23 → ingestr-0.13.25}/docs/media/googleanalytics.png +0 -0
  48. {ingestr-0.13.23 → ingestr-0.13.25}/docs/media/kinesis.bigquery.png +0 -0
  49. {ingestr-0.13.23 → ingestr-0.13.25}/docs/media/linkedin_ads.png +0 -0
  50. {ingestr-0.13.23 → ingestr-0.13.25}/docs/media/personio.png +0 -0
  51. {ingestr-0.13.23 → ingestr-0.13.25}/docs/media/personio_duckdb.png +0 -0
  52. {ingestr-0.13.23 → ingestr-0.13.25}/docs/media/pipedrive.png +0 -0
  53. {ingestr-0.13.23 → ingestr-0.13.25}/docs/media/stripe_postgres.png +0 -0
  54. {ingestr-0.13.23 → ingestr-0.13.25}/docs/media/tiktok.png +0 -0
  55. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/adjust.md +0 -0
  56. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/airtable.md +0 -0
  57. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/applovin.md +0 -0
  58. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/applovin_max.md +0 -0
  59. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/appsflyer.md +0 -0
  60. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/appstore.md +0 -0
  61. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/asana.md +0 -0
  62. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/athena.md +0 -0
  63. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/bigquery.md +0 -0
  64. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/chess.md +0 -0
  65. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/clickhouse.md +0 -0
  66. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/csv.md +0 -0
  67. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/custom_queries.md +0 -0
  68. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/databricks.md +0 -0
  69. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/db2.md +0 -0
  70. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/duckdb.md +0 -0
  71. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/dynamodb.md +0 -0
  72. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/facebook-ads.md +0 -0
  73. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/gcs.md +0 -0
  74. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/github.md +0 -0
  75. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/google-ads.md +0 -0
  76. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/google_analytics.md +0 -0
  77. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/gorgias.md +0 -0
  78. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/gsheets.md +0 -0
  79. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/hubspot.md +0 -0
  80. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/kafka.md +0 -0
  81. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/kinesis.md +0 -0
  82. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/klaviyo.md +0 -0
  83. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/linkedin_ads.md +0 -0
  84. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/mongodb.md +0 -0
  85. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/mssql.md +0 -0
  86. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/mysql.md +0 -0
  87. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/notion.md +0 -0
  88. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/oracle.md +0 -0
  89. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/personio.md +0 -0
  90. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/pipedrive.md +0 -0
  91. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/postgres.md +0 -0
  92. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/redshift.md +0 -0
  93. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/s3.md +0 -0
  94. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/salesforce.md +0 -0
  95. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/sap-hana.md +0 -0
  96. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/shopify.md +0 -0
  97. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/slack.md +0 -0
  98. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/snowflake.md +0 -0
  99. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/sqlite.md +0 -0
  100. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/stripe.md +0 -0
  101. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/tiktok-ads.md +0 -0
  102. {ingestr-0.13.23 → ingestr-0.13.25}/docs/supported-sources/zendesk.md +0 -0
  103. {ingestr-0.13.23 → ingestr-0.13.25}/docs/tutorials/load-kinesis-bigquery.md +0 -0
  104. {ingestr-0.13.23 → ingestr-0.13.25}/docs/tutorials/load-personio-duckdb.md +0 -0
  105. {ingestr-0.13.23 → ingestr-0.13.25}/docs/tutorials/load-stripe-postgres.md +0 -0
  106. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/.gitignore +0 -0
  107. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/adjust/__init__.py +0 -0
  108. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/adjust/adjust_helpers.py +0 -0
  109. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/airtable/__init__.py +0 -0
  110. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/applovin/__init__.py +0 -0
  111. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/applovin_max/__init__.py +0 -0
  112. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/appsflyer/_init_.py +0 -0
  113. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/appsflyer/client.py +0 -0
  114. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/appstore/__init__.py +0 -0
  115. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/appstore/client.py +0 -0
  116. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/appstore/errors.py +0 -0
  117. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/appstore/models.py +0 -0
  118. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/appstore/resources.py +0 -0
  119. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/arrow/__init__.py +0 -0
  120. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/asana_source/__init__.py +0 -0
  121. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/asana_source/helpers.py +0 -0
  122. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/asana_source/settings.py +0 -0
  123. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/blob.py +0 -0
  124. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/chess/__init__.py +0 -0
  125. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/chess/helpers.py +0 -0
  126. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/chess/settings.py +0 -0
  127. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/destinations.py +0 -0
  128. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/dynamodb/__init__.py +0 -0
  129. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/errors.py +0 -0
  130. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/facebook_ads/__init__.py +0 -0
  131. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/facebook_ads/exceptions.py +0 -0
  132. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/facebook_ads/helpers.py +0 -0
  133. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/facebook_ads/settings.py +0 -0
  134. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/filesystem/__init__.py +0 -0
  135. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/filesystem/helpers.py +0 -0
  136. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/filesystem/readers.py +0 -0
  137. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/github/__init__.py +0 -0
  138. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/github/queries.py +0 -0
  139. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/github/settings.py +0 -0
  140. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/google_ads/__init__.py +0 -0
  141. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/google_ads/field.py +0 -0
  142. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/google_ads/metrics.py +0 -0
  143. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/google_ads/predicates.py +0 -0
  144. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/google_ads/reports.py +0 -0
  145. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/google_analytics/__init__.py +0 -0
  146. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/google_analytics/helpers.py +0 -0
  147. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/google_sheets/README.md +0 -0
  148. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/google_sheets/helpers/__init__.py +0 -0
  149. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/google_sheets/helpers/api_calls.py +0 -0
  150. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/gorgias/__init__.py +0 -0
  151. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/gorgias/helpers.py +0 -0
  152. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/hubspot/__init__.py +0 -0
  153. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/hubspot/helpers.py +0 -0
  154. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/hubspot/settings.py +0 -0
  155. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/kafka/__init__.py +0 -0
  156. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/kafka/helpers.py +0 -0
  157. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/kinesis/__init__.py +0 -0
  158. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/kinesis/helpers.py +0 -0
  159. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/klaviyo/_init_.py +0 -0
  160. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/klaviyo/client.py +0 -0
  161. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/klaviyo/helpers.py +0 -0
  162. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/linkedin_ads/__init__.py +0 -0
  163. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/linkedin_ads/dimension_time_enum.py +0 -0
  164. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/linkedin_ads/helpers.py +0 -0
  165. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/loader.py +0 -0
  166. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/mongodb/__init__.py +0 -0
  167. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/mongodb/helpers.py +0 -0
  168. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/notion/__init__.py +0 -0
  169. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/notion/helpers/__init__.py +0 -0
  170. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/notion/helpers/client.py +0 -0
  171. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/notion/helpers/database.py +0 -0
  172. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/notion/settings.py +0 -0
  173. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/partition.py +0 -0
  174. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/personio/__init__.py +0 -0
  175. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/personio/helpers.py +0 -0
  176. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/pipedrive/__init__.py +0 -0
  177. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/pipedrive/helpers/__init__.py +0 -0
  178. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/pipedrive/helpers/custom_fields_munger.py +0 -0
  179. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/pipedrive/helpers/pages.py +0 -0
  180. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/pipedrive/settings.py +0 -0
  181. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/pipedrive/typing.py +0 -0
  182. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/resource.py +0 -0
  183. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/salesforce/__init__.py +0 -0
  184. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/salesforce/helpers.py +0 -0
  185. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/shopify/__init__.py +0 -0
  186. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/shopify/exceptions.py +0 -0
  187. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/shopify/helpers.py +0 -0
  188. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/shopify/settings.py +0 -0
  189. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/slack/__init__.py +0 -0
  190. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/slack/helpers.py +0 -0
  191. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/slack/settings.py +0 -0
  192. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/sql_database/__init__.py +0 -0
  193. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/sql_database/callbacks.py +0 -0
  194. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/stripe_analytics/__init__.py +0 -0
  195. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/stripe_analytics/helpers.py +0 -0
  196. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/stripe_analytics/settings.py +0 -0
  197. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/table_definition.py +0 -0
  198. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/telemetry/event.py +0 -0
  199. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/testdata/fakebqcredentials.json +0 -0
  200. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/tiktok_ads/__init__.py +0 -0
  201. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/tiktok_ads/tiktok_helpers.py +0 -0
  202. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/time.py +0 -0
  203. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/version.py +0 -0
  204. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/zendesk/__init__.py +0 -0
  205. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/zendesk/helpers/__init__.py +0 -0
  206. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/zendesk/helpers/api_helpers.py +0 -0
  207. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/zendesk/helpers/credentials.py +0 -0
  208. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/zendesk/helpers/talk_api.py +0 -0
  209. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/src/zendesk/settings.py +0 -0
  210. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/testdata/.gitignore +0 -0
  211. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/testdata/create_replace.csv +0 -0
  212. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/testdata/delete_insert_expected.csv +0 -0
  213. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/testdata/delete_insert_part1.csv +0 -0
  214. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/testdata/delete_insert_part2.csv +0 -0
  215. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/testdata/merge_expected.csv +0 -0
  216. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/testdata/merge_part1.csv +0 -0
  217. {ingestr-0.13.23 → ingestr-0.13.25}/ingestr/testdata/merge_part2.csv +0 -0
  218. {ingestr-0.13.23 → ingestr-0.13.25}/package-lock.json +0 -0
  219. {ingestr-0.13.23 → ingestr-0.13.25}/package.json +0 -0
  220. {ingestr-0.13.23 → ingestr-0.13.25}/pyproject.toml +0 -0
  221. {ingestr-0.13.23 → ingestr-0.13.25}/resources/demo.gif +0 -0
  222. {ingestr-0.13.23 → ingestr-0.13.25}/resources/demo.tape +0 -0
  223. {ingestr-0.13.23 → ingestr-0.13.25}/resources/ingestr.svg +0 -0
  224. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/AMPM.yml +0 -0
  225. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/Acronyms.yml +0 -0
  226. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/Colons.yml +0 -0
  227. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/Contractions.yml +0 -0
  228. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/DateFormat.yml +0 -0
  229. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/Ellipses.yml +0 -0
  230. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/EmDash.yml +0 -0
  231. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/Exclamation.yml +0 -0
  232. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/FirstPerson.yml +0 -0
  233. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/Gender.yml +0 -0
  234. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/GenderBias.yml +0 -0
  235. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/HeadingPunctuation.yml +0 -0
  236. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/Headings.yml +0 -0
  237. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/Latin.yml +0 -0
  238. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/LyHyphens.yml +0 -0
  239. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/OptionalPlurals.yml +0 -0
  240. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/Ordinal.yml +0 -0
  241. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/OxfordComma.yml +0 -0
  242. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/Parens.yml +0 -0
  243. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/Passive.yml +0 -0
  244. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/Periods.yml +0 -0
  245. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/Quotes.yml +0 -0
  246. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/Ranges.yml +0 -0
  247. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/Semicolons.yml +0 -0
  248. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/Slang.yml +0 -0
  249. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/Spacing.yml +0 -0
  250. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/Spelling.yml +0 -0
  251. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/Units.yml +0 -0
  252. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/We.yml +0 -0
  253. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/Will.yml +0 -0
  254. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/WordList.yml +0 -0
  255. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/meta.json +0 -0
  256. {ingestr-0.13.23 → ingestr-0.13.25}/styles/Google/vocab.txt +0 -0
  257. {ingestr-0.13.23 → ingestr-0.13.25}/styles/bruin/Ingestr.yml +0 -0
  258. {ingestr-0.13.23 → ingestr-0.13.25}/styles/config/vocabularies/bruin/accept.txt +0 -0
@@ -19,4 +19,5 @@ node_modules
19
19
  *.duckdb.wal
20
20
  *.db
21
21
  *.db.wal
22
- *.sql
22
+ *.sql
23
+ ingest.sh
@@ -28,7 +28,7 @@ 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; pytest -rP -vv --tb=short --capture=no -k $(test)
31
+ . venv/bin/activate; 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.23
3
+ Version: 0.13.25
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
@@ -46,10 +46,10 @@ Requires-Dist: databricks-sqlalchemy==1.0.2
46
46
  Requires-Dist: dataclasses-json==0.6.7
47
47
  Requires-Dist: decorator==5.2.1
48
48
  Requires-Dist: deprecation==2.1.0
49
- Requires-Dist: dlt==1.6.1
49
+ Requires-Dist: dlt==1.9.0
50
50
  Requires-Dist: dnspython==2.7.0
51
- Requires-Dist: duckdb-engine==0.15.0
52
- Requires-Dist: duckdb==1.2.0
51
+ Requires-Dist: duckdb-engine==0.17.0
52
+ Requires-Dist: duckdb==1.2.1
53
53
  Requires-Dist: et-xmlfile==2.0.0
54
54
  Requires-Dist: facebook-business==20.0.0
55
55
  Requires-Dist: filelock==3.17.0
@@ -168,6 +168,7 @@ Requires-Dist: sqlalchemy-hana==2.0.0
168
168
  Requires-Dist: sqlalchemy-redshift==0.8.14
169
169
  Requires-Dist: sqlalchemy2-stubs==0.0.2a38
170
170
  Requires-Dist: sqlalchemy==1.4.52
171
+ Requires-Dist: sqlglot==26.12.1
171
172
  Requires-Dist: stripe==10.7.0
172
173
  Requires-Dist: tenacity==9.0.0
173
174
  Requires-Dist: thrift==0.16.0
@@ -115,6 +115,7 @@ export default defineConfig({
115
115
  text: "Facebook Ads",
116
116
  link: "/supported-sources/facebook-ads.md",
117
117
  },
118
+ { text: "Frankfurter", link: "/supported-sources/frankfurter.md" },
118
119
  { text: "Google Cloud Storage (GCS)", link: "/supported-sources/gcs.md" },
119
120
  { text: "Google Analytics", link: "/supported-sources/google_analytics.md" },
120
121
  { text: "Google Ads", link: "/supported-sources/google-ads.md" },
@@ -0,0 +1,167 @@
1
+ # **Frankfurter Source Documentation**
2
+
3
+ The `frankfurter` source in the `ingestr` pipeline is designed to fetch exchange rate data from the [Frankfurter API](https://www.frankfurter.dev/). This source supports fetching the latest exchange rates, historical exchange rates, and currency metadata. The data can be ingested into a specified destination database, such as DuckDB.
4
+
5
+ ---
6
+
7
+ ## **Command Overview**
8
+
9
+ The `ingestr` command to use the `frankfurter` source is as follows:
10
+
11
+ ```bash
12
+ ingestr ingest \
13
+ --source-uri 'frankfurter://' \
14
+ --interval-start '2025-03-27' \ # Optional. See 'exchange_rates'.
15
+ --interval-end '2025-03-28' \ # Optional.
16
+ --source-table '<table_name>' \ # E.g 'currencies', 'latest', 'exchange_rates'. See below.
17
+ --dest-uri '<your-destination-uri>' \
18
+ --dest-table '<your-schema>.<your-table_name>'
19
+ ```
20
+
21
+ ---
22
+
23
+ ## **Command Parameters**
24
+
25
+ ### **`--source-uri`**
26
+ - **Description**: Specifies the source URI for the Frankfurter API.
27
+ - **Value**: `'frankfurter://'`
28
+ - **Purpose**: Indicates that the data will be fetched from the Frankfurter API.
29
+
30
+ ---
31
+
32
+ ### **`--interval-start` (Optional)**
33
+ - **Description**: The start date for fetching historical exchange rates.
34
+ - **Value**: A date in the format `YYYY-MM-DD` (e.g., `'2025-03-27'`).
35
+ - **Purpose**: Defines the starting point for fetching historical data.
36
+ - For `latest` and `currencies` this parameter is ignored.
37
+ - For `exchange_rates`, it defaults to the current date if not provided.
38
+
39
+ ---
40
+
41
+ ### **`--interval-end` (Optional)**
42
+ - **Description**: The end date for fetching historical exchange rates.
43
+ - **Value**: A date in the format `YYYY-MM-DD` (e.g., `'2025-03-28'`).
44
+ - **Purpose**: Defines the ending point for fetching historical data. If not provided, it defaults to the value of `--interval-start`.
45
+ - For `latest` and `currencies` this parameter is ignored.
46
+
47
+ ---
48
+
49
+ ### **`--source-table`**
50
+ - **Description**: Specifies the table to fetch data from.
51
+ - **Value**: One of the following:
52
+ - **`currencies`**: Fetches a list of the available currencies and their ISO 4217 currency code (e.g. `USD`, `EUR`, `GBP`).
53
+ - **`latest`**: Fetches the latest exchange rates.
54
+ - **`exchange_rates`**: Fetches historical exchange rates for a specified date range.
55
+ - **Purpose**: Determines the type of data to fetch from the Frankfurter API.
56
+
57
+ ---
58
+
59
+ ### **`--dest-uri`**
60
+ - **Description**: Specifies the destination database URI.
61
+ - **Value**: The path to the database file (e.g., `'duckdb.db'`).
62
+ - **Purpose**: Defines where the fetched data will be stored.
63
+
64
+ ---
65
+
66
+ ### **`--dest-table`**
67
+ - **Description**: Specifies the destination table name.
68
+ - **Value**: A string in the format `{schema}.{table_name}` (e.g., `'schema.my_table'`).
69
+ - **Purpose**: Defines the schema and table name where the data will be written.
70
+ - **Notes**:
71
+ - If the destination table does not yet exist in your database, ingestr will automatically create the table with name that is provided in this argument. The table will be structured according to the source table (see [Core Concepts](https://bruin-data.github.io/ingestr/getting-started/core-concepts.html)).
72
+
73
+ ---
74
+
75
+ ## **Supported Source Tables**
76
+
77
+ ### **`currencies`**
78
+ - **Description**: Fetches a list of available currencies.
79
+ - **Columns**:
80
+ - `currency_code`: The ISO 4217 currency code (e.g., `USD`, `EUR`).
81
+ - `currency_name`: The name of the currency (e.g., `US Dollar`, `Euro`).
82
+ - **Primary Key**: `currency_code`
83
+
84
+ ---
85
+
86
+ ### **`latest`**
87
+ - **Description**: Fetches the latest exchange rates.
88
+ - **Columns**:
89
+ - `date`: The date of the exchange rates.
90
+ - `currency_name`: The ISO 4217 currency code (e.g., `USD`, `EUR`).
91
+ - `rate`: The exchange rate relative to the base currency.
92
+ - **Primary Key**: Composite key of `date` and `currency_name`.
93
+ - **Notes**:
94
+ - The base currency (e.g., `EUR`) is included with a rate of `1.0`.
95
+
96
+ ---
97
+
98
+ ### **`exchange_rates`**
99
+ - **Description**: Fetches historical exchange rates for a specified date range.
100
+ - **Columns**:
101
+ - `date`: The date of the exchange rates.
102
+ - `currency_name`: The ISO 4217 currency code (e.g., `USD`, `EUR`).
103
+ - `rate`: The exchange rate relative to the base currency.
104
+ - **Primary Key**: Composite key of `date` and `currency_name`.
105
+ - **Notes**:
106
+ - An optional start and end date can be added via the arguments `--interval-start` and optionally `--interval-end` to define the date range (see examples below). If no start date is specified, the date will default today's date (and thus return the latest exchange rates).
107
+ - If a start date but no end date is specified, then the end date will default to the start date and ingestr will retrieve data for the specified start date only.
108
+ - Note that the [Frankfurter API](https://www.frankfurter.dev/) only publishes updates Monday-Friday. If the given date is on the weekend, the date will default to the previous Friday. The source is however implemented in ingestr in such a way as to avoid duplicating rows of data in this case (see [Incremental Loading - Replace](https://bruin-data.github.io/ingestr/getting-started/incremental-loading.html)).
109
+
110
+ #### **Example Table: Handling Weekend Dates**
111
+ Here `--interval-start` is set to a weekend date (e.g., `2025-03-29` -- a Saturday). `--interval-end` is set to a the following Monday (`2025-03-31`).
112
+
113
+ `--interval-start` defaults to the previous Friday (`2025-03-28`) and the next data is from the following Monday (for simplicity, only a subset of currencies is shown below):
114
+
115
+ | **date** | **currency_name** | **rate** |
116
+ |--------------|-------------------|----------|
117
+ | 2025-03-28 | EUR | 1.0 |
118
+ | 2025-03-28 | USD | 1.0783 |
119
+ | 2025-03-28 | GBP | 0.8571 |
120
+ | 2025-03-31 | EUR | 1.0 |
121
+ | 2025-03-31 | USD | 1.0783 |
122
+ | 2025-03-31 | GBP | 0.8571 |
123
+
124
+
125
+ ---
126
+
127
+ ## **Examples**
128
+
129
+ ### **1. Fetch the Latest Exchange Rates**
130
+ ```bash
131
+ ingestr ingest \
132
+ --source-uri 'frankfurter://' \
133
+ --source-table 'latest' \
134
+ --dest-uri 'duckdb.db' \
135
+ --dest-table 'schema.latest_new_scheme'
136
+ ```
137
+
138
+ ---
139
+
140
+ ### **2. Fetch Historical Exchange Rates**
141
+ ```bash
142
+ ingestr ingest \
143
+ --source-uri 'frankfurter://' \
144
+ --interval-start '2025-03-01' \
145
+ --interval-end '2025-03-10' \
146
+ --source-table 'exchange_rates' \
147
+ --dest-uri 'duckdb.db' \
148
+ --dest-table 'schema.exchange_rates'
149
+ ```
150
+
151
+ ---
152
+
153
+ ### **3. Fetch Currency Metadata**
154
+ ```bash
155
+ ingestr ingest \
156
+ --source-uri 'frankfurter://' \
157
+ --source-table 'currencies' \
158
+ --dest-uri 'duckdb.db' \
159
+ --dest-table 'schema.currencies'
160
+ ```
161
+
162
+ ---
163
+
164
+ ## **Notes**
165
+ - Ensure that the destination database (`--dest-uri`) is accessible and writable.
166
+ - The `--interval-start` and `--interval-end` parameters are only applicable for the `exchange_rates` table.
167
+ - The `latest` table always fetches the most recent exchange rates and ignores date parameters.
@@ -0,0 +1,63 @@
1
+ import os
2
+ import tempfile
3
+ from concurrent.futures import ThreadPoolExecutor
4
+
5
+ from main_test import DESTINATIONS, SOURCES # type: ignore
6
+
7
+
8
+ def pytest_configure(config):
9
+ if is_master(config):
10
+ config.shared_directory = tempfile.mkdtemp()
11
+
12
+
13
+ def pytest_configure_node(node):
14
+ """xdist hook"""
15
+ node.workerinput["shared_directory"] = node.config.shared_directory
16
+
17
+
18
+ def is_master(config):
19
+ """True if the code running the given pytest.config object is running in a xdist master
20
+ node or not running xdist at all.
21
+ """
22
+ return not hasattr(config, "workerinput")
23
+
24
+
25
+ def start_containers(config):
26
+ if hasattr(config, "workerinput"):
27
+ return
28
+
29
+ unique_containers = set(SOURCES.values()) | set(DESTINATIONS.values())
30
+ for container in unique_containers:
31
+ container.container_lock_dir = config.shared_directory
32
+
33
+ with ThreadPoolExecutor() as executor:
34
+ for container in unique_containers:
35
+ executor.submit(container.start_fully)
36
+ # futures = [
37
+ # executor.submit(container.start_fully) for container in unique_containers
38
+ # ]
39
+ # # Wait for all futures to complete
40
+ # for future in futures:
41
+ # future.result()
42
+
43
+
44
+ def stop_containers(config):
45
+ if hasattr(config, "workerinput"):
46
+ return
47
+
48
+ should_manage_containers = os.environ.get("PYTEST_XDIST_WORKER", "gw0") == "gw0"
49
+ if not should_manage_containers:
50
+ return
51
+
52
+ unique_containers = set(SOURCES.values()) | set(DESTINATIONS.values())
53
+
54
+ for container in unique_containers:
55
+ container.stop_fully()
56
+
57
+
58
+ def pytest_sessionstart(session):
59
+ start_containers(session.config)
60
+
61
+
62
+ def pytest_sessionfinish(session, exitstatus):
63
+ stop_containers(session.config)
@@ -11,7 +11,7 @@ from typing_extensions import Annotated
11
11
  import ingestr.src.partition as partition
12
12
  import ingestr.src.resource as resource
13
13
  from ingestr.src.destinations import AthenaDestination
14
- from ingestr.src.filters import cast_set_to_list
14
+ from ingestr.src.filters import cast_set_to_list, handle_mysql_empty_dates
15
15
  from ingestr.src.telemetry.event import track
16
16
 
17
17
  app = typer.Typer(
@@ -35,7 +35,7 @@ DATE_FORMATS = [
35
35
 
36
36
  # https://dlthub.com/docs/dlt-ecosystem/file-formats/parquet#supported-destinations
37
37
  PARQUET_SUPPORTED_DESTINATIONS = [
38
- "athena" "bigquery",
38
+ "athenabigquery",
39
39
  "duckdb",
40
40
  "snowflake",
41
41
  "databricks",
@@ -553,6 +553,8 @@ def ingest(
553
553
  )
554
554
 
555
555
  resource.for_each(dlt_source, lambda x: x.add_map(cast_set_to_list))
556
+ if factory.source_scheme.startswith("mysql"):
557
+ resource.for_each(dlt_source, lambda x: x.add_map(handle_mysql_empty_dates))
556
558
 
557
559
  def col_h(x):
558
560
  if column_hints:
@@ -0,0 +1 @@
1
+ version = "v0.13.25"
@@ -28,6 +28,7 @@ from ingestr.src.sources import (
28
28
  ChessSource,
29
29
  DynamoDBSource,
30
30
  FacebookAdsSource,
31
+ FrankfurterSource,
31
32
  GCSSource,
32
33
  GitHubSource,
33
34
  GoogleAdsSource,
@@ -146,6 +147,7 @@ class SourceDestinationFactory:
146
147
  "personio": PersonioSource,
147
148
  "kinesis": KinesisSource,
148
149
  "pipedrive": PipedriveSource,
150
+ "frankfurter": FrankfurterSource,
149
151
  }
150
152
  destinations: Dict[str, Type[DestinationProtocol]] = {
151
153
  "bigquery": BigQueryDestination,
@@ -10,6 +10,27 @@ def cast_set_to_list(row):
10
10
  return row
11
11
 
12
12
 
13
+ def handle_mysql_empty_dates(row):
14
+ # MySQL returns empty dates as 0000-00-00, which is not a valid date, we handle them here.
15
+ if not isinstance(row, dict):
16
+ return row
17
+
18
+ for key in row.keys():
19
+ if not isinstance(row[key], str):
20
+ continue
21
+
22
+ if row[key] == "0000-00-00":
23
+ from datetime import date
24
+
25
+ row[key] = date(1970, 1, 1)
26
+
27
+ elif row[key] == "0000-00-00 00:00:00":
28
+ from datetime import datetime
29
+
30
+ row[key] = datetime(1970, 1, 1, 0, 0, 0)
31
+ return row
32
+
33
+
13
34
  def table_adapter_exclude_columns(cols: list[str]):
14
35
  def excluder(table: Table):
15
36
  cols_to_remove = [col for col in table._columns if col.name in cols] # type: ignore
@@ -0,0 +1,142 @@
1
+ from typing import Any, Iterator, Optional
2
+
3
+ import dlt
4
+ from dlt.common.pendulum import pendulum
5
+ from dlt.common.time import ensure_pendulum_datetime
6
+ from dlt.common.typing import TAnyDateTime
7
+
8
+ from ingestr.src.frankfurter.helpers import get_path_with_retry
9
+
10
+
11
+ @dlt.source(
12
+ name="frankfurter",
13
+ max_table_nesting=0,
14
+ )
15
+ def frankfurter_source(
16
+ table: str,
17
+ start_date: Optional[TAnyDateTime] = None,
18
+ end_date: Optional[TAnyDateTime] = None,
19
+ ) -> Any:
20
+ """
21
+ A dlt source for the frankfurter.dev API. It groups several resources (in this case frankfurter.dev API endpoints) containing
22
+ various types of data: currencies, latest rates, historical rates.
23
+
24
+ Returns the appropriate resource based on the provided parameters.
25
+ """
26
+ # Determine which resource to return based on the `table` parameter
27
+ if table == "currencies":
28
+ return currencies()
29
+
30
+ elif table == "latest":
31
+ return latest()
32
+
33
+ elif table == "exchange_rates":
34
+ return exchange_rates(start_date=start_date, end_date=end_date)
35
+
36
+
37
+ @dlt.resource(
38
+ write_disposition="replace",
39
+ columns={
40
+ "currency_code": {"data_type": "text"},
41
+ "currency_name": {"data_type": "text"},
42
+ },
43
+ )
44
+ def currencies() -> Iterator[dict]:
45
+ """
46
+ Yields each currency as a separate row with two columns: currency_code and currency_name.
47
+ """
48
+ # Retrieve the list of currencies from the API
49
+ currencies_data = get_path_with_retry("currencies")
50
+
51
+ for currency_code, currency_name in currencies_data.items():
52
+ yield {"currency_code": currency_code, "currency_name": currency_name}
53
+
54
+
55
+ @dlt.resource(
56
+ write_disposition="replace",
57
+ columns={
58
+ "date": {"data_type": "text"},
59
+ "currency_name": {"data_type": "text"},
60
+ "rate": {"data_type": "double"},
61
+ },
62
+ primary_key=["date", "currency_name"], # Composite primary key
63
+ )
64
+ def latest() -> Iterator[dict]:
65
+ """
66
+ Fetches the latest exchange rates and yields them as rows.
67
+ """
68
+ # Base URL
69
+ url = "latest?"
70
+
71
+ # Fetch data
72
+ latest_data = get_path_with_retry(url)
73
+
74
+ # Extract rates and base currency
75
+ rates = latest_data["rates"]
76
+
77
+ # Prepare the date
78
+ date = pendulum.now().to_date_string()
79
+
80
+ # Add the base currency (EUR) with a rate of 1.0
81
+ yield {
82
+ "date": date,
83
+ "currency_name": "EUR",
84
+ "rate": 1.0,
85
+ }
86
+
87
+ # Add all currencies and their rates
88
+ for currency_name, rate in rates.items():
89
+ yield {
90
+ "date": date,
91
+ "currency_name": currency_name,
92
+ "rate": rate,
93
+ }
94
+
95
+
96
+ @dlt.resource(
97
+ write_disposition="replace",
98
+ columns={
99
+ "date": {"data_type": "text"},
100
+ "currency_name": {"data_type": "text"},
101
+ "rate": {"data_type": "double"},
102
+ },
103
+ primary_key=["date", "currency_name"], # Composite primary key
104
+ )
105
+ def exchange_rates(
106
+ start_date: TAnyDateTime,
107
+ end_date: TAnyDateTime,
108
+ ) -> Iterator[dict]:
109
+ """
110
+ Fetches exchange rates for a specified date range.
111
+ If only start_date is provided, fetches data for that date.
112
+ If both start_date and end_date are provided, fetches data for each day in the range.
113
+ """
114
+ start_date_str = ensure_pendulum_datetime(start_date).format("YYYY-MM-DD")
115
+ end_date_str = ensure_pendulum_datetime(end_date).format("YYYY-MM-DD")
116
+
117
+ # Compose the URL
118
+ url = f"{start_date_str}..{end_date_str}?"
119
+
120
+ # Fetch data from the API
121
+ data = get_path_with_retry(url)
122
+
123
+ # Extract base currency and rates from the API response
124
+ base_currency = data["base"]
125
+ rates = data["rates"]
126
+
127
+ # Iterate over the rates dictionary (one entry per date)
128
+ for date, daily_rates in rates.items():
129
+ # Add the base currency with a rate of 1.0
130
+ yield {
131
+ "date": date,
132
+ "currency_name": base_currency,
133
+ "rate": 1.0,
134
+ }
135
+
136
+ # Add all other currencies and their rates
137
+ for currency_name, rate in daily_rates.items():
138
+ yield {
139
+ "date": date,
140
+ "currency_name": currency_name,
141
+ "rate": rate,
142
+ }
@@ -0,0 +1,32 @@
1
+ from datetime import datetime
2
+
3
+ from dlt.common.pendulum import pendulum
4
+ from dlt.common.typing import StrAny
5
+ from dlt.sources.helpers import requests
6
+
7
+ FRANKFURTER_API_URL = "https://api.frankfurter.dev/v1/"
8
+
9
+
10
+ def get_url_with_retry(url: str) -> StrAny:
11
+ r = requests.get(url)
12
+ return r.json() # type: ignore
13
+
14
+
15
+ def get_path_with_retry(path: str) -> StrAny:
16
+ return get_url_with_retry(f"{FRANKFURTER_API_URL}{path}")
17
+
18
+
19
+ def validate_dates(start_date: datetime, end_date: datetime) -> None:
20
+ current_date = pendulum.now()
21
+
22
+ # Check if start_date is in the future
23
+ if start_date > current_date:
24
+ raise ValueError("Interval-start cannot be in the future.")
25
+
26
+ # Check if end_date is in the future
27
+ if end_date > current_date:
28
+ raise ValueError("Interval-end cannot be in the future.")
29
+
30
+ # Check if start_date is before end_date
31
+ if start_date > end_date:
32
+ raise ValueError("Interval-end cannot be before interval-start.")
@@ -103,9 +103,9 @@ def get_reactions_data(
103
103
 
104
104
 
105
105
  def _extract_top_connection(data: StrAny, node_type: str) -> StrAny:
106
- assert (
107
- isinstance(data, dict) and len(data) == 1
108
- ), f"The data with list of {node_type} must be a dictionary and contain only one element"
106
+ assert isinstance(data, dict) and len(data) == 1, (
107
+ f"The data with list of {node_type} must be a dictionary and contain only one element"
108
+ )
109
109
  data = next(iter(data.values()))
110
110
  return data[node_type] # type: ignore
111
111
 
@@ -158,7 +158,7 @@ def _get_graphql_pages(
158
158
  )
159
159
  items_count += len(data_items)
160
160
  print(
161
- f'Got {len(data_items)}/{items_count} {node_type}s, query cost {rate_limit["cost"]}, remaining credits: {rate_limit["remaining"]}'
161
+ f"Got {len(data_items)}/{items_count} {node_type}s, query cost {rate_limit['cost']}, remaining credits: {rate_limit['remaining']}"
162
162
  )
163
163
  if data_items:
164
164
  yield data_items
@@ -187,7 +187,7 @@ def _get_comment_reaction(comment_ids: List[str], access_token: str) -> StrAny:
187
187
  # print(query)
188
188
  page, rate_limit = _run_graphql_query(access_token, query, {})
189
189
  print(
190
- f'Got {len(page)} comments, query cost {rate_limit["cost"]}, remaining credits: {rate_limit["remaining"]}'
190
+ f"Got {len(page)} comments, query cost {rate_limit['cost']}, remaining credits: {rate_limit['remaining']}"
191
191
  )
192
192
  data.update(page)
193
193
  return data
@@ -70,9 +70,9 @@ def google_spreadsheet(
70
70
  spreadsheet_id=spreadsheet_id,
71
71
  range_names=list(all_range_names),
72
72
  )
73
- assert len(all_range_names) == len(
74
- all_range_data
75
- ), "Google Sheets API must return values for all requested ranges"
73
+ assert len(all_range_names) == len(all_range_data), (
74
+ "Google Sheets API must return values for all requested ranges"
75
+ )
76
76
 
77
77
  # get metadata for two first rows of each range
78
78
  # first should contain headers
@@ -126,7 +126,7 @@ def google_spreadsheet(
126
126
  headers = get_range_headers(headers_metadata, name)
127
127
  if headers is None:
128
128
  # generate automatic headers and treat the first row as data
129
- headers = [f"col_{idx+1}" for idx in range(len(headers_metadata))]
129
+ headers = [f"col_{idx + 1}" for idx in range(len(headers_metadata))]
130
130
  data_row_metadata = headers_metadata
131
131
  rows_data = values[0:]
132
132
  logger.warning(
@@ -149,12 +149,12 @@ def get_range_headers(headers_metadata: List[DictStrAny], range_name: str) -> Li
149
149
  header_val = str(f"col_{idx + 1}")
150
150
  else:
151
151
  logger.warning(
152
- f"In range {range_name}, header value: {header_val} at position {idx+1} is not a string!"
152
+ f"In range {range_name}, header value: {header_val} at position {idx + 1} is not a string!"
153
153
  )
154
154
  return None
155
155
  else:
156
156
  logger.warning(
157
- f"In range {range_name}, header at position {idx+1} is not missing!"
157
+ f"In range {range_name}, header at position {idx + 1} is not missing!"
158
158
  )
159
159
  return None
160
160
  headers.append(header_val)
@@ -67,6 +67,8 @@ from ingestr.src.errors import (
67
67
  from ingestr.src.facebook_ads import facebook_ads_source, facebook_insights_source
68
68
  from ingestr.src.filesystem import readers
69
69
  from ingestr.src.filters import table_adapter_exclude_columns
70
+ from ingestr.src.frankfurter import frankfurter_source
71
+ from ingestr.src.frankfurter.helpers import validate_dates
70
72
  from ingestr.src.github import github_reactions, github_repo_events, github_stargazers
71
73
  from ingestr.src.google_ads import google_ads
72
74
  from ingestr.src.google_analytics import google_analytics
@@ -2041,3 +2043,35 @@ class PipedriveSource:
2041
2043
  return pipedrive_source(
2042
2044
  pipedrive_api_key=api_key, since_timestamp=start_date
2043
2045
  ).with_resources(table)
2046
+
2047
+
2048
+ class FrankfurterSource:
2049
+ def handles_incrementality(self) -> bool:
2050
+ return True
2051
+
2052
+ def dlt_source(self, uri: str, table: str, **kwargs):
2053
+ # start and end dates only assigned and validated for exchange_rates table
2054
+ # Note: if an end date but no start date is provided, start date and end date will be set to current date
2055
+ if table == "exchange_rates":
2056
+ if kwargs.get("interval_start"):
2057
+ start_date = ensure_pendulum_datetime(str(kwargs.get("interval_start")))
2058
+ if kwargs.get("interval_end"):
2059
+ end_date = ensure_pendulum_datetime(str(kwargs.get("interval_end")))
2060
+ else:
2061
+ end_date = start_date
2062
+ else:
2063
+ start_date = pendulum.now()
2064
+ end_date = pendulum.now()
2065
+ validate_dates(start_date=start_date, end_date=end_date)
2066
+
2067
+ # Validate table
2068
+ if table not in ["currencies", "latest", "exchange_rates"]:
2069
+ raise ValueError(
2070
+ f"Table '{table}' is not supported for Frankfurter source."
2071
+ )
2072
+
2073
+ return frankfurter_source(
2074
+ table=table,
2075
+ start_date=start_date,
2076
+ end_date=end_date,
2077
+ )
@@ -1,13 +1,14 @@
1
1
  -r requirements.txt
2
2
 
3
- mypy==1.13.0
3
+ mypy==1.15.0
4
4
  pytest-cov==4.1.0
5
5
  pytest==8.3.3
6
- ruff==0.8.6
6
+ ruff==0.11.4
7
7
  hatchling==1.27.0
8
8
  build==1.2.1
9
9
  pyodbc==5.2.0
10
10
  twine==6.0.1
11
11
  testcontainers[postgres,mysql]==4.8.2
12
12
  pytest-xdist[psutil]==3.6.1
13
- pkginfo==1.12.0
13
+ pkginfo==1.12.0
14
+ pytest-repeat==0.9.3