ingestr 0.13.87__py3-none-any.whl → 0.13.89__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.
- ingestr/main.py +12 -0
- ingestr/src/applovin/__init__.py +1 -1
- ingestr/src/asana_source/__init__.py +1 -1
- ingestr/src/buildinfo.py +1 -1
- ingestr/src/destinations.py +37 -2
- ingestr/src/filesystem/__init__.py +8 -3
- ingestr/src/filters.py +9 -0
- ingestr/src/frankfurter/__init__.py +10 -14
- ingestr/src/frankfurter/helpers.py +2 -2
- ingestr/src/masking.py +344 -0
- ingestr/src/mongodb/helpers.py +11 -7
- ingestr/src/revenuecat/__init__.py +4 -4
- ingestr/src/revenuecat/helpers.py +4 -4
- ingestr/src/salesforce/__init__.py +9 -8
- ingestr/src/sources.py +1 -0
- {ingestr-0.13.87.dist-info → ingestr-0.13.89.dist-info}/METADATA +2 -2
- {ingestr-0.13.87.dist-info → ingestr-0.13.89.dist-info}/RECORD +20 -19
- {ingestr-0.13.87.dist-info → ingestr-0.13.89.dist-info}/WHEEL +0 -0
- {ingestr-0.13.87.dist-info → ingestr-0.13.89.dist-info}/entry_points.txt +0 -0
- {ingestr-0.13.87.dist-info → ingestr-0.13.89.dist-info}/licenses/LICENSE.md +0 -0
ingestr/main.py
CHANGED
|
@@ -282,6 +282,13 @@ def ingest(
|
|
|
282
282
|
envvar=["STAGING_BUCKET", "INGESTR_STAGING_BUCKET"],
|
|
283
283
|
),
|
|
284
284
|
] = None, # type: ignore
|
|
285
|
+
mask: Annotated[
|
|
286
|
+
Optional[list[str]],
|
|
287
|
+
typer.Option(
|
|
288
|
+
help="Column masking configuration in format 'column:algorithm[:param]'. Can be specified multiple times.",
|
|
289
|
+
envvar=["MASK", "INGESTR_MASK"],
|
|
290
|
+
),
|
|
291
|
+
] = [], # type: ignore
|
|
285
292
|
):
|
|
286
293
|
import hashlib
|
|
287
294
|
import tempfile
|
|
@@ -302,6 +309,7 @@ def ingest(
|
|
|
302
309
|
from ingestr.src.filters import (
|
|
303
310
|
cast_set_to_list,
|
|
304
311
|
cast_spanner_types,
|
|
312
|
+
create_masking_filter,
|
|
305
313
|
handle_mysql_empty_dates,
|
|
306
314
|
)
|
|
307
315
|
from ingestr.src.sources import MongoDbSource
|
|
@@ -562,6 +570,10 @@ def ingest(
|
|
|
562
570
|
if factory.source_scheme.startswith("spanner"):
|
|
563
571
|
resource.for_each(dlt_source, lambda x: x.add_map(cast_spanner_types))
|
|
564
572
|
|
|
573
|
+
if mask:
|
|
574
|
+
masking_filter = create_masking_filter(mask)
|
|
575
|
+
resource.for_each(dlt_source, lambda x: x.add_map(masking_filter))
|
|
576
|
+
|
|
565
577
|
if yield_limit:
|
|
566
578
|
resource.for_each(dlt_source, lambda x: x.add_limit(yield_limit))
|
|
567
579
|
|
ingestr/src/applovin/__init__.py
CHANGED
|
@@ -224,7 +224,7 @@ def resource(
|
|
|
224
224
|
def custom_report_from_spec(spec: str) -> EndpointResource:
|
|
225
225
|
parts = spec.split(":")
|
|
226
226
|
if len(parts) != 4:
|
|
227
|
-
raise InvalidCustomReportError()
|
|
227
|
+
raise InvalidCustomReportError()
|
|
228
228
|
|
|
229
229
|
_, endpoint, report, dims = parts
|
|
230
230
|
report_type = ReportType(report.strip())
|
ingestr/src/buildinfo.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
version = "v0.13.
|
|
1
|
+
version = "v0.13.89"
|
ingestr/src/destinations.py
CHANGED
|
@@ -25,7 +25,20 @@ from ingestr.src.loader import load_dlt_file
|
|
|
25
25
|
|
|
26
26
|
class GenericSqlDestination:
|
|
27
27
|
def dlt_run_params(self, uri: str, table: str, **kwargs) -> dict:
|
|
28
|
-
|
|
28
|
+
|
|
29
|
+
if uri.startswith("databricks://"):
|
|
30
|
+
p = urlparse(uri)
|
|
31
|
+
q = parse_qs(p.query)
|
|
32
|
+
schema = q.get("schema", [None])[0]
|
|
33
|
+
if not schema:
|
|
34
|
+
raise ValueError("Databricks requires schema in the URI.")
|
|
35
|
+
res = {
|
|
36
|
+
"dataset_name": schema,
|
|
37
|
+
"table_name": table,
|
|
38
|
+
}
|
|
39
|
+
return res
|
|
40
|
+
|
|
41
|
+
table_fields = table.split(".")
|
|
29
42
|
if len(table_fields) != 2:
|
|
30
43
|
raise ValueError("Table name must be in the format <schema>.<table>")
|
|
31
44
|
|
|
@@ -270,8 +283,30 @@ class MsSQLDestination(GenericSqlDestination):
|
|
|
270
283
|
|
|
271
284
|
class DatabricksDestination(GenericSqlDestination):
|
|
272
285
|
def dlt_dest(self, uri: str, **kwargs):
|
|
273
|
-
|
|
286
|
+
p = urlparse(uri)
|
|
287
|
+
q = parse_qs(p.query)
|
|
288
|
+
access_token = p.password
|
|
289
|
+
server_hostname = p.hostname
|
|
290
|
+
http_path = q.get("http_path", [None])[0]
|
|
291
|
+
catalog = q.get("catalog", [None])[0]
|
|
292
|
+
schema = q.get("schema", [None])[0]
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
creds = {
|
|
296
|
+
"access_token": access_token,
|
|
297
|
+
"server_hostname": server_hostname,
|
|
298
|
+
"http_path": http_path,
|
|
299
|
+
"catalog": catalog,
|
|
300
|
+
"schema": schema,
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return dlt.destinations.databricks(
|
|
304
|
+
credentials=creds,
|
|
305
|
+
**kwargs,
|
|
306
|
+
)
|
|
307
|
+
|
|
274
308
|
|
|
309
|
+
|
|
275
310
|
|
|
276
311
|
class SynapseDestination(GenericSqlDestination):
|
|
277
312
|
def dlt_dest(self, uri: str, **kwargs):
|
|
@@ -37,9 +37,14 @@ def readers(
|
|
|
37
37
|
file_glob (str, optional): The filter to apply to the files in glob format. by default lists all files in bucket_url non-recursively
|
|
38
38
|
"""
|
|
39
39
|
filesystem_resource = filesystem(bucket_url, credentials, file_glob=file_glob)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
|
|
41
|
+
# NOTE: incremental support is disabled until we can figure out
|
|
42
|
+
# how to support incremental loads per matching file, rather
|
|
43
|
+
# than a blanket threshold.
|
|
44
|
+
#
|
|
45
|
+
# filesystem_resource.apply_hints(
|
|
46
|
+
# incremental=dlt.sources.incremental("modification_date"),
|
|
47
|
+
# )
|
|
43
48
|
return (
|
|
44
49
|
filesystem_resource | dlt.transformer(name="read_csv")(_read_csv),
|
|
45
50
|
filesystem_resource | dlt.transformer(name="read_jsonl")(_read_jsonl),
|
ingestr/src/filters.py
CHANGED
|
@@ -51,3 +51,12 @@ def table_adapter_exclude_columns(cols: list[str]):
|
|
|
51
51
|
table._columns.remove(col) # type: ignore
|
|
52
52
|
|
|
53
53
|
return excluder
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def create_masking_filter(mask_configs: list[str]):
|
|
57
|
+
from ingestr.src.masking import create_masking_mapper
|
|
58
|
+
|
|
59
|
+
if not mask_configs:
|
|
60
|
+
return lambda x: x
|
|
61
|
+
|
|
62
|
+
return create_masking_mapper(mask_configs)
|
|
@@ -14,14 +14,13 @@ 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|None,
|
|
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
|
-
|
|
25
24
|
|
|
26
25
|
@dlt.resource(
|
|
27
26
|
write_disposition="replace",
|
|
@@ -36,7 +35,6 @@ def frankfurter_source(
|
|
|
36
35
|
for currency_code, currency_name in currencies_data.items():
|
|
37
36
|
yield {"currency_code": currency_code, "currency_name": currency_name}
|
|
38
37
|
|
|
39
|
-
|
|
40
38
|
@dlt.resource(
|
|
41
39
|
write_disposition="merge",
|
|
42
40
|
columns={
|
|
@@ -81,7 +79,6 @@ def frankfurter_source(
|
|
|
81
79
|
"base_currency": base_currency,
|
|
82
80
|
}
|
|
83
81
|
|
|
84
|
-
|
|
85
82
|
@dlt.resource(
|
|
86
83
|
write_disposition="merge",
|
|
87
84
|
columns={
|
|
@@ -93,13 +90,13 @@ def frankfurter_source(
|
|
|
93
90
|
primary_key=("date", "currency_code", "base_currency"),
|
|
94
91
|
)
|
|
95
92
|
def exchange_rates(
|
|
96
|
-
date_time
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
93
|
+
date_time=dlt.sources.incremental(
|
|
94
|
+
"date",
|
|
95
|
+
initial_value=start_date,
|
|
96
|
+
end_value=end_date,
|
|
97
|
+
range_start="closed",
|
|
98
|
+
range_end="closed",
|
|
99
|
+
),
|
|
103
100
|
) -> Iterator[dict]:
|
|
104
101
|
"""
|
|
105
102
|
Fetches exchange rates for a specified date range.
|
|
@@ -115,9 +112,9 @@ def frankfurter_source(
|
|
|
115
112
|
end_date = date_time.end_value
|
|
116
113
|
else:
|
|
117
114
|
end_date = pendulum.now()
|
|
118
|
-
|
|
115
|
+
|
|
119
116
|
# Ensure start_date.last_value is a pendulum.DateTime object
|
|
120
|
-
start_date_obj = ensure_pendulum_datetime(start_date)
|
|
117
|
+
start_date_obj = ensure_pendulum_datetime(start_date) # type: ignore
|
|
121
118
|
start_date_str = start_date_obj.format("YYYY-MM-DD")
|
|
122
119
|
|
|
123
120
|
# Ensure end_date is a pendulum.DateTime object
|
|
@@ -158,4 +155,3 @@ def frankfurter_source(
|
|
|
158
155
|
}
|
|
159
156
|
|
|
160
157
|
return currencies, latest, exchange_rates
|
|
161
|
-
|
|
@@ -16,9 +16,9 @@ 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) -> 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.")
|
ingestr/src/masking.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import hmac
|
|
3
|
+
import random
|
|
4
|
+
import re
|
|
5
|
+
import string
|
|
6
|
+
import uuid
|
|
7
|
+
from datetime import date, datetime, timedelta
|
|
8
|
+
from typing import Any, Callable, Dict, Optional, Tuple, Union
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MaskingEngine:
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.token_cache: Dict[str, Union[str, int]] = {}
|
|
14
|
+
self.sequential_counter = 0
|
|
15
|
+
|
|
16
|
+
def parse_mask_config(self, config: str) -> Tuple[str, str, Optional[str]]:
|
|
17
|
+
parts = config.split(":")
|
|
18
|
+
if len(parts) == 2:
|
|
19
|
+
return parts[0], parts[1], None
|
|
20
|
+
elif len(parts) == 3:
|
|
21
|
+
return parts[0], parts[1], parts[2]
|
|
22
|
+
else:
|
|
23
|
+
raise ValueError(
|
|
24
|
+
f"Invalid mask configuration: {config}. Expected format: 'column:algorithm[:param]'"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def get_masking_function(
|
|
28
|
+
self, algorithm: str, param: Optional[str] = None
|
|
29
|
+
) -> Callable:
|
|
30
|
+
algorithm = algorithm.lower()
|
|
31
|
+
|
|
32
|
+
# Hash-based masking
|
|
33
|
+
if algorithm == "hash" or algorithm == "sha256":
|
|
34
|
+
return self._hash_sha256
|
|
35
|
+
elif algorithm == "md5":
|
|
36
|
+
return self._hash_md5
|
|
37
|
+
elif algorithm == "hmac":
|
|
38
|
+
return lambda x: self._hash_hmac(x, param or "default-key")
|
|
39
|
+
|
|
40
|
+
# Format-preserving masking
|
|
41
|
+
elif algorithm == "email":
|
|
42
|
+
return self._mask_email
|
|
43
|
+
elif algorithm == "phone":
|
|
44
|
+
return self._mask_phone
|
|
45
|
+
elif algorithm == "credit_card":
|
|
46
|
+
return self._mask_credit_card
|
|
47
|
+
elif algorithm == "ssn":
|
|
48
|
+
return self._mask_ssn
|
|
49
|
+
|
|
50
|
+
# Redaction strategies
|
|
51
|
+
elif algorithm == "redact":
|
|
52
|
+
return lambda x: "REDACTED"
|
|
53
|
+
elif algorithm == "stars":
|
|
54
|
+
return lambda x: "*" * len(str(x)) if x else ""
|
|
55
|
+
elif algorithm == "fixed":
|
|
56
|
+
return lambda x: param or "MASKED"
|
|
57
|
+
elif algorithm == "random":
|
|
58
|
+
return self._random_replace
|
|
59
|
+
|
|
60
|
+
# Partial masking
|
|
61
|
+
elif algorithm == "partial":
|
|
62
|
+
chars = int(param) if param else 2
|
|
63
|
+
return lambda x: self._partial_mask(x, chars)
|
|
64
|
+
elif algorithm == "first_letter":
|
|
65
|
+
return self._first_letter_mask
|
|
66
|
+
|
|
67
|
+
# Tokenization
|
|
68
|
+
elif algorithm == "uuid":
|
|
69
|
+
return self._tokenize_uuid
|
|
70
|
+
elif algorithm == "sequential":
|
|
71
|
+
return self._tokenize_sequential
|
|
72
|
+
|
|
73
|
+
# Numeric masking
|
|
74
|
+
elif algorithm == "round":
|
|
75
|
+
precision = int(param) if param else 10
|
|
76
|
+
return lambda x: self._round_number(x, precision)
|
|
77
|
+
elif algorithm == "range":
|
|
78
|
+
bucket_size = int(param) if param else 100
|
|
79
|
+
return lambda x: self._range_mask(x, bucket_size)
|
|
80
|
+
elif algorithm == "noise":
|
|
81
|
+
noise_level = float(param) if param else 0.1
|
|
82
|
+
return lambda x: self._add_noise(x, noise_level)
|
|
83
|
+
|
|
84
|
+
# Date masking
|
|
85
|
+
elif algorithm == "date_shift":
|
|
86
|
+
max_days = int(param) if param else 30
|
|
87
|
+
return lambda x: self._date_shift(x, max_days)
|
|
88
|
+
elif algorithm == "year_only":
|
|
89
|
+
return self._year_only
|
|
90
|
+
elif algorithm == "month_year":
|
|
91
|
+
return self._month_year
|
|
92
|
+
|
|
93
|
+
else:
|
|
94
|
+
raise ValueError(f"Unknown masking algorithm: {algorithm}")
|
|
95
|
+
|
|
96
|
+
# Hash functions
|
|
97
|
+
def _hash_sha256(self, value: Any) -> Optional[str]:
|
|
98
|
+
if value is None:
|
|
99
|
+
return None
|
|
100
|
+
return hashlib.sha256(str(value).encode()).hexdigest()
|
|
101
|
+
|
|
102
|
+
def _hash_md5(self, value: Any) -> Optional[str]:
|
|
103
|
+
if value is None:
|
|
104
|
+
return None
|
|
105
|
+
return hashlib.md5(str(value).encode()).hexdigest()
|
|
106
|
+
|
|
107
|
+
def _hash_hmac(self, value: Any, key: str) -> Optional[str]:
|
|
108
|
+
if value is None:
|
|
109
|
+
return None
|
|
110
|
+
return hmac.new(key.encode(), str(value).encode(), hashlib.sha256).hexdigest()
|
|
111
|
+
|
|
112
|
+
# Format-preserving masks
|
|
113
|
+
def _mask_email(self, value: Any) -> Any:
|
|
114
|
+
if value is None or not value:
|
|
115
|
+
return value
|
|
116
|
+
email_str = str(value)
|
|
117
|
+
if "@" not in email_str:
|
|
118
|
+
return self._partial_mask(email_str, 2)
|
|
119
|
+
|
|
120
|
+
local, domain = email_str.split("@", 1)
|
|
121
|
+
if len(local) <= 2:
|
|
122
|
+
masked_local = "*" * len(local)
|
|
123
|
+
else:
|
|
124
|
+
masked_local = local[0] + "*" * (len(local) - 2) + local[-1]
|
|
125
|
+
return f"{masked_local}@{domain}"
|
|
126
|
+
|
|
127
|
+
def _mask_phone(self, value: Any) -> Any:
|
|
128
|
+
if value is None or not value:
|
|
129
|
+
return value
|
|
130
|
+
phone_str = re.sub(r"\D", "", str(value))
|
|
131
|
+
if len(phone_str) < 10:
|
|
132
|
+
return "*" * len(phone_str)
|
|
133
|
+
|
|
134
|
+
# Keep country code and area code, mask the rest
|
|
135
|
+
if len(phone_str) >= 10:
|
|
136
|
+
return phone_str[:3] + "-***-****"
|
|
137
|
+
return phone_str
|
|
138
|
+
|
|
139
|
+
def _mask_credit_card(self, value: Any) -> Any:
|
|
140
|
+
if value is None or not value:
|
|
141
|
+
return value
|
|
142
|
+
cc_str = re.sub(r"\D", "", str(value))
|
|
143
|
+
if len(cc_str) < 12:
|
|
144
|
+
return "*" * len(cc_str)
|
|
145
|
+
return "*" * (len(cc_str) - 4) + cc_str[-4:]
|
|
146
|
+
|
|
147
|
+
def _mask_ssn(self, value: Any) -> Any:
|
|
148
|
+
if value is None or not value:
|
|
149
|
+
return value
|
|
150
|
+
ssn_str = re.sub(r"\D", "", str(value))
|
|
151
|
+
if len(ssn_str) != 9:
|
|
152
|
+
return "*" * len(ssn_str)
|
|
153
|
+
return "***-**-" + ssn_str[-4:]
|
|
154
|
+
|
|
155
|
+
# Partial masking
|
|
156
|
+
def _partial_mask(self, value: Any, chars_to_show: int) -> Any:
|
|
157
|
+
if value is None or not value:
|
|
158
|
+
return value
|
|
159
|
+
val_str = str(value)
|
|
160
|
+
if len(val_str) <= chars_to_show * 2:
|
|
161
|
+
return "*" * len(val_str)
|
|
162
|
+
return (
|
|
163
|
+
val_str[:chars_to_show]
|
|
164
|
+
+ "*" * (len(val_str) - chars_to_show * 2)
|
|
165
|
+
+ val_str[-chars_to_show:]
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def _first_letter_mask(self, value: Any) -> Any:
|
|
169
|
+
if value is None or not value:
|
|
170
|
+
return value
|
|
171
|
+
val_str = str(value)
|
|
172
|
+
if len(val_str) <= 1:
|
|
173
|
+
return val_str
|
|
174
|
+
return val_str[0] + "*" * (len(val_str) - 1)
|
|
175
|
+
|
|
176
|
+
# Random replacement
|
|
177
|
+
def _random_replace(self, value: Any) -> Any:
|
|
178
|
+
if value is None:
|
|
179
|
+
return value
|
|
180
|
+
|
|
181
|
+
if isinstance(value, (int, float)):
|
|
182
|
+
# Generate random number in similar range
|
|
183
|
+
if isinstance(value, int):
|
|
184
|
+
magnitude = len(str(abs(value)))
|
|
185
|
+
return random.randint(10 ** (magnitude - 1), 10**magnitude - 1)
|
|
186
|
+
else:
|
|
187
|
+
return random.uniform(0, abs(value) * 2)
|
|
188
|
+
elif isinstance(value, str):
|
|
189
|
+
# Generate random string of same length
|
|
190
|
+
return "".join(
|
|
191
|
+
random.choices(string.ascii_letters + string.digits, k=len(value))
|
|
192
|
+
)
|
|
193
|
+
else:
|
|
194
|
+
return str(value)
|
|
195
|
+
|
|
196
|
+
# Tokenization
|
|
197
|
+
def _tokenize_uuid(self, value: Any) -> Optional[str]:
|
|
198
|
+
if value is None:
|
|
199
|
+
return None
|
|
200
|
+
val_str = str(value)
|
|
201
|
+
if val_str not in self.token_cache:
|
|
202
|
+
self.token_cache[val_str] = str(uuid.uuid4())
|
|
203
|
+
return str(self.token_cache[val_str])
|
|
204
|
+
|
|
205
|
+
def _tokenize_sequential(self, value: Any) -> Optional[int]:
|
|
206
|
+
if value is None:
|
|
207
|
+
return None
|
|
208
|
+
val_str = str(value)
|
|
209
|
+
if val_str not in self.token_cache:
|
|
210
|
+
self.sequential_counter += 1
|
|
211
|
+
self.token_cache[val_str] = self.sequential_counter
|
|
212
|
+
return int(self.token_cache[val_str])
|
|
213
|
+
|
|
214
|
+
# Numeric masking
|
|
215
|
+
def _round_number(self, value: Any, precision: int) -> Any:
|
|
216
|
+
if value is None:
|
|
217
|
+
return value
|
|
218
|
+
try:
|
|
219
|
+
num = float(value)
|
|
220
|
+
return round(num / precision) * precision
|
|
221
|
+
except (ValueError, TypeError):
|
|
222
|
+
return value
|
|
223
|
+
|
|
224
|
+
def _range_mask(self, value: Any, bucket_size: int) -> Any:
|
|
225
|
+
if value is None:
|
|
226
|
+
return value
|
|
227
|
+
try:
|
|
228
|
+
num = float(value)
|
|
229
|
+
lower = int(num // bucket_size) * bucket_size
|
|
230
|
+
upper = lower + bucket_size
|
|
231
|
+
return f"{lower}-{upper}"
|
|
232
|
+
except (ValueError, TypeError):
|
|
233
|
+
return value
|
|
234
|
+
|
|
235
|
+
def _add_noise(self, value: Any, noise_level: float) -> Any:
|
|
236
|
+
if value is None:
|
|
237
|
+
return value
|
|
238
|
+
try:
|
|
239
|
+
num = float(value)
|
|
240
|
+
noise = random.uniform(-noise_level, noise_level) * abs(num)
|
|
241
|
+
result = num + noise
|
|
242
|
+
if isinstance(value, int):
|
|
243
|
+
return int(result)
|
|
244
|
+
return result
|
|
245
|
+
except (ValueError, TypeError):
|
|
246
|
+
return value
|
|
247
|
+
|
|
248
|
+
# Date masking
|
|
249
|
+
def _date_shift(self, value: Any, max_days: int) -> Any:
|
|
250
|
+
if value is None:
|
|
251
|
+
return value
|
|
252
|
+
|
|
253
|
+
if isinstance(value, (date, datetime)):
|
|
254
|
+
shift_days = random.randint(-max_days, max_days)
|
|
255
|
+
return value + timedelta(days=shift_days)
|
|
256
|
+
|
|
257
|
+
# Try to parse string dates
|
|
258
|
+
try:
|
|
259
|
+
from dateutil import parser # type: ignore
|
|
260
|
+
|
|
261
|
+
dt = parser.parse(str(value))
|
|
262
|
+
shift_days = random.randint(-max_days, max_days)
|
|
263
|
+
result = dt + timedelta(days=shift_days)
|
|
264
|
+
if isinstance(value, str):
|
|
265
|
+
return result.strftime("%Y-%m-%d")
|
|
266
|
+
return result
|
|
267
|
+
except Exception:
|
|
268
|
+
return value
|
|
269
|
+
|
|
270
|
+
def _year_only(self, value: Any) -> Any:
|
|
271
|
+
if value is None:
|
|
272
|
+
return value
|
|
273
|
+
|
|
274
|
+
if isinstance(value, (date, datetime)):
|
|
275
|
+
return value.year
|
|
276
|
+
|
|
277
|
+
# Try to parse string dates
|
|
278
|
+
try:
|
|
279
|
+
from dateutil import parser
|
|
280
|
+
|
|
281
|
+
dt = parser.parse(str(value))
|
|
282
|
+
return dt.year
|
|
283
|
+
except Exception:
|
|
284
|
+
return value
|
|
285
|
+
|
|
286
|
+
def _month_year(self, value: Any) -> Any:
|
|
287
|
+
if value is None:
|
|
288
|
+
return value
|
|
289
|
+
|
|
290
|
+
if isinstance(value, (date, datetime)):
|
|
291
|
+
return f"{value.year}-{value.month:02d}"
|
|
292
|
+
|
|
293
|
+
# Try to parse string dates
|
|
294
|
+
try:
|
|
295
|
+
from dateutil import parser
|
|
296
|
+
|
|
297
|
+
dt = parser.parse(str(value))
|
|
298
|
+
return f"{dt.year}-{dt.month:02d}"
|
|
299
|
+
except Exception:
|
|
300
|
+
return value
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def create_masking_mapper(mask_configs: list[str]) -> Callable:
|
|
304
|
+
engine = MaskingEngine()
|
|
305
|
+
|
|
306
|
+
# Parse all configurations
|
|
307
|
+
masks = {}
|
|
308
|
+
for config in mask_configs:
|
|
309
|
+
column, algorithm, param = engine.parse_mask_config(config)
|
|
310
|
+
masks[column] = engine.get_masking_function(algorithm, param)
|
|
311
|
+
|
|
312
|
+
def apply_masks(data: Any) -> Any:
|
|
313
|
+
# Handle PyArrow tables
|
|
314
|
+
try:
|
|
315
|
+
import pyarrow as pa # type: ignore
|
|
316
|
+
|
|
317
|
+
if isinstance(data, pa.Table):
|
|
318
|
+
# Convert to pandas for easier manipulation
|
|
319
|
+
df = data.to_pandas()
|
|
320
|
+
|
|
321
|
+
# Apply masks to each column
|
|
322
|
+
for column, mask_func in masks.items():
|
|
323
|
+
if column in df.columns:
|
|
324
|
+
df[column] = df[column].apply(mask_func)
|
|
325
|
+
|
|
326
|
+
# Convert back to PyArrow table
|
|
327
|
+
return pa.Table.from_pandas(df)
|
|
328
|
+
except ImportError:
|
|
329
|
+
pass
|
|
330
|
+
|
|
331
|
+
# Handle dictionaries (original behavior)
|
|
332
|
+
if isinstance(data, dict):
|
|
333
|
+
for column, mask_func in masks.items():
|
|
334
|
+
if column in data:
|
|
335
|
+
try:
|
|
336
|
+
data[column] = mask_func(data[column])
|
|
337
|
+
except Exception as e:
|
|
338
|
+
print(f"Warning: Failed to mask column {column}: {e}")
|
|
339
|
+
return data
|
|
340
|
+
|
|
341
|
+
# Return as-is if not a supported type
|
|
342
|
+
return data
|
|
343
|
+
|
|
344
|
+
return apply_masks
|
ingestr/src/mongodb/helpers.py
CHANGED
|
@@ -520,20 +520,24 @@ class CollectionAggregationLoader(CollectionLoader):
|
|
|
520
520
|
|
|
521
521
|
# Add maxTimeMS to prevent hanging
|
|
522
522
|
cursor = self.collection.aggregate(
|
|
523
|
-
pipeline,
|
|
524
|
-
allowDiskUse=True,
|
|
523
|
+
pipeline,
|
|
524
|
+
allowDiskUse=True,
|
|
525
525
|
batchSize=min(self.chunk_size, 101),
|
|
526
|
-
maxTimeMS=30000 # 30 second timeout
|
|
526
|
+
maxTimeMS=30000, # 30 second timeout
|
|
527
527
|
)
|
|
528
|
-
|
|
528
|
+
|
|
529
529
|
docs_buffer = []
|
|
530
530
|
try:
|
|
531
531
|
for doc in cursor:
|
|
532
532
|
docs_buffer.append(doc)
|
|
533
|
-
|
|
533
|
+
|
|
534
534
|
if len(docs_buffer) >= self.chunk_size:
|
|
535
535
|
res = map_nested_in_place(convert_mongo_objs, docs_buffer)
|
|
536
|
-
if
|
|
536
|
+
if (
|
|
537
|
+
len(res) > 0
|
|
538
|
+
and "_id" in res[0]
|
|
539
|
+
and isinstance(res[0]["_id"], dict)
|
|
540
|
+
):
|
|
537
541
|
yield dlt.mark.with_hints(
|
|
538
542
|
res,
|
|
539
543
|
dlt.mark.make_hints(columns={"_id": {"data_type": "json"}}),
|
|
@@ -541,7 +545,7 @@ class CollectionAggregationLoader(CollectionLoader):
|
|
|
541
545
|
else:
|
|
542
546
|
yield res
|
|
543
547
|
docs_buffer = []
|
|
544
|
-
|
|
548
|
+
|
|
545
549
|
# Yield any remaining documents
|
|
546
550
|
if docs_buffer:
|
|
547
551
|
res = map_nested_in_place(convert_mongo_objs, docs_buffer)
|
|
@@ -8,12 +8,11 @@ from .helpers import (
|
|
|
8
8
|
_make_request,
|
|
9
9
|
_paginate,
|
|
10
10
|
convert_timestamps_to_iso,
|
|
11
|
-
process_customer_with_nested_resources_async,
|
|
12
11
|
create_project_resource,
|
|
12
|
+
process_customer_with_nested_resources_async,
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
|
|
17
16
|
@dlt.source(name="revenuecat", max_table_nesting=0)
|
|
18
17
|
def revenuecat_source(
|
|
19
18
|
api_key: str,
|
|
@@ -90,13 +89,14 @@ def revenuecat_source(
|
|
|
90
89
|
# Create project-dependent resources dynamically
|
|
91
90
|
project_resources = []
|
|
92
91
|
resource_names = ["products", "entitlements", "offerings"]
|
|
93
|
-
|
|
92
|
+
|
|
94
93
|
for resource_name in resource_names:
|
|
94
|
+
|
|
95
95
|
@dlt.resource(name=resource_name, primary_key="id", write_disposition="merge")
|
|
96
96
|
def create_resource(resource_name=resource_name) -> Iterator[Dict[str, Any]]:
|
|
97
97
|
"""Get list of project resource."""
|
|
98
98
|
yield from create_project_resource(resource_name, api_key, project_id)
|
|
99
|
-
|
|
99
|
+
|
|
100
100
|
# Set the function name for better identification
|
|
101
101
|
create_resource.__name__ = resource_name
|
|
102
102
|
project_resources.append(create_resource)
|
|
@@ -270,22 +270,22 @@ def create_project_resource(
|
|
|
270
270
|
) -> Iterator[Dict[str, Any]]:
|
|
271
271
|
"""
|
|
272
272
|
Helper function to create DLT resources for project-dependent endpoints.
|
|
273
|
-
|
|
273
|
+
|
|
274
274
|
Args:
|
|
275
275
|
resource_name: Name of the resource (e.g., 'products', 'entitlements', 'offerings')
|
|
276
276
|
api_key: RevenueCat API key
|
|
277
277
|
project_id: RevenueCat project ID
|
|
278
278
|
timestamp_fields: List of timestamp fields to convert to ISO format
|
|
279
|
-
|
|
279
|
+
|
|
280
280
|
Returns:
|
|
281
281
|
Iterator of resource data
|
|
282
282
|
"""
|
|
283
283
|
if project_id is None:
|
|
284
284
|
raise ValueError(f"project_id is required for {resource_name} resource")
|
|
285
|
-
|
|
285
|
+
|
|
286
286
|
endpoint = f"/projects/{project_id}/{resource_name}"
|
|
287
287
|
default_timestamp_fields = timestamp_fields or ["created_at", "updated_at"]
|
|
288
|
-
|
|
288
|
+
|
|
289
289
|
for item in _paginate(api_key, endpoint):
|
|
290
290
|
item = convert_timestamps_to_iso(item, default_timestamp_fields)
|
|
291
291
|
yield item
|
|
@@ -13,6 +13,7 @@ def salesforce_source(
|
|
|
13
13
|
username: str,
|
|
14
14
|
password: str,
|
|
15
15
|
token: str,
|
|
16
|
+
domain: str,
|
|
16
17
|
) -> Iterable[DltResource]:
|
|
17
18
|
"""
|
|
18
19
|
Retrieves data from Salesforce using the Salesforce API.
|
|
@@ -26,7 +27,7 @@ def salesforce_source(
|
|
|
26
27
|
DltResource: Data resources from Salesforce.
|
|
27
28
|
"""
|
|
28
29
|
|
|
29
|
-
client = Salesforce(username, password, token)
|
|
30
|
+
client = Salesforce(username, password, token, domain=domain)
|
|
30
31
|
|
|
31
32
|
# define resources
|
|
32
33
|
@dlt.resource(write_disposition="replace")
|
|
@@ -37,7 +38,7 @@ def salesforce_source(
|
|
|
37
38
|
def user_role() -> Iterable[TDataItem]:
|
|
38
39
|
yield get_records(client, "UserRole")
|
|
39
40
|
|
|
40
|
-
@dlt.resource(write_disposition="merge")
|
|
41
|
+
@dlt.resource(write_disposition="merge", primary_key="id")
|
|
41
42
|
def opportunity(
|
|
42
43
|
last_timestamp: incremental[str] = dlt.sources.incremental(
|
|
43
44
|
"SystemModstamp", initial_value=None
|
|
@@ -47,7 +48,7 @@ def salesforce_source(
|
|
|
47
48
|
client, "Opportunity", last_timestamp.last_value, "SystemModstamp"
|
|
48
49
|
)
|
|
49
50
|
|
|
50
|
-
@dlt.resource(write_disposition="merge")
|
|
51
|
+
@dlt.resource(write_disposition="merge", primary_key="id")
|
|
51
52
|
def opportunity_line_item(
|
|
52
53
|
last_timestamp: incremental[str] = dlt.sources.incremental(
|
|
53
54
|
"SystemModstamp", initial_value=None
|
|
@@ -57,7 +58,7 @@ def salesforce_source(
|
|
|
57
58
|
client, "OpportunityLineItem", last_timestamp.last_value, "SystemModstamp"
|
|
58
59
|
)
|
|
59
60
|
|
|
60
|
-
@dlt.resource(write_disposition="merge")
|
|
61
|
+
@dlt.resource(write_disposition="merge", primary_key="id")
|
|
61
62
|
def opportunity_contact_role(
|
|
62
63
|
last_timestamp: incremental[str] = dlt.sources.incremental(
|
|
63
64
|
"SystemModstamp", initial_value=None
|
|
@@ -70,7 +71,7 @@ def salesforce_source(
|
|
|
70
71
|
"SystemModstamp",
|
|
71
72
|
)
|
|
72
73
|
|
|
73
|
-
@dlt.resource(write_disposition="merge")
|
|
74
|
+
@dlt.resource(write_disposition="merge", primary_key="id")
|
|
74
75
|
def account(
|
|
75
76
|
last_timestamp: incremental[str] = dlt.sources.incremental(
|
|
76
77
|
"LastModifiedDate", initial_value=None
|
|
@@ -92,7 +93,7 @@ def salesforce_source(
|
|
|
92
93
|
def campaign() -> Iterable[TDataItem]:
|
|
93
94
|
yield get_records(client, "Campaign")
|
|
94
95
|
|
|
95
|
-
@dlt.resource(write_disposition="merge")
|
|
96
|
+
@dlt.resource(write_disposition="merge", primary_key="id")
|
|
96
97
|
def campaign_member(
|
|
97
98
|
last_timestamp: incremental[str] = dlt.sources.incremental(
|
|
98
99
|
"SystemModstamp", initial_value=None
|
|
@@ -114,7 +115,7 @@ def salesforce_source(
|
|
|
114
115
|
def pricebook_entry() -> Iterable[TDataItem]:
|
|
115
116
|
yield get_records(client, "PricebookEntry")
|
|
116
117
|
|
|
117
|
-
@dlt.resource(write_disposition="merge")
|
|
118
|
+
@dlt.resource(write_disposition="merge", primary_key="id")
|
|
118
119
|
def task(
|
|
119
120
|
last_timestamp: incremental[str] = dlt.sources.incremental(
|
|
120
121
|
"SystemModstamp", initial_value=None
|
|
@@ -122,7 +123,7 @@ def salesforce_source(
|
|
|
122
123
|
) -> Iterable[TDataItem]:
|
|
123
124
|
yield get_records(client, "Task", last_timestamp.last_value, "SystemModstamp")
|
|
124
125
|
|
|
125
|
-
@dlt.resource(write_disposition="merge")
|
|
126
|
+
@dlt.resource(write_disposition="merge", primary_key="id")
|
|
126
127
|
def event(
|
|
127
128
|
last_timestamp: incremental[str] = dlt.sources.incremental(
|
|
128
129
|
"SystemModstamp", initial_value=None
|
ingestr/src/sources.py
CHANGED
|
@@ -2515,6 +2515,7 @@ class SalesforceSource:
|
|
|
2515
2515
|
"username": params.get("username", [None])[0],
|
|
2516
2516
|
"password": params.get("password", [None])[0],
|
|
2517
2517
|
"token": params.get("token", [None])[0],
|
|
2518
|
+
"domain": params.get("domain", [None])[0],
|
|
2518
2519
|
}
|
|
2519
2520
|
for k, v in creds.items():
|
|
2520
2521
|
if v is None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ingestr
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.89
|
|
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
|
|
@@ -42,7 +42,7 @@ Requires-Dist: confluent-kafka==2.8.0
|
|
|
42
42
|
Requires-Dist: crate==2.0.0
|
|
43
43
|
Requires-Dist: cryptography==44.0.2
|
|
44
44
|
Requires-Dist: curlify==2.2.1
|
|
45
|
-
Requires-Dist: databricks-sql-connector==
|
|
45
|
+
Requires-Dist: databricks-sql-connector==4.0.5
|
|
46
46
|
Requires-Dist: databricks-sqlalchemy==1.0.2
|
|
47
47
|
Requires-Dist: dataclasses-json==0.6.7
|
|
48
48
|
Requires-Dist: decorator==5.2.1
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
ingestr/conftest.py,sha256=OE2yxeTCosS9CUFVuqNypm-2ftYvVBeeq7egm3878cI,1981
|
|
2
|
-
ingestr/main.py,sha256=
|
|
2
|
+
ingestr/main.py,sha256=qo0g3wCFl8a_1jUwXagX8L1Q8PKKQlTF7md9pfnzW0Y,27155
|
|
3
3
|
ingestr/src/.gitignore,sha256=8cX1AZTSI0TcdZFGTmS_oyBjpfCzhOEt0DdAo2dFIY8,203
|
|
4
4
|
ingestr/src/blob.py,sha256=UUWMjHUuoR9xP1XZQ6UANQmnMVyDx3d0X4-2FQC271I,2138
|
|
5
|
-
ingestr/src/buildinfo.py,sha256=
|
|
6
|
-
ingestr/src/destinations.py,sha256=
|
|
5
|
+
ingestr/src/buildinfo.py,sha256=YfKg385xjqfTlxcN3drjtO_64R31p1f28goiV70TrQY,21
|
|
6
|
+
ingestr/src/destinations.py,sha256=QNT2rm91cZmY1_Zyj4VnbI14qGmZOUQOQUg9xUTVVYs,23799
|
|
7
7
|
ingestr/src/errors.py,sha256=Ufs4_DfE77_E3vnA1fOQdi6cmuLVNm7_SbFLkL1XPGk,686
|
|
8
8
|
ingestr/src/factory.py,sha256=hC5E_XgrgTHMqwqPc6ihUYvRGTGMTzdPfQhrgPyD0tY,6945
|
|
9
|
-
ingestr/src/filters.py,sha256=
|
|
9
|
+
ingestr/src/filters.py,sha256=0n0sNAVG_f-B_1r7lW5iNtw9z_G1bxWzPaiL1i6tnbU,1665
|
|
10
10
|
ingestr/src/http_client.py,sha256=bxqsk6nJNXCo-79gW04B53DQO-yr25vaSsqP0AKtjx4,732
|
|
11
11
|
ingestr/src/loader.py,sha256=9NaWAyfkXdqAZSS-N72Iwo36Lbx4PyqIfaaH1dNdkFs,1712
|
|
12
|
+
ingestr/src/masking.py,sha256=VN0LdfvExhQ1bZMRylGtaBUIoH-vjuIUmRnYKwo3yiY,11358
|
|
12
13
|
ingestr/src/partition.py,sha256=BrIP6wFJvyR7Nus_3ElnfxknUXeCipK_E_bB8kZowfc,969
|
|
13
14
|
ingestr/src/resource.py,sha256=ZqmZxFQVGlF8rFPhBiUB08HES0yoTj8sZ--jKfaaVps,1164
|
|
14
|
-
ingestr/src/sources.py,sha256=
|
|
15
|
+
ingestr/src/sources.py,sha256=MM_-6ZmIwFLS_L4kBkwJJc0XDyjDyHUkxMMnQaRfuRA,125176
|
|
15
16
|
ingestr/src/table_definition.py,sha256=REbAbqdlmUMUuRh8nEQRreWjPVOQ5ZcfqGkScKdCrmk,390
|
|
16
17
|
ingestr/src/time.py,sha256=H_Fk2J4ShXyUM-EMY7MqCLZQhlnZMZvO952bmZPc4yE,254
|
|
17
18
|
ingestr/src/version.py,sha256=J_2xgZ0mKlvuHcjdKCx2nlioneLH0I47JiU_Slr_Nwc,189
|
|
18
19
|
ingestr/src/adjust/__init__.py,sha256=ULjtJqrNS6XDvUyGl0tjl12-tLyXlCgeFe2icTbtu3Q,3255
|
|
19
20
|
ingestr/src/adjust/adjust_helpers.py,sha256=IHSS94A7enOWkZ8cP5iW3RdYt0Xl3qZGAmDc1Xy4qkI,3802
|
|
20
21
|
ingestr/src/airtable/__init__.py,sha256=XzRsS39xszUlh_s7P1_zq5v8vLfjz3m-NtTPaa8TTZU,2818
|
|
21
|
-
ingestr/src/applovin/__init__.py,sha256=
|
|
22
|
+
ingestr/src/applovin/__init__.py,sha256=Y02ysL2vRiDoP3uN9ven2OVcH9zTd8PbmIjqIHds4zU,6985
|
|
22
23
|
ingestr/src/applovin_max/__init__.py,sha256=fxXqsIibJarp5NOGe08G964HftwLDymTtYS_LqPJht4,3315
|
|
23
24
|
ingestr/src/appsflyer/__init__.py,sha256=QoK-B3cYYMD3bqzQaLWNH6FkJyjRbzRkBF2n6urxubs,8071
|
|
24
25
|
ingestr/src/appsflyer/client.py,sha256=E6xPW4KlbBnQZ0K4eq2Xgb3AmGrtrzIX9bX8EnQr-D4,3615
|
|
@@ -28,7 +29,7 @@ ingestr/src/appstore/errors.py,sha256=KVpPWth5qlv6_QWEm3aJAt3cdf6miPJs0UDzxknx2M
|
|
|
28
29
|
ingestr/src/appstore/models.py,sha256=tW1JSATHBIxZ6a77-RTCBQptJk6iRC8fWcmx4NW7SVA,1716
|
|
29
30
|
ingestr/src/appstore/resources.py,sha256=DJxnNrBohVV0uSeruGV-N_e7UHSlhMhjhYNYdBuqECU,5375
|
|
30
31
|
ingestr/src/arrow/__init__.py,sha256=8fEntgHseKjFMiPQIzxYzw_raicNsEgnveLi1IzBca0,2848
|
|
31
|
-
ingestr/src/asana_source/__init__.py,sha256=
|
|
32
|
+
ingestr/src/asana_source/__init__.py,sha256=p9p89e62Qd3YmrrCCkIclswciSX51pBOMCuT7Ukeq2I,8184
|
|
32
33
|
ingestr/src/asana_source/helpers.py,sha256=PukcdDQWIGqnGxuuobbLw4hUy4-t6gxXg_XywR7Lg9M,375
|
|
33
34
|
ingestr/src/asana_source/settings.py,sha256=-2tpdkwh04RvLKFvwQodnFLYn9MaxOO1hsebGnDQMTU,2829
|
|
34
35
|
ingestr/src/attio/__init__.py,sha256=CLejJjp5vGkt6r18nfNNZ-Xjc1SZgQ5IlcBW5XFQR90,3243
|
|
@@ -46,13 +47,13 @@ ingestr/src/facebook_ads/exceptions.py,sha256=4Nlbc0Mv3i5g-9AoyT-n1PIa8IDi3VCTfE
|
|
|
46
47
|
ingestr/src/facebook_ads/helpers.py,sha256=c-WG008yU_zIdhFwljtqE2jfjVYuaVoNKldxcnJN3U4,9761
|
|
47
48
|
ingestr/src/facebook_ads/settings.py,sha256=Bsic8RcmH-NfEZ7r_NGospTCmwISK9XaMT5y2NZirtg,4938
|
|
48
49
|
ingestr/src/facebook_ads/utils.py,sha256=ES2ylPoW3j3fjp6OMUgp21n1cG1OktXsmWWMk5vBW_I,1590
|
|
49
|
-
ingestr/src/filesystem/__init__.py,sha256=
|
|
50
|
+
ingestr/src/filesystem/__init__.py,sha256=42YAOHQxZ7TkTXC1eeaLUJpjqJ3l7DH7C8j927pV4pc,4353
|
|
50
51
|
ingestr/src/filesystem/helpers.py,sha256=bg0muSHZr3hMa8H4jN2-LGWzI-SUoKlQNiWJ74-YYms,3211
|
|
51
52
|
ingestr/src/filesystem/readers.py,sha256=a0fKkaRpnAOGsXI3EBNYZa7x6tlmAOsgRzb883StY30,3987
|
|
52
53
|
ingestr/src/fluxx/__init__.py,sha256=Ei8BE0KAEzpadJT9RO5-8zMA7LvnIPhNPDKF4EyBcLo,328980
|
|
53
54
|
ingestr/src/fluxx/helpers.py,sha256=dCNgvMMTSEO4LNp6luNZ-XrV4NPW-_OUfmp0k3jFhuc,6602
|
|
54
|
-
ingestr/src/frankfurter/__init__.py,sha256=
|
|
55
|
-
ingestr/src/frankfurter/helpers.py,sha256=
|
|
55
|
+
ingestr/src/frankfurter/__init__.py,sha256=aeyiv1jwcwblV5OeqG81vFcJo_Wc1bUlDwzdE4gnQiw,5246
|
|
56
|
+
ingestr/src/frankfurter/helpers.py,sha256=SpRr992OcSf7IDI5y-ToUdO6m6sGpqFz59LTY0ojchI,1502
|
|
56
57
|
ingestr/src/freshdesk/__init__.py,sha256=ukyorgCNsW_snzsYBDsr3Q0WB8f-to9Fk0enqHHFQlk,3087
|
|
57
58
|
ingestr/src/freshdesk/freshdesk_client.py,sha256=1nFf0K4MQ0KZbWwk4xSbYHaykVqmPLfN39miOFDpWVc,4385
|
|
58
59
|
ingestr/src/freshdesk/settings.py,sha256=0Wr_OMnUZcTlry7BmALssLxD2yh686JW4moLNv12Jnw,409
|
|
@@ -95,7 +96,7 @@ ingestr/src/linkedin_ads/helpers.py,sha256=eUWudRVlXl4kqIhfXQ1eVsUpZwJn7UFqKSpnb
|
|
|
95
96
|
ingestr/src/mixpanel/__init__.py,sha256=s1QtqMP0BTGW6YtdCabJFWj7lEn7KujzELwGpBOQgfs,1796
|
|
96
97
|
ingestr/src/mixpanel/client.py,sha256=c_reouegOVYBOwHLfgYFwpmkba0Sxro1Zkml07NCYf0,3602
|
|
97
98
|
ingestr/src/mongodb/__init__.py,sha256=wu3KJ3VH5FF67gctJqm4T3ZTdBOQam1u6xuFBohq7bs,7486
|
|
98
|
-
ingestr/src/mongodb/helpers.py,sha256=
|
|
99
|
+
ingestr/src/mongodb/helpers.py,sha256=TmEbQ-Rz5ajxmaMgZa7nrI13-L7Z_ClbFCFPnmPIrgE,31739
|
|
99
100
|
ingestr/src/notion/__init__.py,sha256=36wUui8finbc85ObkRMq8boMraXMUehdABN_AMe_hzA,1834
|
|
100
101
|
ingestr/src/notion/settings.py,sha256=MwQVZViJtnvOegfjXYc_pJ50oUYgSRPgwqu7TvpeMOA,82
|
|
101
102
|
ingestr/src/notion/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -113,9 +114,9 @@ ingestr/src/pipedrive/helpers/__init__.py,sha256=UX1K_qnGXB0ShtnBOfp2XuVbK8RRoCK
|
|
|
113
114
|
ingestr/src/pipedrive/helpers/custom_fields_munger.py,sha256=rZ4AjdITHfJE2NNomCR7vMBS1KnWpEGVF6fADwsIHUE,4488
|
|
114
115
|
ingestr/src/pipedrive/helpers/pages.py,sha256=Klpjw2OnMuhzit3PpiHKsfzGcJ3rQPSQBl3HhE3-6eA,3358
|
|
115
116
|
ingestr/src/quickbooks/__init__.py,sha256=cZUuVCOTGPHTscRj6i0DytO63_fWF-4ieMxoU4PcyTg,3727
|
|
116
|
-
ingestr/src/revenuecat/__init__.py,sha256=
|
|
117
|
-
ingestr/src/revenuecat/helpers.py,sha256=
|
|
118
|
-
ingestr/src/salesforce/__init__.py,sha256=
|
|
117
|
+
ingestr/src/revenuecat/__init__.py,sha256=5HbyZuEOekkbeeT72sM_bnGygSyYdmd_vczfAUz7xoM,4029
|
|
118
|
+
ingestr/src/revenuecat/helpers.py,sha256=CYU6l79kplnfL87GfdxyGeEBrBSWEZfGP0GyjPHuVDk,9619
|
|
119
|
+
ingestr/src/salesforce/__init__.py,sha256=HVHY8pDngB498B6g6KDzwq-q2KPU4PxuEd9Y_8tDDFs,4716
|
|
119
120
|
ingestr/src/salesforce/helpers.py,sha256=QTdazBt-qRTBbCQMZnyclIaDQFmBixBy_RDKD00Lt-8,2492
|
|
120
121
|
ingestr/src/shopify/__init__.py,sha256=RzSSG93g-Qlkz6TAxi1XasFDdxxtVXIo53ZTtjGczW4,62602
|
|
121
122
|
ingestr/src/shopify/exceptions.py,sha256=BhV3lIVWeBt8Eh4CWGW_REFJpGCzvW6-62yZrBWa3nQ,50
|
|
@@ -157,8 +158,8 @@ ingestr/testdata/merge_expected.csv,sha256=DReHqWGnQMsf2PBv_Q2pfjsgvikYFnf1zYcQZ
|
|
|
157
158
|
ingestr/testdata/merge_part1.csv,sha256=Pw8Z9IDKcNU0qQHx1z6BUf4rF_-SxKGFOvymCt4OY9I,185
|
|
158
159
|
ingestr/testdata/merge_part2.csv,sha256=T_GiWxA81SN63_tMOIuemcvboEFeAmbKc7xRXvL9esw,287
|
|
159
160
|
ingestr/tests/unit/test_smartsheets.py,sha256=eiC2CCO4iNJcuN36ONvqmEDryCA1bA1REpayHpu42lk,5058
|
|
160
|
-
ingestr-0.13.
|
|
161
|
-
ingestr-0.13.
|
|
162
|
-
ingestr-0.13.
|
|
163
|
-
ingestr-0.13.
|
|
164
|
-
ingestr-0.13.
|
|
161
|
+
ingestr-0.13.89.dist-info/METADATA,sha256=EfmN2TdrNG9oBfU-U78YNWe9tSZr6smMS5kynvFUBZE,15182
|
|
162
|
+
ingestr-0.13.89.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
163
|
+
ingestr-0.13.89.dist-info/entry_points.txt,sha256=oPJy0KBnPWYjDtP1k8qwAihcTLHSZokSQvRAw_wtfJM,46
|
|
164
|
+
ingestr-0.13.89.dist-info/licenses/LICENSE.md,sha256=cW8wIhn8HFE-KLStDF9jHQ1O_ARWP3kTpk_-eOccL24,1075
|
|
165
|
+
ingestr-0.13.89.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|