ingestr 0.13.2__py3-none-any.whl → 0.14.104__py3-none-any.whl

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.
Files changed (146) hide show
  1. ingestr/conftest.py +72 -0
  2. ingestr/main.py +134 -87
  3. ingestr/src/adjust/__init__.py +4 -4
  4. ingestr/src/adjust/adjust_helpers.py +7 -3
  5. ingestr/src/airtable/__init__.py +3 -2
  6. ingestr/src/allium/__init__.py +128 -0
  7. ingestr/src/anthropic/__init__.py +277 -0
  8. ingestr/src/anthropic/helpers.py +525 -0
  9. ingestr/src/applovin/__init__.py +262 -0
  10. ingestr/src/applovin_max/__init__.py +117 -0
  11. ingestr/src/appsflyer/__init__.py +325 -0
  12. ingestr/src/appsflyer/client.py +49 -45
  13. ingestr/src/appstore/__init__.py +1 -0
  14. ingestr/src/arrow/__init__.py +9 -1
  15. ingestr/src/asana_source/__init__.py +1 -1
  16. ingestr/src/attio/__init__.py +102 -0
  17. ingestr/src/attio/helpers.py +65 -0
  18. ingestr/src/blob.py +38 -11
  19. ingestr/src/buildinfo.py +1 -0
  20. ingestr/src/chess/__init__.py +1 -1
  21. ingestr/src/clickup/__init__.py +85 -0
  22. ingestr/src/clickup/helpers.py +47 -0
  23. ingestr/src/collector/spinner.py +43 -0
  24. ingestr/src/couchbase_source/__init__.py +118 -0
  25. ingestr/src/couchbase_source/helpers.py +135 -0
  26. ingestr/src/cursor/__init__.py +83 -0
  27. ingestr/src/cursor/helpers.py +188 -0
  28. ingestr/src/destinations.py +520 -33
  29. ingestr/src/docebo/__init__.py +589 -0
  30. ingestr/src/docebo/client.py +435 -0
  31. ingestr/src/docebo/helpers.py +97 -0
  32. ingestr/src/elasticsearch/__init__.py +80 -0
  33. ingestr/src/elasticsearch/helpers.py +138 -0
  34. ingestr/src/errors.py +8 -0
  35. ingestr/src/facebook_ads/__init__.py +47 -28
  36. ingestr/src/facebook_ads/helpers.py +59 -37
  37. ingestr/src/facebook_ads/settings.py +2 -0
  38. ingestr/src/facebook_ads/utils.py +39 -0
  39. ingestr/src/factory.py +116 -2
  40. ingestr/src/filesystem/__init__.py +8 -3
  41. ingestr/src/filters.py +46 -3
  42. ingestr/src/fluxx/__init__.py +9906 -0
  43. ingestr/src/fluxx/helpers.py +209 -0
  44. ingestr/src/frankfurter/__init__.py +157 -0
  45. ingestr/src/frankfurter/helpers.py +48 -0
  46. ingestr/src/freshdesk/__init__.py +89 -0
  47. ingestr/src/freshdesk/freshdesk_client.py +137 -0
  48. ingestr/src/freshdesk/settings.py +9 -0
  49. ingestr/src/fundraiseup/__init__.py +95 -0
  50. ingestr/src/fundraiseup/client.py +81 -0
  51. ingestr/src/github/__init__.py +41 -6
  52. ingestr/src/github/helpers.py +5 -5
  53. ingestr/src/google_analytics/__init__.py +22 -4
  54. ingestr/src/google_analytics/helpers.py +124 -6
  55. ingestr/src/google_sheets/__init__.py +4 -4
  56. ingestr/src/google_sheets/helpers/data_processing.py +2 -2
  57. ingestr/src/hostaway/__init__.py +302 -0
  58. ingestr/src/hostaway/client.py +288 -0
  59. ingestr/src/http/__init__.py +35 -0
  60. ingestr/src/http/readers.py +114 -0
  61. ingestr/src/http_client.py +24 -0
  62. ingestr/src/hubspot/__init__.py +66 -23
  63. ingestr/src/hubspot/helpers.py +52 -22
  64. ingestr/src/hubspot/settings.py +14 -7
  65. ingestr/src/influxdb/__init__.py +46 -0
  66. ingestr/src/influxdb/client.py +34 -0
  67. ingestr/src/intercom/__init__.py +142 -0
  68. ingestr/src/intercom/helpers.py +674 -0
  69. ingestr/src/intercom/settings.py +279 -0
  70. ingestr/src/isoc_pulse/__init__.py +159 -0
  71. ingestr/src/jira_source/__init__.py +340 -0
  72. ingestr/src/jira_source/helpers.py +439 -0
  73. ingestr/src/jira_source/settings.py +170 -0
  74. ingestr/src/kafka/__init__.py +4 -1
  75. ingestr/src/kinesis/__init__.py +139 -0
  76. ingestr/src/kinesis/helpers.py +82 -0
  77. ingestr/src/klaviyo/{_init_.py → __init__.py} +5 -6
  78. ingestr/src/linear/__init__.py +634 -0
  79. ingestr/src/linear/helpers.py +111 -0
  80. ingestr/src/linkedin_ads/helpers.py +0 -1
  81. ingestr/src/loader.py +69 -0
  82. ingestr/src/mailchimp/__init__.py +126 -0
  83. ingestr/src/mailchimp/helpers.py +226 -0
  84. ingestr/src/mailchimp/settings.py +164 -0
  85. ingestr/src/masking.py +344 -0
  86. ingestr/src/mixpanel/__init__.py +62 -0
  87. ingestr/src/mixpanel/client.py +99 -0
  88. ingestr/src/monday/__init__.py +246 -0
  89. ingestr/src/monday/helpers.py +392 -0
  90. ingestr/src/monday/settings.py +328 -0
  91. ingestr/src/mongodb/__init__.py +72 -8
  92. ingestr/src/mongodb/helpers.py +915 -38
  93. ingestr/src/partition.py +32 -0
  94. ingestr/src/personio/__init__.py +331 -0
  95. ingestr/src/personio/helpers.py +86 -0
  96. ingestr/src/phantombuster/__init__.py +65 -0
  97. ingestr/src/phantombuster/client.py +87 -0
  98. ingestr/src/pinterest/__init__.py +82 -0
  99. ingestr/src/pipedrive/__init__.py +198 -0
  100. ingestr/src/pipedrive/helpers/__init__.py +23 -0
  101. ingestr/src/pipedrive/helpers/custom_fields_munger.py +102 -0
  102. ingestr/src/pipedrive/helpers/pages.py +115 -0
  103. ingestr/src/pipedrive/settings.py +27 -0
  104. ingestr/src/pipedrive/typing.py +3 -0
  105. ingestr/src/plusvibeai/__init__.py +335 -0
  106. ingestr/src/plusvibeai/helpers.py +544 -0
  107. ingestr/src/plusvibeai/settings.py +252 -0
  108. ingestr/src/quickbooks/__init__.py +117 -0
  109. ingestr/src/resource.py +40 -0
  110. ingestr/src/revenuecat/__init__.py +83 -0
  111. ingestr/src/revenuecat/helpers.py +237 -0
  112. ingestr/src/salesforce/__init__.py +156 -0
  113. ingestr/src/salesforce/helpers.py +64 -0
  114. ingestr/src/shopify/__init__.py +1 -17
  115. ingestr/src/smartsheets/__init__.py +82 -0
  116. ingestr/src/snapchat_ads/__init__.py +489 -0
  117. ingestr/src/snapchat_ads/client.py +72 -0
  118. ingestr/src/snapchat_ads/helpers.py +535 -0
  119. ingestr/src/socrata_source/__init__.py +83 -0
  120. ingestr/src/socrata_source/helpers.py +85 -0
  121. ingestr/src/socrata_source/settings.py +8 -0
  122. ingestr/src/solidgate/__init__.py +219 -0
  123. ingestr/src/solidgate/helpers.py +154 -0
  124. ingestr/src/sources.py +3132 -212
  125. ingestr/src/stripe_analytics/__init__.py +49 -21
  126. ingestr/src/stripe_analytics/helpers.py +286 -1
  127. ingestr/src/stripe_analytics/settings.py +62 -10
  128. ingestr/src/telemetry/event.py +10 -9
  129. ingestr/src/tiktok_ads/__init__.py +12 -6
  130. ingestr/src/tiktok_ads/tiktok_helpers.py +0 -1
  131. ingestr/src/trustpilot/__init__.py +48 -0
  132. ingestr/src/trustpilot/client.py +48 -0
  133. ingestr/src/version.py +6 -1
  134. ingestr/src/wise/__init__.py +68 -0
  135. ingestr/src/wise/client.py +63 -0
  136. ingestr/src/zoom/__init__.py +99 -0
  137. ingestr/src/zoom/helpers.py +102 -0
  138. ingestr/tests/unit/test_smartsheets.py +133 -0
  139. ingestr-0.14.104.dist-info/METADATA +563 -0
  140. ingestr-0.14.104.dist-info/RECORD +203 -0
  141. ingestr/src/appsflyer/_init_.py +0 -24
  142. ingestr-0.13.2.dist-info/METADATA +0 -302
  143. ingestr-0.13.2.dist-info/RECORD +0 -107
  144. {ingestr-0.13.2.dist-info → ingestr-0.14.104.dist-info}/WHEEL +0 -0
  145. {ingestr-0.13.2.dist-info → ingestr-0.14.104.dist-info}/entry_points.txt +0 -0
  146. {ingestr-0.13.2.dist-info → ingestr-0.14.104.dist-info}/licenses/LICENSE.md +0 -0
@@ -0,0 +1,252 @@
1
+ """PlusVibeAI source settings and constants"""
2
+
3
+ # Default start date for PlusVibeAI API requests
4
+ DEFAULT_START_DATE = "2020-01-01"
5
+
6
+ # PlusVibeAI API request timeout in seconds
7
+ REQUEST_TIMEOUT = 300
8
+
9
+ # Default page size for paginated requests
10
+ DEFAULT_PAGE_SIZE = 100
11
+
12
+ # Maximum page size (adjust based on API limits)
13
+ MAX_PAGE_SIZE = 1000
14
+
15
+ # Base API path for PlusVibeAI
16
+ API_BASE_PATH = "/api/v1"
17
+
18
+ # Campaign fields to retrieve from PlusVibeAI API
19
+ CAMPAIGN_FIELDS = (
20
+ # Basic Information
21
+ "id",
22
+ "camp_name",
23
+ "parent_camp_id",
24
+ "campaign_type",
25
+ "organization_id",
26
+ "workspace_id",
27
+ "status",
28
+ # Timestamps
29
+ "created_at",
30
+ "modified_at",
31
+ "last_lead_sent",
32
+ "last_paused_at_bounced",
33
+ # Campaign Configuration
34
+ "tags",
35
+ "template_id",
36
+ "email_accounts",
37
+ "daily_limit",
38
+ "interval_limit_in_min",
39
+ "send_priority",
40
+ "send_as_txt",
41
+ # Tracking & Settings
42
+ "is_emailopened_tracking",
43
+ "is_unsubscribed_link",
44
+ "exclude_ooo",
45
+ "is_acc_based_sending",
46
+ "send_risky_email",
47
+ "unsub_blocklist",
48
+ "other_email_acc",
49
+ "is_esp_match",
50
+ "stop_on_lead_replied",
51
+ # Bounce Settings
52
+ "is_pause_on_bouncerate",
53
+ "bounce_rate_limit",
54
+ "is_paused_at_bounced",
55
+ # Schedule
56
+ "schedule",
57
+ "first_wait_time",
58
+ "camp_st_date",
59
+ "camp_end_date",
60
+ # Events & Sequences
61
+ "events",
62
+ "sequences",
63
+ "sequence_steps",
64
+ "camp_emails",
65
+ # Lead Statistics
66
+ "lead_count",
67
+ "completed_lead_count",
68
+ "lead_contacted_count",
69
+ # Email Performance Metrics
70
+ "sent_count",
71
+ "opened_count",
72
+ "unique_opened_count",
73
+ "replied_count",
74
+ "bounced_count",
75
+ "unsubscribed_count",
76
+ # Reply Classification
77
+ "positive_reply_count",
78
+ "negative_reply_count",
79
+ "neutral_reply_count",
80
+ # Daily & Business Metrics
81
+ "email_sent_today",
82
+ "opportunity_val",
83
+ "open_rate",
84
+ "replied_rate",
85
+ # Custom Data
86
+ "custom_fields",
87
+ )
88
+
89
+ # Lead fields to retrieve from PlusVibeAI API
90
+ LEAD_FIELDS = (
91
+ # Basic Information
92
+ "_id",
93
+ "organization_id",
94
+ "campaign_id",
95
+ "workspace_id",
96
+ # Lead Status & Progress
97
+ "is_completed",
98
+ "current_step",
99
+ "status",
100
+ "label",
101
+ # Email Account Info
102
+ "email_account_id",
103
+ "email_acc_name",
104
+ # Campaign Info
105
+ "camp_name",
106
+ # Timestamps
107
+ "created_at",
108
+ "modified_at",
109
+ "last_sent_at",
110
+ # Email Engagement Metrics
111
+ "sent_step",
112
+ "replied_count",
113
+ "opened_count",
114
+ # Email Verification
115
+ "is_mx",
116
+ "mx",
117
+ # Contact Information
118
+ "email",
119
+ "first_name",
120
+ "last_name",
121
+ "phone_number",
122
+ # Address Information
123
+ "address_line",
124
+ "city",
125
+ "state",
126
+ "country",
127
+ "country_code",
128
+ # Professional Information
129
+ "job_title",
130
+ "department",
131
+ "company_name",
132
+ "company_website",
133
+ "industry",
134
+ # Social Media
135
+ "linkedin_person_url",
136
+ "linkedin_company_url",
137
+ # Workflow
138
+ "total_steps",
139
+ # Bounce Information
140
+ "bounce_msg",
141
+ )
142
+
143
+ # Email Account fields to retrieve from PlusVibeAI API
144
+ EMAIL_ACCOUNT_FIELDS = (
145
+ # Basic Information
146
+ "_id",
147
+ "email",
148
+ "status",
149
+ "warmup_status",
150
+ # Timestamps
151
+ "timestamp_created",
152
+ "timestamp_updated",
153
+ # Payload - nested object containing all configuration
154
+ "payload",
155
+ # Payload sub-fields (for reference, stored in payload object):
156
+ # - name (first_name, last_name)
157
+ # - warmup (limit, warmup_custom_words, warmup_signature, advanced, increment, reply_rate)
158
+ # - imap_host, imap_port
159
+ # - smtp_host, smtp_port
160
+ # - daily_limit, sending_gap
161
+ # - reply_to, custom_domain, signature
162
+ # - tags, cmps
163
+ # - analytics (health_scores, reply_rates, daily_counters)
164
+ )
165
+
166
+ # Email fields to retrieve from PlusVibeAI API
167
+ EMAIL_FIELDS = (
168
+ # Basic Information
169
+ "id",
170
+ "message_id",
171
+ "is_unread",
172
+ # Lead Information
173
+ "lead",
174
+ "lead_id",
175
+ "campaign_id",
176
+ # From Address
177
+ "from_address_email",
178
+ "from_address_json",
179
+ # Subject & Content
180
+ "subject",
181
+ "content_preview",
182
+ "body",
183
+ # Headers & Metadata
184
+ "headers",
185
+ "label",
186
+ "thread_id",
187
+ "eaccount",
188
+ # To/CC/BCC Addresses
189
+ "to_address_email_list",
190
+ "to_address_json",
191
+ "cc_address_email_list",
192
+ "cc_address_json",
193
+ "bcc_address_email_list",
194
+ # Timestamps
195
+ "timestamp_created",
196
+ "source_modified_at",
197
+ )
198
+
199
+ # Blocklist fields to retrieve from PlusVibeAI API
200
+ BLOCKLIST_FIELDS = (
201
+ # Basic Information
202
+ "_id",
203
+ "workspace_id",
204
+ "value",
205
+ "created_by_label",
206
+ # Timestamps
207
+ "created_at",
208
+ )
209
+
210
+ # Webhook fields to retrieve from PlusVibeAI API
211
+ WEBHOOK_FIELDS = (
212
+ # Basic Information
213
+ "_id",
214
+ "workspace_id",
215
+ "org_id",
216
+ "url",
217
+ "name",
218
+ "secret",
219
+ # Configuration
220
+ "camp_ids",
221
+ "evt_types",
222
+ "status",
223
+ "integration_type",
224
+ # Settings
225
+ "ignore_ooo",
226
+ "ignore_automatic",
227
+ # Timestamps
228
+ "created_at",
229
+ "modified_at",
230
+ "last_run",
231
+ # Response Data
232
+ "last_resp",
233
+ "last_recv_resp",
234
+ # User Information
235
+ "created_by",
236
+ "modified_by",
237
+ )
238
+
239
+ # Tag fields to retrieve from PlusVibeAI API
240
+ TAG_FIELDS = (
241
+ # Basic Information
242
+ "_id",
243
+ "workspace_id",
244
+ "org_id",
245
+ "name",
246
+ "color",
247
+ "description",
248
+ "status",
249
+ # Timestamps
250
+ "created_at",
251
+ "modified_at",
252
+ )
@@ -0,0 +1,117 @@
1
+ """QuickBooks source built on top of python-quickbooks."""
2
+
3
+ from typing import Iterable, Iterator, List, Optional
4
+
5
+ import dlt
6
+ import pendulum
7
+ from dlt.common.time import ensure_pendulum_datetime
8
+ from dlt.common.typing import TDataItem
9
+ from dlt.sources import DltResource
10
+ from intuitlib.client import AuthClient # type: ignore
11
+
12
+ from quickbooks import QuickBooks # type: ignore
13
+
14
+
15
+ @dlt.source(name="quickbooks", max_table_nesting=0)
16
+ def quickbooks_source(
17
+ company_id: str,
18
+ start_date: pendulum.DateTime,
19
+ object: str,
20
+ end_date: pendulum.DateTime | None,
21
+ client_id: str,
22
+ client_secret: str,
23
+ refresh_token: str,
24
+ environment: str = "production",
25
+ minor_version: Optional[str] = None,
26
+ ) -> Iterable[DltResource]:
27
+ """Create dlt resources for QuickBooks objects.
28
+
29
+ Parameters
30
+ ----------
31
+ company_id: str
32
+ QuickBooks company id (realm id).
33
+ client_id: str
34
+ OAuth client id.
35
+ client_secret: str
36
+ OAuth client secret.
37
+ refresh_token: str
38
+ OAuth refresh token.
39
+ access_token: Optional[str]
40
+ Optional access token. If not provided the library will refresh using the
41
+ provided refresh token.
42
+ environment: str
43
+ Either ``"production"`` or ``"sandbox"``.
44
+ minor_version: Optional[int]
45
+ QuickBooks API minor version if needed.
46
+ """
47
+
48
+ auth_client = AuthClient(
49
+ client_id=client_id,
50
+ client_secret=client_secret,
51
+ environment=environment,
52
+ # redirect_uri is not used since we authenticate using refresh token which skips the step of redirect callback.
53
+ # as redirect_uri is required param, we are passing empty string.
54
+ redirect_uri="",
55
+ )
56
+
57
+ # https://help.developer.intuit.com/s/article/Validity-of-Refresh-Token
58
+ client = QuickBooks(
59
+ auth_client=auth_client,
60
+ refresh_token=refresh_token,
61
+ company_id=company_id,
62
+ minorversion=minor_version,
63
+ )
64
+
65
+ def fetch_object(
66
+ obj_name: str,
67
+ updated_at: dlt.sources.incremental[str] = dlt.sources.incremental(
68
+ "lastupdatedtime",
69
+ initial_value=start_date, # type: ignore
70
+ end_value=end_date, # type: ignore
71
+ range_start="closed",
72
+ range_end="closed",
73
+ allow_external_schedulers=True,
74
+ ),
75
+ ) -> Iterator[List[TDataItem]]:
76
+ start_pos = 1
77
+
78
+ end_dt = updated_at.end_value or pendulum.now(tz="UTC")
79
+ start_dt = ensure_pendulum_datetime(str(updated_at.last_value)).in_tz("UTC")
80
+
81
+ start_str = start_dt.isoformat()
82
+ end_str = end_dt.isoformat()
83
+
84
+ where_clause = f"WHERE MetaData.LastUpdatedTime >= '{start_str}' AND MetaData.LastUpdatedTime < '{end_str}'"
85
+ while True:
86
+ query = (
87
+ f"SELECT * FROM {obj_name} {where_clause} "
88
+ f"ORDERBY MetaData.LastUpdatedTime ASC STARTPOSITION {start_pos} MAXRESULTS 1000"
89
+ )
90
+
91
+ result = client.query(query)
92
+
93
+ items = result.get("QueryResponse", {}).get(obj_name.capitalize(), [])
94
+ if not items:
95
+ break
96
+
97
+ for item in items:
98
+ if item.get("MetaData") and item["MetaData"].get("LastUpdatedTime"):
99
+ item["lastupdatedtime"] = ensure_pendulum_datetime(
100
+ item["MetaData"]["LastUpdatedTime"]
101
+ )
102
+ item["id"] = item["Id"]
103
+ del item["Id"]
104
+
105
+ yield item
106
+
107
+ if len(items) < 1000:
108
+ break
109
+
110
+ start_pos += 1000
111
+
112
+ yield dlt.resource(
113
+ fetch_object,
114
+ name=object.lower(),
115
+ write_disposition="merge",
116
+ primary_key="id",
117
+ )(object)
@@ -0,0 +1,40 @@
1
+ from typing import Callable
2
+
3
+ from dlt.sources import DltResource, DltSource
4
+
5
+
6
+ def for_each(
7
+ source: DltSource | DltResource, ex: Callable[[DltResource], None | DltResource]
8
+ ):
9
+ """
10
+ Apply a function to each resource in a source.
11
+ """
12
+ if hasattr(source, "selected_resources") and source.selected_resources:
13
+ resource_names = list(source.selected_resources.keys())
14
+ for res in resource_names:
15
+ ex(source.resources[res]) # type: ignore[union-attr]
16
+ else:
17
+ ex(source) # type: ignore[arg-type]
18
+
19
+
20
+ class TypeHintMap:
21
+ def __init__(self):
22
+ self.handled_typehints = False
23
+
24
+ def type_hint_map(self, item):
25
+ if self.handled_typehints:
26
+ return item
27
+
28
+ array_cols = []
29
+ for col in item:
30
+ if isinstance(item[col], (list, tuple)):
31
+ array_cols.append(col)
32
+ if array_cols:
33
+ import dlt
34
+
35
+ source = dlt.current.source()
36
+ columns = [{"name": col, "data_type": "json"} for col in array_cols]
37
+ for_each(source, lambda x: x.apply_hints(columns=columns))
38
+
39
+ self.handled_typehints = True
40
+ return item
@@ -0,0 +1,83 @@
1
+ from typing import Any, Dict, Iterable, Iterator
2
+
3
+ import aiohttp
4
+ import dlt
5
+
6
+ from .helpers import (
7
+ _make_request,
8
+ _paginate,
9
+ convert_timestamps_to_iso,
10
+ create_project_resource,
11
+ process_customer_with_nested_resources_async,
12
+ )
13
+
14
+
15
+ @dlt.source(name="revenuecat", max_table_nesting=0)
16
+ def revenuecat_source(
17
+ api_key: str,
18
+ project_id: str = None,
19
+ ) -> Iterable[dlt.sources.DltResource]:
20
+ """
21
+ RevenueCat source for extracting data from RevenueCat API v2.
22
+
23
+ Args:
24
+ api_key: RevenueCat API v2 secret key with Bearer token format
25
+ project_id: RevenueCat project ID (required for customers, products, entitlements, offerings, subscriptions, purchases)
26
+
27
+ Returns:
28
+ Iterable of DLT resources for customers, products, entitlements, offerings, purchases, subscriptions, and projects
29
+ """
30
+
31
+ @dlt.resource(name="projects", primary_key="id", write_disposition="merge")
32
+ def projects() -> Iterator[Dict[str, Any]]:
33
+ """Get list of projects."""
34
+ # Get projects list
35
+ data = _make_request(api_key, "/projects")
36
+ if "items" in data:
37
+ for project in data["items"]:
38
+ project = convert_timestamps_to_iso(project, ["created_at"])
39
+ yield project
40
+
41
+ @dlt.resource(
42
+ name="customer_ids",
43
+ write_disposition="replace",
44
+ selected=False,
45
+ parallelized=True,
46
+ )
47
+ def customer_ids():
48
+ if project_id is None:
49
+ raise ValueError("project_id is required for customers resource")
50
+
51
+ yield _paginate(api_key, f"/projects/{project_id}/customers")
52
+
53
+ @dlt.transformer(
54
+ data_from=customer_ids, write_disposition="replace", parallelized=True
55
+ )
56
+ async def customers(customers) -> Iterator[Dict[str, Any]]:
57
+ async with aiohttp.ClientSession() as session:
58
+ for customer in customers:
59
+ yield await process_customer_with_nested_resources_async(
60
+ session, api_key, project_id, customer
61
+ )
62
+
63
+ # Create project-dependent resources dynamically
64
+ project_resources = []
65
+ resource_names = ["products", "entitlements", "offerings"]
66
+
67
+ for resource_name in resource_names:
68
+
69
+ @dlt.resource(name=resource_name, primary_key="id", write_disposition="merge")
70
+ def create_resource(resource_name=resource_name) -> Iterator[Dict[str, Any]]:
71
+ """Get list of project resource."""
72
+ yield from create_project_resource(resource_name, api_key, project_id)
73
+
74
+ # Set the function name for better identification
75
+ create_resource.__name__ = resource_name
76
+ project_resources.append(create_resource)
77
+
78
+ return [
79
+ projects,
80
+ customer_ids,
81
+ customers,
82
+ *project_resources,
83
+ ]