omniload 0.0.0.dev0__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 (218) hide show
  1. omniload/conftest.py +72 -0
  2. omniload/main.py +810 -0
  3. omniload/src/.gitignore +10 -0
  4. omniload/src/adjust/__init__.py +108 -0
  5. omniload/src/adjust/adjust_helpers.py +122 -0
  6. omniload/src/airtable/__init__.py +84 -0
  7. omniload/src/allium/__init__.py +128 -0
  8. omniload/src/anthropic/__init__.py +277 -0
  9. omniload/src/anthropic/helpers.py +525 -0
  10. omniload/src/applovin/__init__.py +316 -0
  11. omniload/src/applovin_max/__init__.py +117 -0
  12. omniload/src/appsflyer/__init__.py +325 -0
  13. omniload/src/appsflyer/client.py +110 -0
  14. omniload/src/appstore/__init__.py +142 -0
  15. omniload/src/appstore/client.py +126 -0
  16. omniload/src/appstore/errors.py +15 -0
  17. omniload/src/appstore/models.py +117 -0
  18. omniload/src/appstore/resources.py +179 -0
  19. omniload/src/arrow/__init__.py +81 -0
  20. omniload/src/asana_source/__init__.py +281 -0
  21. omniload/src/asana_source/helpers.py +30 -0
  22. omniload/src/asana_source/settings.py +158 -0
  23. omniload/src/attio/__init__.py +102 -0
  24. omniload/src/attio/helpers.py +65 -0
  25. omniload/src/blob.py +95 -0
  26. omniload/src/bruin/__init__.py +76 -0
  27. omniload/src/chess/__init__.py +180 -0
  28. omniload/src/chess/helpers.py +35 -0
  29. omniload/src/chess/settings.py +18 -0
  30. omniload/src/clickup/__init__.py +85 -0
  31. omniload/src/clickup/helpers.py +47 -0
  32. omniload/src/collector/spinner.py +43 -0
  33. omniload/src/couchbase_source/__init__.py +118 -0
  34. omniload/src/couchbase_source/helpers.py +135 -0
  35. omniload/src/cursor/__init__.py +83 -0
  36. omniload/src/cursor/helpers.py +188 -0
  37. omniload/src/customer_io/__init__.py +486 -0
  38. omniload/src/customer_io/helpers.py +530 -0
  39. omniload/src/destinations.py +982 -0
  40. omniload/src/docebo/__init__.py +589 -0
  41. omniload/src/docebo/client.py +435 -0
  42. omniload/src/docebo/helpers.py +97 -0
  43. omniload/src/dune/__init__.py +104 -0
  44. omniload/src/dune/helpers.py +108 -0
  45. omniload/src/dynamodb/__init__.py +86 -0
  46. omniload/src/elasticsearch/__init__.py +80 -0
  47. omniload/src/elasticsearch/helpers.py +141 -0
  48. omniload/src/errors.py +26 -0
  49. omniload/src/facebook_ads/__init__.py +403 -0
  50. omniload/src/facebook_ads/exceptions.py +19 -0
  51. omniload/src/facebook_ads/helpers.py +296 -0
  52. omniload/src/facebook_ads/settings.py +224 -0
  53. omniload/src/facebook_ads/utils.py +53 -0
  54. omniload/src/factory.py +305 -0
  55. omniload/src/filesystem/__init__.py +133 -0
  56. omniload/src/filesystem/helpers.py +114 -0
  57. omniload/src/filesystem/readers.py +187 -0
  58. omniload/src/filters.py +62 -0
  59. omniload/src/fireflies/__init__.py +151 -0
  60. omniload/src/fireflies/helpers.py +753 -0
  61. omniload/src/fluxx/__init__.py +10013 -0
  62. omniload/src/fluxx/helpers.py +233 -0
  63. omniload/src/frankfurter/__init__.py +157 -0
  64. omniload/src/frankfurter/helpers.py +48 -0
  65. omniload/src/freshdesk/__init__.py +103 -0
  66. omniload/src/freshdesk/freshdesk_client.py +151 -0
  67. omniload/src/freshdesk/settings.py +23 -0
  68. omniload/src/fundraiseup/__init__.py +95 -0
  69. omniload/src/fundraiseup/client.py +81 -0
  70. omniload/src/github/__init__.py +202 -0
  71. omniload/src/github/helpers.py +207 -0
  72. omniload/src/github/queries.py +129 -0
  73. omniload/src/github/settings.py +24 -0
  74. omniload/src/google_ads/__init__.py +198 -0
  75. omniload/src/google_ads/field.py +17 -0
  76. omniload/src/google_ads/metrics.py +254 -0
  77. omniload/src/google_ads/predicates.py +37 -0
  78. omniload/src/google_ads/reports.py +411 -0
  79. omniload/src/google_ads/test_google_ads.py +184 -0
  80. omniload/src/google_analytics/__init__.py +144 -0
  81. omniload/src/google_analytics/helpers.py +312 -0
  82. omniload/src/google_sheets/README.md +95 -0
  83. omniload/src/google_sheets/__init__.py +166 -0
  84. omniload/src/google_sheets/helpers/__init__.py +15 -0
  85. omniload/src/google_sheets/helpers/api_calls.py +160 -0
  86. omniload/src/google_sheets/helpers/data_processing.py +316 -0
  87. omniload/src/gorgias/__init__.py +595 -0
  88. omniload/src/gorgias/helpers.py +166 -0
  89. omniload/src/hostaway/__init__.py +302 -0
  90. omniload/src/hostaway/client.py +288 -0
  91. omniload/src/http/__init__.py +38 -0
  92. omniload/src/http/readers.py +146 -0
  93. omniload/src/http_client.py +24 -0
  94. omniload/src/hubspot/__init__.py +800 -0
  95. omniload/src/hubspot/helpers.py +417 -0
  96. omniload/src/hubspot/settings.py +329 -0
  97. omniload/src/indeed/__init__.py +153 -0
  98. omniload/src/indeed/helpers.py +228 -0
  99. omniload/src/influxdb/__init__.py +46 -0
  100. omniload/src/influxdb/client.py +34 -0
  101. omniload/src/intercom/__init__.py +142 -0
  102. omniload/src/intercom/helpers.py +674 -0
  103. omniload/src/intercom/settings.py +279 -0
  104. omniload/src/isoc_pulse/__init__.py +159 -0
  105. omniload/src/jira_source/__init__.py +377 -0
  106. omniload/src/jira_source/helpers.py +510 -0
  107. omniload/src/jira_source/settings.py +184 -0
  108. omniload/src/kafka/__init__.py +120 -0
  109. omniload/src/kafka/helpers.py +241 -0
  110. omniload/src/kinesis/__init__.py +153 -0
  111. omniload/src/kinesis/helpers.py +96 -0
  112. omniload/src/klaviyo/__init__.py +237 -0
  113. omniload/src/klaviyo/client.py +212 -0
  114. omniload/src/klaviyo/helpers.py +19 -0
  115. omniload/src/linear/__init__.py +634 -0
  116. omniload/src/linear/helpers.py +111 -0
  117. omniload/src/linkedin_ads/__init__.py +266 -0
  118. omniload/src/linkedin_ads/dimension_time_enum.py +17 -0
  119. omniload/src/linkedin_ads/helpers.py +246 -0
  120. omniload/src/loader.py +69 -0
  121. omniload/src/mailchimp/__init__.py +126 -0
  122. omniload/src/mailchimp/helpers.py +226 -0
  123. omniload/src/mailchimp/settings.py +164 -0
  124. omniload/src/masking.py +344 -0
  125. omniload/src/mixpanel/__init__.py +62 -0
  126. omniload/src/mixpanel/client.py +104 -0
  127. omniload/src/monday/__init__.py +246 -0
  128. omniload/src/monday/helpers.py +392 -0
  129. omniload/src/monday/settings.py +325 -0
  130. omniload/src/mongodb/__init__.py +281 -0
  131. omniload/src/mongodb/helpers.py +975 -0
  132. omniload/src/notion/__init__.py +69 -0
  133. omniload/src/notion/helpers/__init__.py +14 -0
  134. omniload/src/notion/helpers/client.py +178 -0
  135. omniload/src/notion/helpers/database.py +92 -0
  136. omniload/src/notion/settings.py +17 -0
  137. omniload/src/partition.py +32 -0
  138. omniload/src/personio/__init__.py +345 -0
  139. omniload/src/personio/helpers.py +100 -0
  140. omniload/src/phantombuster/__init__.py +65 -0
  141. omniload/src/phantombuster/client.py +87 -0
  142. omniload/src/pinterest/__init__.py +82 -0
  143. omniload/src/pipedrive/__init__.py +212 -0
  144. omniload/src/pipedrive/helpers/__init__.py +37 -0
  145. omniload/src/pipedrive/helpers/custom_fields_munger.py +116 -0
  146. omniload/src/pipedrive/helpers/pages.py +129 -0
  147. omniload/src/pipedrive/settings.py +41 -0
  148. omniload/src/pipedrive/typing.py +17 -0
  149. omniload/src/plusvibeai/__init__.py +335 -0
  150. omniload/src/plusvibeai/helpers.py +544 -0
  151. omniload/src/plusvibeai/settings.py +252 -0
  152. omniload/src/primer/__init__.py +45 -0
  153. omniload/src/primer/helpers.py +79 -0
  154. omniload/src/quickbooks/__init__.py +117 -0
  155. omniload/src/reddit_ads/__init__.py +183 -0
  156. omniload/src/reddit_ads/helpers.py +232 -0
  157. omniload/src/resource.py +40 -0
  158. omniload/src/revenuecat/__init__.py +83 -0
  159. omniload/src/revenuecat/helpers.py +237 -0
  160. omniload/src/salesforce/__init__.py +170 -0
  161. omniload/src/salesforce/helpers.py +78 -0
  162. omniload/src/shopify/__init__.py +1953 -0
  163. omniload/src/shopify/exceptions.py +17 -0
  164. omniload/src/shopify/helpers.py +202 -0
  165. omniload/src/shopify/settings.py +19 -0
  166. omniload/src/slack/__init__.py +290 -0
  167. omniload/src/slack/helpers.py +218 -0
  168. omniload/src/slack/settings.py +36 -0
  169. omniload/src/smartsheets/__init__.py +82 -0
  170. omniload/src/snapchat_ads/__init__.py +455 -0
  171. omniload/src/snapchat_ads/client.py +72 -0
  172. omniload/src/snapchat_ads/helpers.py +630 -0
  173. omniload/src/snapchat_ads/settings.py +130 -0
  174. omniload/src/socrata_source/__init__.py +83 -0
  175. omniload/src/socrata_source/helpers.py +85 -0
  176. omniload/src/socrata_source/settings.py +8 -0
  177. omniload/src/solidgate/__init__.py +219 -0
  178. omniload/src/solidgate/helpers.py +154 -0
  179. omniload/src/sources.py +5408 -0
  180. omniload/src/sql_database/__init__.py +0 -0
  181. omniload/src/sql_database/callbacks.py +66 -0
  182. omniload/src/stripe_analytics/__init__.py +183 -0
  183. omniload/src/stripe_analytics/helpers.py +386 -0
  184. omniload/src/stripe_analytics/settings.py +80 -0
  185. omniload/src/table_definition.py +15 -0
  186. omniload/src/testdata/fakebqcredentials.json +14 -0
  187. omniload/src/tiktok_ads/__init__.py +150 -0
  188. omniload/src/tiktok_ads/tiktok_helpers.py +130 -0
  189. omniload/src/time.py +11 -0
  190. omniload/src/trustpilot/__init__.py +48 -0
  191. omniload/src/trustpilot/client.py +48 -0
  192. omniload/src/version.py +6 -0
  193. omniload/src/wise/__init__.py +68 -0
  194. omniload/src/wise/client.py +63 -0
  195. omniload/src/zendesk/__init__.py +480 -0
  196. omniload/src/zendesk/helpers/__init__.py +39 -0
  197. omniload/src/zendesk/helpers/api_helpers.py +119 -0
  198. omniload/src/zendesk/helpers/credentials.py +68 -0
  199. omniload/src/zendesk/helpers/talk_api.py +132 -0
  200. omniload/src/zendesk/settings.py +71 -0
  201. omniload/src/zoom/__init__.py +99 -0
  202. omniload/src/zoom/helpers.py +102 -0
  203. omniload/testdata/.gitignore +2 -0
  204. omniload/testdata/create_replace.csv +21 -0
  205. omniload/testdata/delete_insert_expected.csv +6 -0
  206. omniload/testdata/delete_insert_part1.csv +5 -0
  207. omniload/testdata/delete_insert_part2.csv +6 -0
  208. omniload/testdata/merge_expected.csv +5 -0
  209. omniload/testdata/merge_part1.csv +4 -0
  210. omniload/testdata/merge_part2.csv +5 -0
  211. omniload/tests/unit/test_smartsheets.py +133 -0
  212. omniload-0.0.0.dev0.dist-info/METADATA +439 -0
  213. omniload-0.0.0.dev0.dist-info/RECORD +218 -0
  214. omniload-0.0.0.dev0.dist-info/WHEEL +4 -0
  215. omniload-0.0.0.dev0.dist-info/entry_points.txt +2 -0
  216. omniload-0.0.0.dev0.dist-info/licenses/LICENSE.Apache-2.0 +201 -0
  217. omniload-0.0.0.dev0.dist-info/licenses/LICENSE.md +21 -0
  218. omniload-0.0.0.dev0.dist-info/licenses/NOTICE +35 -0
@@ -0,0 +1,279 @@
1
+ """
2
+ Configuration settings and constants for Intercom API integration.
3
+ """
4
+
5
+ from datetime import datetime
6
+ from typing import Dict, List, Tuple
7
+
8
+ # API Version - REQUIRED for all requests
9
+ API_VERSION = "2.14"
10
+
11
+ # Default start date for incremental loading
12
+ DEFAULT_START_DATE = datetime(2020, 1, 1)
13
+
14
+ # Pagination settings
15
+ DEFAULT_PAGE_SIZE = 150
16
+ MAX_PAGE_SIZE = 150 # Intercom's maximum
17
+ SCROLL_EXPIRY_SECONDS = 60 # Scroll sessions expire after 1 minute
18
+
19
+ # Rate limiting settings
20
+ RATE_LIMIT_PER_10_SECONDS = 166
21
+ RATE_LIMIT_RETRY_AFTER_DEFAULT = 10
22
+
23
+ # Regional API endpoints
24
+ REGIONAL_ENDPOINTS = {
25
+ "us": "https://api.intercom.io",
26
+ "eu": "https://api.eu.intercom.io",
27
+ "au": "https://api.au.intercom.io",
28
+ }
29
+
30
+ # Resource configuration for automatic generation
31
+ # Format: resource_name -> config dict
32
+ RESOURCE_CONFIGS = {
33
+ # Search-based incremental resources
34
+ "contacts": {
35
+ "type": "search",
36
+ "incremental": True,
37
+ "transform_func": "transform_contact",
38
+ "columns": {
39
+ "custom_attributes": {"data_type": "json"},
40
+ "tags": {"data_type": "json"},
41
+ },
42
+ },
43
+ "conversations": {
44
+ "type": "search",
45
+ "incremental": True,
46
+ "transform_func": "transform_conversation",
47
+ "columns": {
48
+ "custom_attributes": {"data_type": "json"},
49
+ "tags": {"data_type": "json"},
50
+ },
51
+ },
52
+ # Pagination-based incremental resources
53
+ "companies": {
54
+ "type": "pagination",
55
+ "endpoint": "/companies",
56
+ "data_key": "data",
57
+ "pagination_type": "cursor",
58
+ "incremental": True,
59
+ "transform_func": "transform_company",
60
+ "params": {"per_page": 50},
61
+ "columns": {
62
+ "custom_attributes": {"data_type": "json"},
63
+ "tags": {"data_type": "json"},
64
+ },
65
+ },
66
+ "articles": {
67
+ "type": "pagination",
68
+ "endpoint": "/articles",
69
+ "data_key": "data",
70
+ "pagination_type": "cursor",
71
+ "incremental": True,
72
+ "transform_func": None,
73
+ "params": None,
74
+ "columns": {},
75
+ },
76
+ # Special case - tickets
77
+ "tickets": {
78
+ "type": "tickets",
79
+ "incremental": True,
80
+ "transform_func": None,
81
+ "columns": {
82
+ "ticket_attributes": {"data_type": "json"},
83
+ },
84
+ },
85
+ # Simple replace resources (non-incremental)
86
+ "tags": {
87
+ "type": "simple",
88
+ "endpoint": "/tags",
89
+ "data_key": "data",
90
+ "pagination_type": "simple",
91
+ "incremental": False,
92
+ "transform_func": None,
93
+ "columns": {},
94
+ },
95
+ "segments": {
96
+ "type": "simple",
97
+ "endpoint": "/segments",
98
+ "data_key": "segments",
99
+ "pagination_type": "cursor",
100
+ "incremental": False,
101
+ "transform_func": None,
102
+ "columns": {},
103
+ },
104
+ "teams": {
105
+ "type": "simple",
106
+ "endpoint": "/teams",
107
+ "data_key": "teams",
108
+ "pagination_type": "simple",
109
+ "incremental": False,
110
+ "transform_func": None,
111
+ "columns": {},
112
+ },
113
+ "admins": {
114
+ "type": "simple",
115
+ "endpoint": "/admins",
116
+ "data_key": "admins",
117
+ "pagination_type": "simple",
118
+ "incremental": False,
119
+ "transform_func": None,
120
+ "columns": {},
121
+ },
122
+ "data_attributes": {
123
+ "type": "simple",
124
+ "endpoint": "/data_attributes",
125
+ "data_key": "data",
126
+ "pagination_type": "cursor",
127
+ "incremental": False,
128
+ "transform_func": None,
129
+ "columns": {
130
+ "id": {"data_type": "bigint", "nullable": True},
131
+ },
132
+ },
133
+ }
134
+
135
+ # Core endpoints with their configuration (kept for backwards compatibility)
136
+ # Format: (endpoint_path, data_key, supports_incremental, pagination_type)
137
+ CORE_ENDPOINTS: Dict[str, Tuple[str, str, bool, str]] = {
138
+ "contacts": ("/contacts", "data", True, "cursor"),
139
+ "companies": ("/companies", "data", True, "cursor"),
140
+ "conversations": ("/conversations", "conversations", True, "cursor"),
141
+ "tickets": ("/tickets", "tickets", True, "cursor"),
142
+ "admins": ("/admins", "admins", False, "simple"),
143
+ "teams": ("/teams", "teams", False, "simple"),
144
+ "tags": ("/tags", "data", False, "simple"),
145
+ "segments": ("/segments", "segments", False, "cursor"),
146
+ "articles": ("/articles", "data", True, "cursor"),
147
+ "collections": ("/help_center/collections", "data", False, "cursor"),
148
+ "data_attributes": ("/data_attributes", "data", False, "cursor"),
149
+ }
150
+
151
+ # Incremental endpoints using search API
152
+ SEARCH_ENDPOINTS: Dict[str, str] = {
153
+ "contacts_search": "/contacts/search",
154
+ "companies_search": "/companies/search",
155
+ "conversations_search": "/conversations/search",
156
+ }
157
+
158
+ # Special endpoints requiring different handling
159
+ SCROLL_ENDPOINTS: List[str] = [
160
+ "companies", # Can use scroll for large exports
161
+ ]
162
+
163
+ # Event tracking endpoint
164
+ EVENTS_ENDPOINT = "/events"
165
+
166
+ # Ticket fields endpoint for custom field mapping
167
+ TICKET_FIELDS_ENDPOINT = "/ticket_types/{ticket_type_id}/attributes"
168
+
169
+ # Default fields to retrieve for each resource type
170
+ DEFAULT_CONTACT_FIELDS = [
171
+ "id",
172
+ "type",
173
+ "external_id",
174
+ "email",
175
+ "phone",
176
+ "name",
177
+ "created_at",
178
+ "updated_at",
179
+ "signed_up_at",
180
+ "last_seen_at",
181
+ "last_contacted_at",
182
+ "last_email_opened_at",
183
+ "last_email_clicked_at",
184
+ "browser",
185
+ "browser_language",
186
+ "browser_version",
187
+ "location",
188
+ "os",
189
+ "role",
190
+ "custom_attributes",
191
+ "tags",
192
+ "companies",
193
+ ]
194
+
195
+ DEFAULT_COMPANY_FIELDS = [
196
+ "id",
197
+ "type",
198
+ "company_id",
199
+ "name",
200
+ "plan",
201
+ "size",
202
+ "website",
203
+ "industry",
204
+ "created_at",
205
+ "updated_at",
206
+ "monthly_spend",
207
+ "session_count",
208
+ "user_count",
209
+ "custom_attributes",
210
+ "tags",
211
+ ]
212
+
213
+ DEFAULT_CONVERSATION_FIELDS = [
214
+ "id",
215
+ "type",
216
+ "created_at",
217
+ "updated_at",
218
+ "waiting_since",
219
+ "snoozed_until",
220
+ "state",
221
+ "open",
222
+ "read",
223
+ "priority",
224
+ "admin_assignee_id",
225
+ "team_assignee_id",
226
+ "tags",
227
+ "conversation_rating",
228
+ "source",
229
+ "contacts",
230
+ "teammates",
231
+ "custom_attributes",
232
+ "first_contact_reply",
233
+ "sla_applied",
234
+ "statistics",
235
+ "conversation_parts",
236
+ ]
237
+
238
+ DEFAULT_TICKET_FIELDS = [
239
+ "id",
240
+ "type",
241
+ "ticket_id",
242
+ "category",
243
+ "ticket_attributes",
244
+ "ticket_state",
245
+ "ticket_type",
246
+ "created_at",
247
+ "updated_at",
248
+ "ticket_parts",
249
+ "contacts",
250
+ "admin_assignee_id",
251
+ "team_assignee_id",
252
+ "open",
253
+ "snoozed_until",
254
+ ]
255
+
256
+ # Resources that support custom attributes
257
+ SUPPORTS_CUSTOM_ATTRIBUTES = [
258
+ "contacts",
259
+ "companies",
260
+ "conversations",
261
+ ]
262
+
263
+ # Maximum limits
264
+ MAX_CUSTOM_ATTRIBUTES_PER_RESOURCE = 100
265
+ MAX_EVENT_TYPES_PER_WORKSPACE = 120
266
+ MAX_CONVERSATION_PARTS = 500
267
+ MAX_SEARCH_RESULTS = 10000
268
+
269
+ # Field type mapping for custom attributes
270
+ INTERCOM_TO_DLT_TYPE_MAPPING = {
271
+ "string": "text",
272
+ "integer": "bigint",
273
+ "float": "double",
274
+ "boolean": "bool",
275
+ "date": "timestamp",
276
+ "datetime": "timestamp",
277
+ "object": "json",
278
+ "list": "json",
279
+ }
@@ -0,0 +1,159 @@
1
+ import math
2
+ from dataclasses import dataclass
3
+ from datetime import datetime
4
+ from typing import Any, Dict, Iterable, List, Optional
5
+
6
+ import dlt
7
+ from dlt.sources.rest_api import RESTAPIConfig, rest_api_resources
8
+
9
+ METRICS: Dict[str, str] = {
10
+ "dnssec_adoption": "dnssec/adoption",
11
+ "dnssec_tld_adoption": "dnssec/adoption",
12
+ "dnssec_validation": "dnssec/validation",
13
+ "http": "http",
14
+ "http3": "http3",
15
+ "https": "https",
16
+ "ipv6": "ipv6",
17
+ "net_loss": "net-loss",
18
+ "resilience": "resilience",
19
+ "roa": "roa",
20
+ "rov": "rov",
21
+ "tls": "tls",
22
+ "tls13": "tls13",
23
+ }
24
+
25
+
26
+ @dlt.source
27
+ def pulse_source(
28
+ token: str,
29
+ start_date: str,
30
+ metric: str,
31
+ opts: List[str],
32
+ end_date: Optional[str] = None,
33
+ ) -> Iterable[dlt.sources.DltResource]:
34
+ validate(metric, opts)
35
+ cfg = get_metric_cfg(metric, opts, start_date)
36
+ endpoint: Dict[str, Any] = {
37
+ "path": cfg.path,
38
+ "params": {
39
+ "start_date": "{incremental.start_value}",
40
+ **cfg.params,
41
+ },
42
+ "incremental": {
43
+ "cursor_path": "date",
44
+ "start_param": "start_date",
45
+ "end_param": "end_date",
46
+ "initial_value": start_date,
47
+ "end_value": end_date,
48
+ "range_start": "closed",
49
+ "range_end": "closed",
50
+ },
51
+ "paginator": "single_page",
52
+ }
53
+
54
+ if end_date is not None:
55
+ endpoint["params"]["end_date"] = end_date
56
+
57
+ resources = [
58
+ {
59
+ "name": metric,
60
+ "write_disposition": "merge",
61
+ "primary_key": "date",
62
+ "columns": {"date": {"data_type": "date"}},
63
+ "endpoint": endpoint,
64
+ }
65
+ ]
66
+
67
+ config: RESTAPIConfig = {
68
+ "client": {
69
+ "base_url": "https://pulse.internetsociety.org/api/",
70
+ "headers": {"Authorization": f"Bearer {token}"},
71
+ },
72
+ "resource_defaults": {
73
+ "write_disposition": "merge",
74
+ "primary_key": "date",
75
+ },
76
+ "resources": resources, # type:ignore
77
+ }
78
+ res = rest_api_resources(config)
79
+ if metric == "net_loss":
80
+ res[0].add_map(add_date(start_date))
81
+ yield from res
82
+
83
+
84
+ @dataclass
85
+ class MetricCfg:
86
+ path: str
87
+ params: Dict[str, Any]
88
+
89
+
90
+ def get_metric_cfg(metric: str, opts: List[str], start_date: str) -> MetricCfg:
91
+ path = METRICS.get(metric)
92
+ if path is None:
93
+ raise ValueError(f"Unknown metric '{metric}'.")
94
+ if len(opts) == 0:
95
+ return MetricCfg(path=path, params={})
96
+
97
+ if metric == "https":
98
+ return MetricCfg(
99
+ path=f"{path}/country/{opts[-1]}",
100
+ params={
101
+ "topsites": True if "topsites" in opts else False,
102
+ },
103
+ )
104
+ elif metric in ["dnssec_validation", "dnssec_tld_adoption"]:
105
+ return MetricCfg(path=f"{path}/country/{opts[-1]}", params={})
106
+ elif metric == "dnssec_adoption":
107
+ return MetricCfg(path=f"{path}/domains/{opts[-1]}", params={})
108
+ elif metric == "ipv6":
109
+ if "topsites" in opts:
110
+ return MetricCfg(path=path, params={"topsites": True})
111
+ return MetricCfg(path=f"{path}/country/{opts[-1]}", params={})
112
+ elif metric == "roa":
113
+ if len(opts) > 1:
114
+ return MetricCfg(
115
+ path=f"{path}/country/{opts[-1]}", params={"ip_version": opts[-2]}
116
+ )
117
+ return MetricCfg(path=path, params={"ip_version": opts[-1]})
118
+ elif metric == "net_loss":
119
+ return MetricCfg(
120
+ path=path,
121
+ params={
122
+ "country": opts[-1],
123
+ "shutdown_type": opts[-2],
124
+ },
125
+ )
126
+ elif metric == "resilience":
127
+ date = datetime.strptime(start_date, "%Y-%m-%d")
128
+ return MetricCfg(
129
+ path=path,
130
+ params={
131
+ "country": opts[-1],
132
+ "year": date.year,
133
+ "quarter": math.floor(date.month / 4) + 1,
134
+ },
135
+ )
136
+ else:
137
+ raise ValueError(
138
+ f"Unsupported metric '{metric}' with options {opts}. "
139
+ "Please check the metric and options."
140
+ )
141
+
142
+
143
+ def add_date(start_date: str):
144
+ def transform(item: dict):
145
+ item["date"] = start_date
146
+ return item
147
+
148
+ return transform
149
+
150
+
151
+ def validate(metric: str, opts: List[str]) -> None:
152
+ nopts = len(opts)
153
+ if metric == "net_loss" and nopts != 2:
154
+ raise ValueError(
155
+ "For 'net_loss' metric, two options are required: "
156
+ "'shutdown_type' and 'country'."
157
+ )
158
+ if nopts > 0 and metric in ["http", "http3", "tls", "tls13", "rov"]:
159
+ raise ValueError(f"metric '{metric}' does not support options. ")