ingestr 0.7.8__tar.gz → 0.8.2__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 (123) hide show
  1. {ingestr-0.7.8 → ingestr-0.8.2}/PKG-INFO +10 -1
  2. {ingestr-0.7.8 → ingestr-0.8.2}/README.md +8 -0
  3. {ingestr-0.7.8 → ingestr-0.8.2}/docs/.vitepress/config.mjs +2 -0
  4. ingestr-0.8.2/docs/supported-sources/adjust.md +30 -0
  5. ingestr-0.8.2/docs/supported-sources/appsflyer.md +28 -0
  6. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/shopify.md +6 -0
  7. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/main.py +1 -0
  8. ingestr-0.8.2/ingestr/src/adjust/_init_.py +31 -0
  9. ingestr-0.8.2/ingestr/src/adjust/helpers.py +82 -0
  10. ingestr-0.8.2/ingestr/src/appsflyer/_init_.py +24 -0
  11. ingestr-0.8.2/ingestr/src/appsflyer/client.py +106 -0
  12. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/factory.py +6 -0
  13. ingestr-0.8.2/ingestr/src/shopify/__init__.py +1940 -0
  14. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/shopify/helpers.py +73 -32
  15. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/sources.py +89 -7
  16. ingestr-0.8.2/ingestr/src/version.py +1 -0
  17. {ingestr-0.7.8 → ingestr-0.8.2}/pyproject.toml +5 -1
  18. {ingestr-0.7.8 → ingestr-0.8.2}/requirements.txt +1 -0
  19. ingestr-0.7.8/ingestr/src/shopify/__init__.py +0 -227
  20. ingestr-0.7.8/ingestr/src/version.py +0 -1
  21. {ingestr-0.7.8 → ingestr-0.8.2}/.dockerignore +0 -0
  22. {ingestr-0.7.8 → ingestr-0.8.2}/.github/workflows/deploy-docs.yml +0 -0
  23. {ingestr-0.7.8 → ingestr-0.8.2}/.github/workflows/tests.yml +0 -0
  24. {ingestr-0.7.8 → ingestr-0.8.2}/.gitignore +0 -0
  25. {ingestr-0.7.8 → ingestr-0.8.2}/.python-version +0 -0
  26. {ingestr-0.7.8 → ingestr-0.8.2}/Dockerfile +0 -0
  27. {ingestr-0.7.8 → ingestr-0.8.2}/LICENSE.md +0 -0
  28. {ingestr-0.7.8 → ingestr-0.8.2}/Makefile +0 -0
  29. {ingestr-0.7.8 → ingestr-0.8.2}/docs/.vitepress/theme/custom.css +0 -0
  30. {ingestr-0.7.8 → ingestr-0.8.2}/docs/.vitepress/theme/index.js +0 -0
  31. {ingestr-0.7.8 → ingestr-0.8.2}/docs/commands/example-uris.md +0 -0
  32. {ingestr-0.7.8 → ingestr-0.8.2}/docs/commands/ingest.md +0 -0
  33. {ingestr-0.7.8 → ingestr-0.8.2}/docs/getting-started/core-concepts.md +0 -0
  34. {ingestr-0.7.8 → ingestr-0.8.2}/docs/getting-started/incremental-loading.md +0 -0
  35. {ingestr-0.7.8 → ingestr-0.8.2}/docs/getting-started/quickstart.md +0 -0
  36. {ingestr-0.7.8 → ingestr-0.8.2}/docs/getting-started/telemetry.md +0 -0
  37. {ingestr-0.7.8 → ingestr-0.8.2}/docs/index.md +0 -0
  38. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/airtable.md +0 -0
  39. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/bigquery.md +0 -0
  40. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/chess.md +0 -0
  41. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/csv.md +0 -0
  42. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/databricks.md +0 -0
  43. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/duckdb.md +0 -0
  44. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/facebook-ads.md +0 -0
  45. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/gorgias.md +0 -0
  46. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/gsheets.md +0 -0
  47. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/hubspot.md +0 -0
  48. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/kafka.md +0 -0
  49. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/klaviyo.md +0 -0
  50. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/mongodb.md +0 -0
  51. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/mssql.md +0 -0
  52. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/mysql.md +0 -0
  53. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/notion.md +0 -0
  54. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/oracle.md +0 -0
  55. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/postgres.md +0 -0
  56. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/redshift.md +0 -0
  57. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/sap-hana.md +0 -0
  58. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/slack.md +0 -0
  59. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/snowflake.md +0 -0
  60. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/sqlite.md +0 -0
  61. {ingestr-0.7.8 → ingestr-0.8.2}/docs/supported-sources/stripe.md +0 -0
  62. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/.gitignore +0 -0
  63. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/airtable/__init__.py +0 -0
  64. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/chess/__init__.py +0 -0
  65. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/chess/helpers.py +0 -0
  66. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/chess/settings.py +0 -0
  67. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/destinations.py +0 -0
  68. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/facebook_ads/__init__.py +0 -0
  69. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/facebook_ads/exceptions.py +0 -0
  70. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/facebook_ads/helpers.py +0 -0
  71. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/facebook_ads/settings.py +0 -0
  72. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/google_sheets/README.md +0 -0
  73. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/google_sheets/__init__.py +0 -0
  74. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/google_sheets/helpers/__init__.py +0 -0
  75. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/google_sheets/helpers/api_calls.py +0 -0
  76. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/google_sheets/helpers/data_processing.py +0 -0
  77. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/gorgias/__init__.py +0 -0
  78. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/gorgias/helpers.py +0 -0
  79. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/hubspot/__init__.py +0 -0
  80. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/hubspot/helpers.py +0 -0
  81. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/hubspot/settings.py +0 -0
  82. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/kafka/__init__.py +0 -0
  83. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/kafka/helpers.py +0 -0
  84. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/klaviyo/_init_.py +0 -0
  85. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/klaviyo/client.py +0 -0
  86. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/klaviyo/helpers.py +0 -0
  87. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/mongodb/__init__.py +0 -0
  88. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/mongodb/helpers.py +0 -0
  89. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/notion/__init__.py +0 -0
  90. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/notion/helpers/__init__.py +0 -0
  91. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/notion/helpers/client.py +0 -0
  92. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/notion/helpers/database.py +0 -0
  93. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/notion/settings.py +0 -0
  94. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/shopify/exceptions.py +0 -0
  95. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/shopify/settings.py +0 -0
  96. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/slack/__init__.py +0 -0
  97. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/slack/helpers.py +0 -0
  98. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/slack/settings.py +0 -0
  99. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/sql_database/__init__.py +0 -0
  100. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/sql_database/arrow_helpers.py +0 -0
  101. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/sql_database/helpers.py +0 -0
  102. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/sql_database/override.py +0 -0
  103. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/sql_database/schema_types.py +0 -0
  104. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/stripe_analytics/__init__.py +0 -0
  105. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/stripe_analytics/helpers.py +0 -0
  106. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/stripe_analytics/settings.py +0 -0
  107. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/table_definition.py +0 -0
  108. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/telemetry/event.py +0 -0
  109. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/src/testdata/fakebqcredentials.json +0 -0
  110. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/testdata/.gitignore +0 -0
  111. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/testdata/create_replace.csv +0 -0
  112. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/testdata/delete_insert_expected.csv +0 -0
  113. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/testdata/delete_insert_part1.csv +0 -0
  114. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/testdata/delete_insert_part2.csv +0 -0
  115. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/testdata/merge_expected.csv +0 -0
  116. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/testdata/merge_part1.csv +0 -0
  117. {ingestr-0.7.8 → ingestr-0.8.2}/ingestr/testdata/merge_part2.csv +0 -0
  118. {ingestr-0.7.8 → ingestr-0.8.2}/package-lock.json +0 -0
  119. {ingestr-0.7.8 → ingestr-0.8.2}/package.json +0 -0
  120. {ingestr-0.7.8 → ingestr-0.8.2}/requirements-dev.txt +0 -0
  121. {ingestr-0.7.8 → ingestr-0.8.2}/resources/demo.gif +0 -0
  122. {ingestr-0.7.8 → ingestr-0.8.2}/resources/demo.tape +0 -0
  123. {ingestr-0.7.8 → ingestr-0.8.2}/resources/ingestr.svg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ingestr
3
- Version: 0.7.8
3
+ Version: 0.8.2
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
@@ -44,6 +44,7 @@ Requires-Dist: sqlalchemy==1.4.52
44
44
  Requires-Dist: stripe==10.7.0
45
45
  Requires-Dist: tqdm==4.66.2
46
46
  Requires-Dist: typer==0.12.3
47
+ Requires-Dist: types-requests==2.32.0.20240907
47
48
  Description-Content-Type: text/markdown
48
49
 
49
50
  <div align="center">
@@ -178,10 +179,18 @@ Join our Slack community [here](https://join.slack.com/t/bruindatacommunity/shar
178
179
  <tr>
179
180
  <td colspan="3" style='text-align:center;'><strong>Platforms</strong></td>
180
181
  </tr>
182
+ <td>Adjust</td>
183
+ <td>✅</td>
184
+ <td>-</td>
181
185
  <tr>
182
186
  <td>Airtable</td>
183
187
  <td>✅</td>
184
188
  <td>-</td>
189
+ </tr>
190
+ <tr>
191
+ <td>AppsFlyer</td>
192
+ <td>✅</td>
193
+ <td>-</td>
185
194
  </tr>
186
195
  <tr>
187
196
  <td>Chess.com</td>
@@ -130,10 +130,18 @@ Join our Slack community [here](https://join.slack.com/t/bruindatacommunity/shar
130
130
  <tr>
131
131
  <td colspan="3" style='text-align:center;'><strong>Platforms</strong></td>
132
132
  </tr>
133
+ <td>Adjust</td>
134
+ <td>✅</td>
135
+ <td>-</td>
133
136
  <tr>
134
137
  <td>Airtable</td>
135
138
  <td>✅</td>
136
139
  <td>-</td>
140
+ </tr>
141
+ <tr>
142
+ <td>AppsFlyer</td>
143
+ <td>✅</td>
144
+ <td>-</td>
137
145
  </tr>
138
146
  <tr>
139
147
  <td>Chess.com</td>
@@ -84,7 +84,9 @@ export default defineConfig({
84
84
  text: "Platforms",
85
85
  collapsed: false,
86
86
  items: [
87
+ { text: "Adjust", link: "/supported-sources/adjust.md" },
87
88
  { text: "Airtable", link: "/supported-sources/airtable.md" },
89
+ { text: "AppsFlyer", link: "/supported-sources/appsflyer.md" },
88
90
  { text: "Chess.com", link: "/supported-sources/chess.md" },
89
91
  {
90
92
  text: "Facebook Ads",
@@ -0,0 +1,30 @@
1
+ # Adjust
2
+
3
+ [Adjust](https://www.adjust.com/) is a mobile marketing analytics platform that provides solutions for measuring and optimizing campaigns, as well as protecting user data.
4
+
5
+ ingestr supports Adjust as a source.
6
+
7
+ ## URI Format
8
+
9
+ The URI format for Adjust is as follows:
10
+
11
+ ```plaintext
12
+ adjust://?api_key=<api-key-here>
13
+ ```
14
+
15
+ An API token is required to retrieve reports from the Adjust reporting API. please follow the guide to [obtain a API key](https://dev.adjust.com/en/api/rs-api/authentication/).
16
+
17
+ Once you complete the guide, you should have an API key. Let's say your API key is `nr_123`, here's a sample command that will copy the data from Adjust into a duckdb database:
18
+
19
+ ```sh
20
+ ingestr ingest --source-uri 'adjust://?api_key=nr_123' --source-table 'campaigns' --dest-uri duckdb:///adjust.duckdb --dest-table 'adjust.output' --interval-start '2024-09-05' --interval-end '2024-09-08'
21
+ ```
22
+
23
+ The result of this command will be a table in the `adjust.duckdb` database
24
+
25
+ Available Source Table:
26
+ Adjust source allows ingesting the following source into separate tables:
27
+
28
+ -`Campaigns`: Retrieves data for a campaign, showing the app's revenue and network costs over multiple days.
29
+
30
+ --`Creatives`: Retrieves data for a creative assest, detailing the app's revenue and network costs across multiple days
@@ -0,0 +1,28 @@
1
+ # AppsFlyer
2
+
3
+ [AppsFlyer](https://www.appsflyer.com/) is a mobile marketing analytics and attribution platform that helps businesses track, measure, and optimize their app marketing efforts across various channels.
4
+
5
+ ingestr supports AppsFlyer as a source.
6
+
7
+ The URI format for AppsFlyer is as follows:
8
+
9
+ ```plaintext
10
+ appsflyer://?api_key=<api-key>
11
+ ```
12
+
13
+ An API token is required to retrieve reports from the AppsFlyer API. Please follow the guide to [obtain a API key](https://support.appsflyer.com/hc/en-us/articles/360004562377-Managing-AppsFlyer-tokens)
14
+
15
+ Once you complete the guide, you should have an API key. Let's say your API key is ey123, here's a sample command that will copy the data from AppsFlyer into a duckdb database:
16
+
17
+ ingestr ingest --source-uri 'appsflyer://?api_key=ey123' --source-table 'campaigns' --dest-uri duckdb:///appsflyer.duckdb --dest-table 'appsflyer.output' --interval-start '2024-08-01' --interval-end '2024-08-28'
18
+
19
+ The result of this command will be a table in the appsflyer.duckdb database
20
+
21
+ Available Source Table:
22
+ AppsFlyer source allows ingesting the following source into separate tables:
23
+
24
+ -Campaigns: Retrieves data for campaigns, detailing the app's costs, loyal users, total installs, and revenue over multiple days.
25
+
26
+ -Creatives: Retrieves data for a creative asset, including revenue and cost.
27
+
28
+ Use these as `--source-table` parameter in the `ingestr ingest` command.
@@ -32,6 +32,12 @@ The result of this command will be a table in the `shopify.duckdb` database with
32
32
  Shopify source allows ingesting the following sources into separate tables:
33
33
  - `orders`
34
34
  - `customers`
35
+ - `discounts`: Uses the GraphQL API, `discountNodes` query, take a look at the [Shopify docs](https://shopify.dev/docs/api/admin-graphql/2024-07/queries/discountNodes) for more details.
35
36
  - `products`
37
+ - `inventory_items`
38
+ - `transactions`
39
+ - `balance`
40
+ - `events`
41
+ - `price_rules`: this is a deprecated table, please use `discounts` instead.
36
42
 
37
43
  Use these as `--source-table` parameter in the `ingestr ingest` command.
@@ -259,6 +259,7 @@ def ingest(
259
259
  },
260
260
  )
261
261
 
262
+ dlt.config["data_writer.buffer_max_items"] = page_size
262
263
  dlt.config["data_writer.file_max_items"] = loader_file_size
263
264
  dlt.config["extract.workers"] = extract_parallelism
264
265
  dlt.config["extract.max_parallel_items"] = extract_parallelism
@@ -0,0 +1,31 @@
1
+ from typing import Sequence
2
+
3
+ import dlt
4
+ from dlt.sources import DltResource
5
+
6
+ from .helpers import DEFAULT_DIMENSIONS, AdjustAPI
7
+
8
+
9
+ @dlt.source(max_table_nesting=0)
10
+ def adjust_source(
11
+ start_date: str,
12
+ end_date: str,
13
+ api_key: str,
14
+ ) -> Sequence[DltResource]:
15
+ @dlt.resource(write_disposition="merge", merge_key="day")
16
+ def campaigns():
17
+ adjust_api = AdjustAPI(api_key=api_key)
18
+ yield from adjust_api.fetch_report_data(
19
+ start_date=start_date,
20
+ end_date=end_date,
21
+ )
22
+
23
+ @dlt.resource(write_disposition="merge", merge_key="day")
24
+ def creatives():
25
+ dimensions = DEFAULT_DIMENSIONS + ["adgroup", "creative"]
26
+ adjust_api = AdjustAPI(api_key=api_key)
27
+ yield from adjust_api.fetch_report_data(
28
+ start_date=start_date, end_date=end_date, dimensions=dimensions
29
+ )
30
+
31
+ return campaigns, creatives
@@ -0,0 +1,82 @@
1
+ import requests
2
+ from dlt.sources.helpers.requests import Client
3
+ from requests.exceptions import HTTPError
4
+
5
+ DEFAULT_DIMENSIONS = ["campaign", "day", "app", "store_type", "channel", "country"]
6
+
7
+ DEFAULT_METRICS = [
8
+ "network_cost",
9
+ "all_revenue_total_d0",
10
+ "ad_revenue_total_d0",
11
+ "revenue_total_d0",
12
+ "all_revenue_total_d1",
13
+ "ad_revenue_total_d1",
14
+ "revenue_total_d1",
15
+ "all_revenue_total_d3",
16
+ "ad_revenue_total_d3",
17
+ "revenue_total_d3",
18
+ "all_revenue_total_d7",
19
+ "ad_revenue_total_d7",
20
+ "revenue_total_d7",
21
+ "all_revenue_total_d14",
22
+ "ad_revenue_total_d14",
23
+ "revenue_total_d14",
24
+ "all_revenue_total_d21",
25
+ ]
26
+
27
+
28
+ class AdjustAPI:
29
+ def __init__(self, api_key):
30
+ self.api_key = api_key
31
+ self.uri = "https://automate.adjust.com/reports-service/report"
32
+
33
+ def fetch_report_data(
34
+ self,
35
+ start_date,
36
+ end_date,
37
+ dimensions=DEFAULT_DIMENSIONS,
38
+ metrics=DEFAULT_METRICS,
39
+ utc_offset="+00:00",
40
+ ad_spend_mode="network",
41
+ attribution_source="first",
42
+ attribution_type="all",
43
+ cohort_maturity="immature",
44
+ reattributed="all",
45
+ sandbox="false",
46
+ ):
47
+ headers = {"Authorization": f"Bearer {self.api_key}"}
48
+ comma_separated_dimensions = ",".join(dimensions)
49
+ comma_separated_metrics = ",".join(metrics)
50
+ params = {
51
+ "date_period": f"{start_date}:{end_date}",
52
+ "dimensions": comma_separated_dimensions,
53
+ "metrics": comma_separated_metrics,
54
+ "utc_offset": utc_offset,
55
+ "ad_spend_mode": ad_spend_mode,
56
+ "attribution_source": attribution_source,
57
+ "attribution_type": attribution_type,
58
+ "cohort_maturity": cohort_maturity,
59
+ "reattributed": reattributed,
60
+ "sandbox": sandbox,
61
+ }
62
+
63
+ def retry_on_limit(
64
+ response: requests.Response, exception: BaseException
65
+ ) -> bool:
66
+ return response.status_code == 429
67
+
68
+ request_client = Client(
69
+ request_timeout=8.0,
70
+ raise_for_status=False,
71
+ retry_condition=retry_on_limit,
72
+ request_max_attempts=12,
73
+ request_backoff_factor=2,
74
+ ).session
75
+
76
+ response = request_client.get(self.uri, headers=headers, params=params)
77
+ if response.status_code == 200:
78
+ result = response.json()
79
+ items = result.get("rows", [])
80
+ yield items
81
+ else:
82
+ raise HTTPError(f"Request failed with status code: {response.status_code}")
@@ -0,0 +1,24 @@
1
+ from typing import Iterable
2
+
3
+ import dlt
4
+ from dlt.common.typing import TDataItem
5
+ from dlt.sources import DltResource
6
+
7
+ from ingestr.src.appsflyer.client import AppsflyerClient
8
+
9
+
10
+ @dlt.source(max_table_nesting=0)
11
+ def appsflyer_source(
12
+ api_key: str, start_date: str, end_date: str
13
+ ) -> Iterable[DltResource]:
14
+ client = AppsflyerClient(api_key)
15
+
16
+ @dlt.resource(write_disposition="merge", merge_key="install_time")
17
+ def campaigns() -> Iterable[TDataItem]:
18
+ yield from client.fetch_campaigns(start_date, end_date)
19
+
20
+ @dlt.resource(write_disposition="merge", merge_key="install_time")
21
+ def creatives() -> Iterable[TDataItem]:
22
+ yield from client.fetch_creatives(start_date, end_date)
23
+
24
+ return campaigns, creatives
@@ -0,0 +1,106 @@
1
+ from typing import Optional
2
+
3
+ import requests
4
+ from dlt.sources.helpers.requests import Client
5
+ from requests.exceptions import HTTPError
6
+
7
+ DEFAULT_GROUPING = ["c", "geo", "app_id", "install_time"]
8
+ DEFAULT_KPIS = [
9
+ "impressions",
10
+ "clicks",
11
+ "installs",
12
+ "cost",
13
+ "revenue",
14
+ "average_ecpi",
15
+ "loyal_users",
16
+ "uninstalls",
17
+ "roi",
18
+ ]
19
+
20
+
21
+ class AppsflyerClient:
22
+ def __init__(self, api_key: str):
23
+ self.api_key = api_key
24
+ self.uri = "https://hq1.appsflyer.com/api/master-agg-data/v4/app/all"
25
+
26
+ def __get_headers(self):
27
+ return {
28
+ "Authorization": f"{self.api_key}",
29
+ "accept": "text/json",
30
+ }
31
+
32
+ def _fetch_data(
33
+ self,
34
+ from_date: str,
35
+ to_date: str,
36
+ maximum_rows=1000000,
37
+ dimensions=DEFAULT_GROUPING,
38
+ metrics=DEFAULT_KPIS,
39
+ ):
40
+ params = {
41
+ "from": from_date,
42
+ "to": to_date,
43
+ "groupings": ",".join(dimensions),
44
+ "kpis": ",".join(metrics),
45
+ "format": "json",
46
+ "maximum_rows": maximum_rows,
47
+ }
48
+
49
+ def retry_on_limit(
50
+ response: Optional[requests.Response], exception: Optional[BaseException]
51
+ ) -> bool:
52
+ return (
53
+ isinstance(response, requests.Response) and response.status_code == 429
54
+ )
55
+
56
+ request_client = Client(
57
+ request_timeout=10.0,
58
+ raise_for_status=False,
59
+ retry_condition=retry_on_limit,
60
+ request_max_attempts=12,
61
+ request_backoff_factor=2,
62
+ ).session
63
+
64
+ try:
65
+ response = request_client.get(
66
+ url=self.uri, headers=self.__get_headers(), params=params
67
+ )
68
+
69
+ if response.status_code == 200:
70
+ result = response.json()
71
+ yield result
72
+ else:
73
+ raise HTTPError(
74
+ f"Request failed with status code: {response.status_code}"
75
+ )
76
+
77
+ except requests.RequestException as e:
78
+ raise HTTPError(f"Request failed: {e}")
79
+
80
+ def fetch_campaigns(
81
+ self,
82
+ start_date: str,
83
+ end_date: str,
84
+ ):
85
+ metrics = DEFAULT_KPIS + [
86
+ "cohort_day_1_revenue_per_user",
87
+ "cohort_day_1_total_revenue_per_user",
88
+ "cohort_day_3_revenue_per_user",
89
+ "cohort_day_3_total_revenue_per_user",
90
+ "cohort_day_7_total_revenue_per_user",
91
+ "cohort_day_7_revenue_per_user",
92
+ "cohort_day_14_total_revenue_per_user",
93
+ "cohort_day_14_revenue_per_user",
94
+ "cohort_day_21_total_revenue_per_user",
95
+ "cohort_day_21_revenue_per_user",
96
+ "retention_day_7",
97
+ ]
98
+ return self._fetch_data(start_date, end_date, metrics=metrics)
99
+
100
+ def fetch_creatives(
101
+ self,
102
+ start_date: str,
103
+ end_date: str,
104
+ ):
105
+ dimensions = DEFAULT_GROUPING + ["af_adset_id", "af_adset", "af_ad_id"]
106
+ return self._fetch_data(start_date, end_date, dimensions=dimensions)
@@ -15,7 +15,9 @@ from ingestr.src.destinations import (
15
15
  SynapseDestination,
16
16
  )
17
17
  from ingestr.src.sources import (
18
+ AdjustSource,
18
19
  AirtableSource,
20
+ AppsflyerSource,
19
21
  ChessSource,
20
22
  FacebookAdsSource,
21
23
  GoogleSheetsSource,
@@ -124,8 +126,12 @@ class SourceDestinationFactory:
124
126
  return AirtableSource()
125
127
  elif self.source_scheme == "klaviyo":
126
128
  return KlaviyoSource()
129
+ elif self.source_scheme == "appsflyer":
130
+ return AppsflyerSource()
127
131
  elif self.source_scheme == "kafka":
128
132
  return KafkaSource()
133
+ elif self.source_scheme == "adjust":
134
+ return AdjustSource()
129
135
  else:
130
136
  raise ValueError(f"Unsupported source scheme: {self.source_scheme}")
131
137