ingestr 0.13.85__py3-none-any.whl → 0.13.87__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.

Potentially problematic release.


This version of ingestr might be problematic. Click here for more details.

@@ -0,0 +1,216 @@
1
+ import json
2
+ from typing import Any, Dict, Iterator, List, Optional
3
+
4
+ import dlt
5
+ import pendulum
6
+ import requests
7
+
8
+ FLUXX_API_BASE = "https://{instance}.fluxxlabs.com"
9
+ FLUXX_OAUTH_TOKEN_PATH = "/oauth/token"
10
+ FLUXX_API_V2_PATH = "/api/rest/v2"
11
+
12
+
13
+ def get_access_token(instance: str, client_id: str, client_secret: str) -> str:
14
+ """Obtain OAuth access token using client credentials flow."""
15
+ token_url = f"{FLUXX_API_BASE.format(instance=instance)}{FLUXX_OAUTH_TOKEN_PATH}"
16
+
17
+ response = requests.post(
18
+ token_url,
19
+ data={
20
+ "grant_type": "client_credentials",
21
+ "client_id": client_id,
22
+ "client_secret": client_secret,
23
+ },
24
+ )
25
+ response.raise_for_status()
26
+
27
+ token_data = response.json()
28
+ return token_data["access_token"]
29
+
30
+
31
+ def fluxx_api_request(
32
+ instance: str,
33
+ access_token: str,
34
+ endpoint: str,
35
+ method: str = "GET",
36
+ params: Optional[Dict[str, Any]] = None,
37
+ data: Optional[Dict[str, Any]] = None,
38
+ ) -> Dict[str, Any]:
39
+ """Make an authenticated request to the Fluxx API."""
40
+ url = f"{FLUXX_API_BASE.format(instance=instance)}{FLUXX_API_V2_PATH}/{endpoint}"
41
+
42
+ headers = {
43
+ "Authorization": f"Bearer {access_token}",
44
+ "Content-Type": "application/json",
45
+ }
46
+ # print(f"Making request to Fluxx API:")
47
+ # print(f" Method: {method}")
48
+ # print(f" URL: {url}")
49
+ # print(f" Headers: {headers}")
50
+ # print(f" Params: {params}")
51
+ # print(f" Data: {data}")
52
+
53
+ response = requests.request(
54
+ method=method,
55
+ url=url,
56
+ headers=headers,
57
+ params=params,
58
+ json=data,
59
+ )
60
+ response.raise_for_status()
61
+
62
+ if response.text:
63
+ return response.json()
64
+ return {}
65
+
66
+
67
+ def paginate_fluxx_resource(
68
+ instance: str,
69
+ access_token: str,
70
+ endpoint: str,
71
+ params: Optional[Dict[str, Any]] = None,
72
+ page_size: int = 100,
73
+ ) -> Iterator[List[Dict[str, Any]]]:
74
+ """Paginate through a Fluxx API resource."""
75
+ if params is None:
76
+ params = {}
77
+
78
+ page = 1
79
+ params["per_page"] = page_size
80
+
81
+ while True:
82
+ params["page"] = page
83
+
84
+ response = fluxx_api_request(
85
+ instance=instance,
86
+ access_token=access_token,
87
+ endpoint=endpoint,
88
+ params=params,
89
+ )
90
+
91
+ print("resssponse", response)
92
+ if not response:
93
+ break
94
+
95
+ # Get the first available key from records instead of assuming endpoint name
96
+ records = response["records"]
97
+ if records:
98
+ # Pick the first key available in records
99
+ first_key = next(iter(records))
100
+ items = records[first_key]
101
+ else:
102
+ items = []
103
+
104
+ yield items
105
+
106
+ if response["per_page"] is None or len(items) < response["per_page"]:
107
+ break
108
+
109
+ page += 1
110
+
111
+
112
+ def get_date_range(updated_at, start_date):
113
+ """Extract current start and end dates from incremental state."""
114
+ if updated_at.last_value:
115
+ current_start_date = pendulum.parse(updated_at.last_value)
116
+ else:
117
+ current_start_date = (
118
+ pendulum.parse(start_date)
119
+ if start_date
120
+ else pendulum.now().subtract(days=30)
121
+ )
122
+
123
+ if updated_at.end_value:
124
+ current_end_date = pendulum.parse(updated_at.end_value)
125
+ else:
126
+ current_end_date = pendulum.now(tz="UTC")
127
+
128
+ return current_start_date, current_end_date
129
+
130
+
131
+ def create_dynamic_resource(
132
+ resource_name: str,
133
+ endpoint: str,
134
+ instance: str,
135
+ access_token: str,
136
+ start_date: Optional[pendulum.DateTime] = None,
137
+ end_date: Optional[pendulum.DateTime] = None,
138
+ fields_to_extract: Optional[Dict[str, Any]] = None,
139
+ ):
140
+ """Factory function to create dynamic Fluxx resources."""
141
+
142
+ # Extract column definitions for DLT resource
143
+ columns = {}
144
+ if fields_to_extract:
145
+ for field_name, field_config in fields_to_extract.items():
146
+ data_type = field_config.get("data_type")
147
+ if data_type:
148
+ columns[field_name] = {"data_type": data_type}
149
+
150
+ @dlt.resource(name=resource_name, write_disposition="replace", columns=columns) # type: ignore
151
+ def fluxx_resource() -> Iterator[Dict[str, Any]]:
152
+ params = {}
153
+ if fields_to_extract:
154
+ field_names = list(fields_to_extract.keys())
155
+ params["cols"] = json.dumps(field_names)
156
+
157
+ for page in paginate_fluxx_resource(
158
+ instance=instance,
159
+ access_token=access_token,
160
+ endpoint=endpoint,
161
+ params=params,
162
+ page_size=100,
163
+ ):
164
+ yield [normalize_fluxx_item(item, fields_to_extract) for item in page] # type: ignore
165
+
166
+ return fluxx_resource
167
+
168
+
169
+ def normalize_fluxx_item(
170
+ item: Dict[str, Any], fields_to_extract: Optional[Dict[str, Any]] = None
171
+ ) -> Dict[str, Any]:
172
+ """
173
+ Normalize a Fluxx API response item.
174
+ Handles nested structures and field extraction based on field types.
175
+ Rounds all decimal/float values to 4 decimal places regardless of field type.
176
+ """
177
+ normalized: Dict[str, Any] = {}
178
+
179
+ # If no field mapping provided, just return the item as-is
180
+ if not fields_to_extract:
181
+ return item
182
+
183
+ for field_name, field_config in fields_to_extract.items():
184
+ if field_name in item:
185
+ value = item[field_name]
186
+ field_type = field_config.get("data_type")
187
+
188
+ if isinstance(value, float):
189
+ # Round any numeric value with decimal places
190
+ normalized[field_name] = round(value, 4)
191
+ elif field_type == "json":
192
+ # Handle json fields (arrays/relations)
193
+ if value is None:
194
+ normalized[field_name] = None
195
+ elif value == "":
196
+ normalized[field_name] = None
197
+ elif isinstance(value, (list, dict)):
198
+ normalized[field_name] = value
199
+ else:
200
+ # Single value - wrap in array for json fields
201
+ normalized[field_name] = [value]
202
+ elif field_type in ("date", "timestamp", "datetime", "text"):
203
+ # Handle text/date fields - convert empty strings to None
204
+ if value == "":
205
+ normalized[field_name] = None
206
+ else:
207
+ normalized[field_name] = value
208
+ else:
209
+ # All other field types - pass through as-is
210
+ normalized[field_name] = value
211
+
212
+ # Always include id if present
213
+ if "id" in item:
214
+ normalized["id"] = item["id"]
215
+
216
+ return normalized
@@ -14,150 +14,148 @@ from ingestr.src.frankfurter.helpers import get_path_with_retry
14
14
  )
15
15
  def frankfurter_source(
16
16
  start_date: TAnyDateTime,
17
- end_date: TAnyDateTime,
17
+ end_date: TAnyDateTime|None,
18
18
  base_currency: str,
19
19
  ) -> Any:
20
20
  """
21
21
  A dlt source for the frankfurter.dev API. It groups several resources (in this case frankfurter.dev API endpoints) containing
22
22
  various types of data: currencies, latest rates, historical rates.
23
23
  """
24
- date_time = dlt.sources.incremental(
25
- "date",
26
- initial_value=start_date,
27
- end_value=end_date,
28
- range_start="closed",
29
- range_end="closed",
30
- )
24
+
31
25
 
32
- return (
33
- currencies(),
34
- latest(base_currency=base_currency),
35
- exchange_rates(
36
- start_date=date_time, end_date=end_date, base_currency=base_currency
37
- ),
26
+ @dlt.resource(
27
+ write_disposition="replace",
38
28
  )
29
+ def currencies() -> Iterator[dict]:
30
+ """
31
+ Yields each currency as a separate row with two columns: currency_code and currency_name.
32
+ """
33
+ # Retrieve the list of currencies from the API
34
+ currencies_data = get_path_with_retry("currencies")
35
+
36
+ for currency_code, currency_name in currencies_data.items():
37
+ yield {"currency_code": currency_code, "currency_name": currency_name}
38
+
39
+
40
+ @dlt.resource(
41
+ write_disposition="merge",
42
+ columns={
43
+ "date": {"data_type": "text"},
44
+ "currency_code": {"data_type": "text"},
45
+ "rate": {"data_type": "double"},
46
+ "base_currency": {"data_type": "text"},
47
+ },
48
+ primary_key=["date", "currency_code", "base_currency"],
49
+ )
50
+ def latest(base_currency: Optional[str] = "") -> Iterator[dict]:
51
+ """
52
+ Fetches the latest exchange rates and yields them as rows.
53
+ """
54
+ # Base URL
55
+ url = "latest?"
39
56
 
57
+ if base_currency:
58
+ url += f"base={base_currency}"
40
59
 
41
- @dlt.resource(
42
- write_disposition="replace",
43
- columns={
44
- "currency_code": {"data_type": "text"},
45
- "currency_name": {"data_type": "text"},
46
- },
47
- )
48
- def currencies() -> Iterator[dict]:
49
- """
50
- Yields each currency as a separate row with two columns: currency_code and currency_name.
51
- """
52
- # Retrieve the list of currencies from the API
53
- currencies_data = get_path_with_retry("currencies")
54
-
55
- for currency_code, currency_name in currencies_data.items():
56
- yield {"currency_code": currency_code, "currency_name": currency_name}
57
-
58
-
59
- @dlt.resource(
60
- write_disposition="merge",
61
- columns={
62
- "date": {"data_type": "text"},
63
- "currency_code": {"data_type": "text"},
64
- "rate": {"data_type": "double"},
65
- "base_currency": {"data_type": "text"},
66
- },
67
- primary_key=["date", "currency_code", "base_currency"],
68
- )
69
- def latest(base_currency: Optional[str] = "") -> Iterator[dict]:
70
- """
71
- Fetches the latest exchange rates and yields them as rows.
72
- """
73
- # Base URL
74
- url = "latest?"
75
-
76
- if base_currency:
77
- url += f"base={base_currency}"
78
-
79
- # Fetch data
80
- data = get_path_with_retry(url)
81
-
82
- # Extract rates and base currency
83
- rates = data["rates"]
84
- date = pendulum.parse(data["date"])
85
-
86
- # Add the base currency with a rate of 1.0
87
- yield {
88
- "date": date,
89
- "currency_code": base_currency,
90
- "rate": 1.0,
91
- "base_currency": base_currency,
92
- }
93
-
94
- # Add all currencies and their rates
95
- for currency_code, rate in rates.items():
96
- yield {
97
- "date": date,
98
- "currency_code": currency_code,
99
- "rate": rate,
100
- "base_currency": base_currency,
101
- }
102
-
103
-
104
- @dlt.resource(
105
- write_disposition="merge",
106
- columns={
107
- "date": {"data_type": "text"},
108
- "currency_code": {"data_type": "text"},
109
- "rate": {"data_type": "double"},
110
- "base_currency": {"data_type": "text"},
111
- },
112
- primary_key=("date", "currency_code", "base_currency"),
113
- )
114
- def exchange_rates(
115
- end_date: TAnyDateTime,
116
- start_date: dlt.sources.incremental[TAnyDateTime] = dlt.sources.incremental("date"),
117
- base_currency: Optional[str] = "",
118
- ) -> Iterator[dict]:
119
- """
120
- Fetches exchange rates for a specified date range.
121
- If only start_date is provided, fetches data until now.
122
- If both start_date and end_date are provided, fetches data for each day in the range.
123
- """
124
- # Ensure start_date.last_value is a pendulum.DateTime object
125
- start_date_obj = ensure_pendulum_datetime(start_date.last_value) # type: ignore
126
- start_date_str = start_date_obj.format("YYYY-MM-DD")
127
-
128
- # Ensure end_date is a pendulum.DateTime object
129
- end_date_obj = ensure_pendulum_datetime(end_date)
130
- end_date_str = end_date_obj.format("YYYY-MM-DD")
131
-
132
- # Compose the URL
133
- url = f"{start_date_str}..{end_date_str}?"
134
-
135
- if base_currency:
136
- url += f"base={base_currency}"
137
-
138
- # Fetch data from the API
139
- data = get_path_with_retry(url)
140
-
141
- # Extract base currency and rates from the API response
142
- rates = data["rates"]
60
+ # Fetch data
61
+ data = get_path_with_retry(url)
143
62
 
144
- # Iterate over the rates dictionary (one entry per date)
145
- for date, daily_rates in rates.items():
146
- formatted_date = pendulum.parse(date)
63
+ # Extract rates and base currency
64
+ rates = data["rates"]
65
+ date = pendulum.parse(data["date"])
147
66
 
148
67
  # Add the base currency with a rate of 1.0
149
68
  yield {
150
- "date": formatted_date,
69
+ "date": date,
151
70
  "currency_code": base_currency,
152
71
  "rate": 1.0,
153
72
  "base_currency": base_currency,
154
73
  }
155
74
 
156
- # Add all other currencies and their rates
157
- for currency_code, rate in daily_rates.items():
75
+ # Add all currencies and their rates
76
+ for currency_code, rate in rates.items():
158
77
  yield {
159
- "date": formatted_date,
78
+ "date": date,
160
79
  "currency_code": currency_code,
161
80
  "rate": rate,
162
81
  "base_currency": base_currency,
163
82
  }
83
+
84
+
85
+ @dlt.resource(
86
+ write_disposition="merge",
87
+ columns={
88
+ "date": {"data_type": "text"},
89
+ "currency_code": {"data_type": "text"},
90
+ "rate": {"data_type": "double"},
91
+ "base_currency": {"data_type": "text"},
92
+ },
93
+ primary_key=("date", "currency_code", "base_currency"),
94
+ )
95
+ def exchange_rates(
96
+ date_time = dlt.sources.incremental(
97
+ "date",
98
+ initial_value=start_date,
99
+ end_value=end_date,
100
+ range_start="closed",
101
+ range_end="closed",
102
+ )
103
+ ) -> Iterator[dict]:
104
+ """
105
+ Fetches exchange rates for a specified date range.
106
+ If only start_date is provided, fetches data until now.
107
+ If both start_date and end_date are provided, fetches data for each day in the range.
108
+ """
109
+ if date_time.last_value is not None:
110
+ start_date = date_time.last_value
111
+ else:
112
+ start_date = start_date
113
+
114
+ if date_time.end_value is not None:
115
+ end_date = date_time.end_value
116
+ else:
117
+ end_date = pendulum.now()
118
+
119
+ # Ensure start_date.last_value is a pendulum.DateTime object
120
+ start_date_obj = ensure_pendulum_datetime(start_date) # type: ignore
121
+ start_date_str = start_date_obj.format("YYYY-MM-DD")
122
+
123
+ # Ensure end_date is a pendulum.DateTime object
124
+ end_date_obj = ensure_pendulum_datetime(end_date)
125
+ end_date_str = end_date_obj.format("YYYY-MM-DD")
126
+
127
+ # Compose the URL
128
+ url = f"{start_date_str}..{end_date_str}?"
129
+
130
+ if base_currency:
131
+ url += f"base={base_currency}"
132
+
133
+ # Fetch data from the API
134
+ data = get_path_with_retry(url)
135
+
136
+ # Extract base currency and rates from the API response
137
+ rates = data["rates"]
138
+
139
+ # Iterate over the rates dictionary (one entry per date)
140
+ for date, daily_rates in rates.items():
141
+ formatted_date = pendulum.parse(date)
142
+
143
+ # Add the base currency with a rate of 1.0
144
+ yield {
145
+ "date": formatted_date,
146
+ "currency_code": base_currency,
147
+ "rate": 1.0,
148
+ "base_currency": base_currency,
149
+ }
150
+
151
+ # Add all other currencies and their rates
152
+ for currency_code, rate in daily_rates.items():
153
+ yield {
154
+ "date": formatted_date,
155
+ "currency_code": currency_code,
156
+ "rate": rate,
157
+ "base_currency": base_currency,
158
+ }
159
+
160
+ return currencies, latest, exchange_rates
161
+
@@ -16,19 +16,19 @@ def get_path_with_retry(path: str) -> StrAny:
16
16
  return get_url_with_retry(f"{FRANKFURTER_API_URL}{path}")
17
17
 
18
18
 
19
- def validate_dates(start_date: datetime, end_date: datetime) -> None:
19
+ def validate_dates(start_date: datetime, end_date: datetime|None) -> None:
20
20
  current_date = pendulum.now()
21
-
21
+
22
22
  # Check if start_date is in the futurep
23
23
  if start_date > current_date:
24
24
  raise ValueError("Interval-start cannot be in the future.")
25
25
 
26
26
  # Check if end_date is in the future
27
- if end_date > current_date:
27
+ if end_date is not None and end_date > current_date:
28
28
  raise ValueError("Interval-end cannot be in the future.")
29
29
 
30
30
  # Check if start_date is before end_date
31
- if start_date > end_date:
31
+ if end_date is not None and start_date > end_date:
32
32
  raise ValueError("Interval-end cannot be before interval-start.")
33
33
 
34
34
 
@@ -30,7 +30,7 @@ def klaviyo_source(api_key: str, start_date: TAnyDateTime) -> Iterable[DltResour
30
30
  start_date_obj = ensure_pendulum_datetime(start_date)
31
31
  client = KlaviyoClient(api_key)
32
32
 
33
- @dlt.resource(write_disposition="append", primary_key="id", parallelized=True)
33
+ @dlt.resource(write_disposition="merge", primary_key="id", parallelized=True)
34
34
  def events(
35
35
  datetime=dlt.sources.incremental(
36
36
  "datetime",
@@ -135,7 +135,7 @@ def klaviyo_source(api_key: str, start_date: TAnyDateTime) -> Iterable[DltResour
135
135
  ) -> Iterable[TDataItem]:
136
136
  yield from client.fetch_catalog_item(create_client(), updated.start_value)
137
137
 
138
- @dlt.resource(write_disposition="append", primary_key="id", parallelized=True)
138
+ @dlt.resource(write_disposition="merge", primary_key="id", parallelized=True)
139
139
  def forms(
140
140
  updated_at=dlt.sources.incremental(
141
141
  "updated_at",
@@ -162,7 +162,7 @@ def klaviyo_source(api_key: str, start_date: TAnyDateTime) -> Iterable[DltResour
162
162
  ) -> Iterable[TDataItem]:
163
163
  yield from client.fetch_lists(create_client(), updated.start_value)
164
164
 
165
- @dlt.resource(write_disposition="append", primary_key="id", parallelized=True)
165
+ @dlt.resource(write_disposition="merge", primary_key="id", parallelized=True)
166
166
  def images(
167
167
  updated_at=dlt.sources.incremental(
168
168
  "updated_at",
@@ -188,7 +188,7 @@ def klaviyo_source(api_key: str, start_date: TAnyDateTime) -> Iterable[DltResour
188
188
  ) -> Iterable[TDataItem]:
189
189
  yield from client.fetch_segments(create_client(), updated.start_value)
190
190
 
191
- @dlt.resource(write_disposition="append", primary_key="id", parallelized=True)
191
+ @dlt.resource(write_disposition="merge", primary_key="id", parallelized=True)
192
192
  def flows(
193
193
  updated=dlt.sources.incremental(
194
194
  "updated",
@@ -203,7 +203,7 @@ def klaviyo_source(api_key: str, start_date: TAnyDateTime) -> Iterable[DltResour
203
203
  for start, end in intervals:
204
204
  yield lambda s=start, e=end: client.fetch_flows(create_client(), s, e)
205
205
 
206
- @dlt.resource(write_disposition="append", primary_key="id", parallelized=True)
206
+ @dlt.resource(write_disposition="merge", primary_key="id", parallelized=True)
207
207
  def templates(
208
208
  updated=dlt.sources.incremental(
209
209
  "updated",
@@ -2,10 +2,13 @@ from typing import Any, Dict, Iterable, Iterator
2
2
 
3
3
  import dlt
4
4
  import pendulum
5
- import requests
6
-
7
- from .helpers import _graphql, normalize_dictionaries, _get_date_range, _create_paginated_resource
8
5
 
6
+ from .helpers import (
7
+ _create_paginated_resource,
8
+ _get_date_range,
9
+ _graphql,
10
+ normalize_dictionaries,
11
+ )
9
12
 
10
13
  ISSUES_QUERY = """
11
14
  query Issues($cursor: String) {
@@ -249,7 +252,6 @@ query Initiatives($cursor: String) {
249
252
  """
250
253
 
251
254
 
252
-
253
255
  INITIATIVE_TO_PROJECTS_QUERY = """
254
256
  query InitiativeToProjects($cursor: String) {
255
257
  initiativeToProjects(first: 50, after: $cursor) {
@@ -406,7 +408,6 @@ query ProjectUpdates($cursor: String) {
406
408
  """
407
409
 
408
410
 
409
-
410
411
  TEAM_MEMBERSHIPS_QUERY = """
411
412
  query TeamMemberships($cursor: String) {
412
413
  teamMemberships(first: 50, after: $cursor) {
@@ -594,14 +595,12 @@ PAGINATED_RESOURCES = [
594
595
  ]
595
596
 
596
597
 
597
-
598
598
  @dlt.source(name="linear", max_table_nesting=0)
599
599
  def linear_source(
600
600
  api_key: str,
601
601
  start_date: pendulum.DateTime,
602
602
  end_date: pendulum.DateTime | None = None,
603
603
  ) -> Iterable[dlt.sources.DltResource]:
604
-
605
604
  @dlt.resource(name="organization", primary_key="id", write_disposition="merge")
606
605
  def organization(
607
606
  updated_at: dlt.sources.incremental[str] = dlt.sources.incremental(
@@ -623,10 +622,12 @@ def linear_source(
623
622
 
624
623
  # Create paginated resources dynamically
625
624
  paginated_resources = [
626
- _create_paginated_resource(resource_name, query, query_field, api_key, start_date, end_date)
625
+ _create_paginated_resource(
626
+ resource_name, query, query_field, api_key, start_date, end_date
627
+ )
627
628
  for resource_name, query, query_field in PAGINATED_RESOURCES
628
629
  ]
629
-
630
+
630
631
  return [
631
632
  *paginated_resources,
632
633
  organization,
@@ -1,8 +1,8 @@
1
1
  from typing import Any, Dict, Iterator, Optional
2
2
 
3
- import requests
4
- import pendulum
5
3
  import dlt
4
+ import pendulum
5
+ import requests
6
6
 
7
7
  LINEAR_GRAPHQL_ENDPOINT = "https://api.linear.app/graphql"
8
8
 
@@ -34,8 +34,6 @@ def _paginate(api_key: str, query: str, root: str) -> Iterator[Dict[str, Any]]:
34
34
  cursor = data["pageInfo"]["endCursor"]
35
35
 
36
36
 
37
-
38
-
39
37
  def _get_date_range(updated_at, start_date):
40
38
  """Extract current start and end dates from incremental state."""
41
39
  if updated_at.last_value:
@@ -47,11 +45,13 @@ def _get_date_range(updated_at, start_date):
47
45
  current_end_date = pendulum.parse(updated_at.end_value)
48
46
  else:
49
47
  current_end_date = pendulum.now(tz="UTC")
50
-
48
+
51
49
  return current_start_date, current_end_date
52
50
 
53
51
 
54
- def _paginated_resource(api_key: str, query: str, query_field: str, updated_at, start_date) -> Iterator[Dict[str, Any]]:
52
+ def _paginated_resource(
53
+ api_key: str, query: str, query_field: str, updated_at, start_date
54
+ ) -> Iterator[Dict[str, Any]]:
55
55
  """Helper function for paginated resources with date filtering."""
56
56
  current_start_date, current_end_date = _get_date_range(updated_at, start_date)
57
57
 
@@ -61,8 +61,16 @@ def _paginated_resource(api_key: str, query: str, query_field: str, updated_at,
61
61
  yield normalize_dictionaries(item)
62
62
 
63
63
 
64
- def _create_paginated_resource(resource_name: str, query: str, query_field: str, api_key: str, start_date, end_date = None):
64
+ def _create_paginated_resource(
65
+ resource_name: str,
66
+ query: str,
67
+ query_field: str,
68
+ api_key: str,
69
+ start_date,
70
+ end_date=None,
71
+ ):
65
72
  """Factory function to create paginated resources dynamically."""
73
+
66
74
  @dlt.resource(name=resource_name, primary_key="id", write_disposition="merge")
67
75
  def paginated_resource(
68
76
  updated_at: dlt.sources.incremental[str] = dlt.sources.incremental(
@@ -73,9 +81,11 @@ def _create_paginated_resource(resource_name: str, query: str, query_field: str,
73
81
  range_end="closed",
74
82
  ),
75
83
  ) -> Iterator[Dict[str, Any]]:
76
- for item in _paginated_resource(api_key, query, query_field, updated_at, start_date):
84
+ for item in _paginated_resource(
85
+ api_key, query, query_field, updated_at, start_date
86
+ ):
77
87
  yield normalize_dictionaries(item)
78
-
88
+
79
89
  return paginated_resource
80
90
 
81
91
 
@@ -84,7 +94,7 @@ def normalize_dictionaries(item: Dict[str, Any]) -> Dict[str, Any]:
84
94
  Automatically normalize dictionary fields by detecting their structure:
85
95
  - Convert nested objects with 'id' field to {field_name}_id
86
96
  - Convert objects with 'nodes' field to arrays
87
-
97
+
88
98
  """
89
99
  normalized_item = item.copy()
90
100