ingestr 0.13.52__py3-none-any.whl → 0.13.54__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/src/buildinfo.py CHANGED
@@ -1 +1 @@
1
- version = "v0.13.52"
1
+ version = "v0.13.54"
@@ -476,7 +476,22 @@ class SqliteDestination(GenericSqlDestination):
476
476
 
477
477
  def dlt_run_params(self, uri: str, table: str, **kwargs):
478
478
  return {
479
- #https://dlthub.com/docs/dlt-ecosystem/destinations/sqlalchemy#dataset-files
479
+ # https://dlthub.com/docs/dlt-ecosystem/destinations/sqlalchemy#dataset-files
480
480
  "dataset_name": "main",
481
481
  "table_name": table,
482
482
  }
483
+
484
+
485
+ class MySqlDestination(GenericSqlDestination):
486
+ def dlt_dest(self, uri: str, **kwargs):
487
+ return dlt.destinations.sqlalchemy(credentials=uri)
488
+
489
+ def dlt_run_params(self, uri: str, table: str, **kwargs):
490
+ parsed = urlparse(uri)
491
+ database = parsed.path.lstrip("/")
492
+ if not database:
493
+ raise ValueError("You need to specify a database")
494
+ return {
495
+ "dataset_name": database,
496
+ "table_name": table,
497
+ }
ingestr/src/factory.py CHANGED
@@ -11,6 +11,7 @@ from ingestr.src.destinations import (
11
11
  DatabricksDestination,
12
12
  DuckDBDestination,
13
13
  MsSQLDestination,
14
+ MySqlDestination,
14
15
  PostgresDestination,
15
16
  RedshiftDestination,
16
17
  S3Destination,
@@ -46,13 +47,16 @@ from ingestr.src.sources import (
46
47
  KlaviyoSource,
47
48
  LinkedInAdsSource,
48
49
  LocalCsvSource,
50
+ MixpanelSource,
49
51
  MongoDbSource,
50
52
  NotionSource,
51
53
  PersonioSource,
52
54
  PhantombusterSource,
53
55
  PipedriveSource,
56
+ QuickBooksSource,
54
57
  S3Source,
55
58
  SalesforceSource,
59
+ SFTPSource,
56
60
  ShopifySource,
57
61
  SlackSource,
58
62
  SmartsheetSource,
@@ -65,6 +69,7 @@ from ingestr.src.sources import (
65
69
 
66
70
  SQL_SOURCE_SCHEMES = [
67
71
  "bigquery",
72
+ "crate",
68
73
  "duckdb",
69
74
  "mssql",
70
75
  "mysql",
@@ -137,6 +142,7 @@ class SourceDestinationFactory:
137
142
  "hubspot": HubspotSource,
138
143
  "airtable": AirtableSource,
139
144
  "klaviyo": KlaviyoSource,
145
+ "mixpanel": MixpanelSource,
140
146
  "appsflyer": AppsflyerSource,
141
147
  "kafka": KafkaSource,
142
148
  "adjust": AdjustSource,
@@ -163,7 +169,9 @@ class SourceDestinationFactory:
163
169
  "elasticsearch": ElasticsearchSource,
164
170
  "attio": AttioSource,
165
171
  "solidgate": SolidgateSource,
172
+ "quickbooks": QuickBooksSource,
166
173
  "smartsheet": SmartsheetSource,
174
+ "sftp": SFTPSource,
167
175
  }
168
176
  destinations: Dict[str, Type[DestinationProtocol]] = {
169
177
  "bigquery": BigQueryDestination,
@@ -184,6 +192,8 @@ class SourceDestinationFactory:
184
192
  "clickhouse": ClickhouseDestination,
185
193
  "s3": S3Destination,
186
194
  "sqlite": SqliteDestination,
195
+ "mysql": MySqlDestination,
196
+ "mysql+pymysql": MySqlDestination,
187
197
  }
188
198
 
189
199
  def __init__(self, source_uri: str, destination_uri: str):
@@ -0,0 +1,62 @@
1
+ from typing import Iterable
2
+
3
+ import dlt
4
+ import pendulum
5
+ from dlt.common.typing import TDataItem
6
+ from dlt.sources import DltResource
7
+
8
+ from .client import MixpanelClient
9
+
10
+
11
+ @dlt.source(max_table_nesting=0)
12
+ def mixpanel_source(
13
+ username: str,
14
+ password: str,
15
+ project_id: str,
16
+ server: str,
17
+ start_date: pendulum.DateTime,
18
+ end_date: pendulum.DateTime | None = None,
19
+ ) -> Iterable[DltResource]:
20
+ client = MixpanelClient(username, password, project_id, server)
21
+
22
+ @dlt.resource(write_disposition="merge", name="events", primary_key="distinct_id")
23
+ def events(
24
+ date=dlt.sources.incremental(
25
+ "time",
26
+ initial_value=start_date.int_timestamp,
27
+ end_value=end_date.int_timestamp if end_date else None,
28
+ range_end="closed",
29
+ range_start="closed",
30
+ ),
31
+ ) -> Iterable[TDataItem]:
32
+ if date.end_value is None:
33
+ end_dt = pendulum.now(tz="UTC")
34
+ else:
35
+ end_dt = pendulum.from_timestamp(date.end_value)
36
+
37
+ start_dt = pendulum.from_timestamp(date.last_value)
38
+
39
+ yield from client.fetch_events(
40
+ start_dt,
41
+ end_dt,
42
+ )
43
+
44
+ @dlt.resource(write_disposition="merge", primary_key="distinct_id", name="profiles")
45
+ def profiles(
46
+ last_seen=dlt.sources.incremental(
47
+ "last_seen",
48
+ initial_value=start_date,
49
+ end_value=end_date,
50
+ range_end="closed",
51
+ range_start="closed",
52
+ ),
53
+ ) -> Iterable[TDataItem]:
54
+ if last_seen.end_value is None:
55
+ end_dt = pendulum.now(tz="UTC")
56
+ else:
57
+ end_dt = last_seen.end_value
58
+
59
+ start_dt = last_seen.last_value
60
+ yield from client.fetch_profiles(start_dt, end_dt)
61
+
62
+ return events, profiles
@@ -0,0 +1,99 @@
1
+ import json
2
+ from typing import Iterable
3
+
4
+ import pendulum
5
+ from dlt.sources.helpers.requests import Client
6
+
7
+
8
+ class MixpanelClient:
9
+ def __init__(self, username: str, password: str, project_id: str, server: str):
10
+ self.username = username
11
+ self.password = password
12
+ self.project_id = project_id
13
+ self.server = server
14
+ self.session = Client(raise_for_status=False).session
15
+
16
+ def fetch_events(
17
+ self, start_date: pendulum.DateTime, end_date: pendulum.DateTime
18
+ ) -> Iterable[dict]:
19
+ if self.server == "us":
20
+ server = "data"
21
+ elif self.server == "in":
22
+ server = "data-in"
23
+ else:
24
+ server = "data-eu"
25
+
26
+ url = f"https://{server}.mixpanel.com/api/2.0/export/"
27
+ params = {
28
+ "project_id": self.project_id,
29
+ "from_date": start_date.format("YYYY-MM-DD"),
30
+ "to_date": end_date.format("YYYY-MM-DD"),
31
+ }
32
+ headers = {
33
+ "accept": "text/plain",
34
+ }
35
+ from requests.auth import HTTPBasicAuth
36
+
37
+ auth = HTTPBasicAuth(self.username, self.password)
38
+ resp = self.session.get(url, params=params, headers=headers, auth=auth)
39
+ resp.raise_for_status()
40
+ for line in resp.iter_lines():
41
+ if line:
42
+ data = json.loads(line.decode())
43
+ if "properties" in data:
44
+ for key, value in data["properties"].items():
45
+ if key.startswith("$"):
46
+ data[key[1:]] = value
47
+ else:
48
+ data[key] = value
49
+ del data["properties"]
50
+ yield data
51
+
52
+ def fetch_profiles(
53
+ self, start_date: pendulum.DateTime, end_date: pendulum.DateTime
54
+ ) -> Iterable[dict]:
55
+ if self.server == "us":
56
+ server = ""
57
+ elif self.server == "in":
58
+ server = "in."
59
+ else:
60
+ server = "eu."
61
+ url = f"https://{server}mixpanel.com/api/query/engage"
62
+ headers = {
63
+ "accept": "application/json",
64
+ "content-type": "application/x-www-form-urlencoded",
65
+ }
66
+ from requests.auth import HTTPBasicAuth
67
+
68
+ auth = HTTPBasicAuth(self.username, self.password)
69
+ page = 0
70
+ session_id = None
71
+ while True:
72
+ params = {"project_id": self.project_id, "page": str(page)}
73
+ if session_id:
74
+ params["session_id"] = session_id
75
+ start_str = start_date.format("YYYY-MM-DDTHH:mm:ss")
76
+ end_str = end_date.format("YYYY-MM-DDTHH:mm:ss")
77
+ where = f'properties["$last_seen"] >= "{start_str}" and properties["$last_seen"] <= "{end_str}"'
78
+ params["where"] = where
79
+ resp = self.session.post(url, params=params, headers=headers, auth=auth)
80
+
81
+ resp.raise_for_status()
82
+ data = resp.json()
83
+
84
+ for result in data.get("results", []):
85
+ for key, value in result["$properties"].items():
86
+ if key.startswith("$"):
87
+ if key == "$last_seen":
88
+ result["last_seen"] = pendulum.parse(value)
89
+ else:
90
+ result[key[1:]] = value
91
+ result["distinct_id"] = result["$distinct_id"]
92
+ del result["$properties"]
93
+ del result["$distinct_id"]
94
+ yield result
95
+ if not data.get("results"):
96
+ break
97
+ session_id = data.get("session_id", session_id)
98
+
99
+ page += 1
@@ -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)
ingestr/src/sources.py CHANGED
@@ -18,6 +18,7 @@ from typing import (
18
18
  )
19
19
  from urllib.parse import ParseResult, parse_qs, quote, urlencode, urlparse
20
20
 
21
+ import fsspec # type: ignore
21
22
  import pendulum
22
23
  from dlt.common.time import ensure_pendulum_datetime
23
24
  from dlt.extract import Incremental
@@ -79,30 +80,6 @@ class SqlSource:
79
80
  if uri.startswith("clickhouse://"):
80
81
  parsed_uri = urlparse(uri)
81
82
 
82
- username = parsed_uri.username
83
- if not username:
84
- raise ValueError(
85
- "A username is required to connect to the ClickHouse database."
86
- )
87
-
88
- password = parsed_uri.password
89
- if not password:
90
- raise ValueError(
91
- "A password is required to authenticate with the ClickHouse database."
92
- )
93
-
94
- host = parsed_uri.hostname
95
- if not host:
96
- raise ValueError(
97
- "The hostname or IP address of the ClickHouse server is required to establish a connection."
98
- )
99
-
100
- port = parsed_uri.port
101
- if not port:
102
- raise ValueError(
103
- "The TCP port of the ClickHouse server is required to establish a connection."
104
- )
105
-
106
83
  query_params = parse_qs(parsed_uri.query)
107
84
 
108
85
  if "http_port" in query_params:
@@ -714,8 +691,6 @@ class StripeAnalyticsSource:
714
691
  endpoint,
715
692
  ],
716
693
  stripe_secret_key=api_key[0],
717
- start_date=kwargs.get("interval_start", None),
718
- end_date=kwargs.get("interval_end", None),
719
694
  ).with_resources(endpoint)
720
695
 
721
696
  elif table in INCREMENTAL_ENDPOINTS:
@@ -988,6 +963,57 @@ class KlaviyoSource:
988
963
  ).with_resources(resource)
989
964
 
990
965
 
966
+ class MixpanelSource:
967
+ def handles_incrementality(self) -> bool:
968
+ return True
969
+
970
+ def dlt_source(self, uri: str, table: str, **kwargs):
971
+ if kwargs.get("incremental_key"):
972
+ raise ValueError(
973
+ "Mixpanel takes care of incrementality on its own, you should not provide incremental_key"
974
+ )
975
+
976
+ parsed = urlparse(uri)
977
+ params = parse_qs(parsed.query)
978
+ username = params.get("username")
979
+ password = params.get("password")
980
+ project_id = params.get("project_id")
981
+ server = params.get("server", ["eu"])
982
+
983
+ if not username or not password or not project_id:
984
+ raise ValueError(
985
+ "username, password, project_id are required to connect to Mixpanel"
986
+ )
987
+
988
+ if table not in ["events", "profiles"]:
989
+ raise ValueError(
990
+ f"Resource '{table}' is not supported for Mixpanel source yet, if you are interested in it please create a GitHub issue at https://github.com/bruin-data/ingestr"
991
+ )
992
+
993
+ start_date = kwargs.get("interval_start")
994
+ if start_date:
995
+ start_date = ensure_pendulum_datetime(start_date).in_timezone("UTC")
996
+ else:
997
+ start_date = pendulum.datetime(2020, 1, 1).in_timezone("UTC")
998
+
999
+ end_date = kwargs.get("interval_end")
1000
+ if end_date:
1001
+ end_date = ensure_pendulum_datetime(end_date).in_timezone("UTC")
1002
+ else:
1003
+ end_date = pendulum.now().in_timezone("UTC")
1004
+
1005
+ from ingestr.src.mixpanel import mixpanel_source
1006
+
1007
+ return mixpanel_source(
1008
+ username=username[0],
1009
+ password=password[0],
1010
+ project_id=project_id[0],
1011
+ start_date=start_date,
1012
+ end_date=end_date,
1013
+ server=server[0],
1014
+ ).with_resources(table)
1015
+
1016
+
991
1017
  class KafkaSource:
992
1018
  def handles_incrementality(self) -> bool:
993
1019
  return False
@@ -2506,3 +2532,126 @@ class SolidgateSource:
2506
2532
  ).with_resources(table_name)
2507
2533
  except ResourcesNotFoundError:
2508
2534
  raise UnsupportedResourceError(table_name, "Solidgate")
2535
+
2536
+
2537
+ class SFTPSource:
2538
+ def handles_incrementality(self) -> bool:
2539
+ return True
2540
+
2541
+ def dlt_source(self, uri: str, table: str, **kwargs):
2542
+ parsed_uri = urlparse(uri)
2543
+ host = parsed_uri.hostname
2544
+ if not host:
2545
+ raise MissingValueError("host", "SFTP URI")
2546
+ port = parsed_uri.port or 22
2547
+ username = parsed_uri.username
2548
+ password = parsed_uri.password
2549
+
2550
+ params: Dict[str, Any] = {
2551
+ "host": host,
2552
+ "port": port,
2553
+ "username": username,
2554
+ "password": password,
2555
+ "look_for_keys": False,
2556
+ "allow_agent": False,
2557
+ }
2558
+
2559
+ try:
2560
+ fs = fsspec.filesystem("sftp", **params)
2561
+ except Exception as e:
2562
+ raise ConnectionError(
2563
+ f"Failed to connect or authenticate to sftp server {host}:{port}. Error: {e}"
2564
+ )
2565
+ bucket_url = f"sftp://{host}:{port}"
2566
+
2567
+ if table.startswith("/"):
2568
+ file_glob = table
2569
+ else:
2570
+ file_glob = f"/{table}"
2571
+
2572
+ file_extension = table.split(".")[-1].lower()
2573
+ endpoint: str
2574
+ if file_extension == "csv":
2575
+ endpoint = "read_csv"
2576
+ elif file_extension == "jsonl":
2577
+ endpoint = "read_jsonl"
2578
+ elif file_extension == "parquet":
2579
+ endpoint = "read_parquet"
2580
+ else:
2581
+ raise ValueError(
2582
+ "FTPServer Source only supports specific file formats: csv, jsonl, parquet."
2583
+ )
2584
+ from ingestr.src.filesystem import readers
2585
+
2586
+ dlt_source_resource = readers(bucket_url, fs, file_glob)
2587
+ return dlt_source_resource.with_resources(endpoint)
2588
+
2589
+
2590
+ class QuickBooksSource:
2591
+ def handles_incrementality(self) -> bool:
2592
+ return True
2593
+
2594
+ # quickbooks://?company_id=<company_id>&client_id=<client_id>&client_secret=<client_secret>&refresh_token=<refresh>&access_token=<access_token>&environment=<env>&minor_version=<version>
2595
+ def dlt_source(self, uri: str, table: str, **kwargs):
2596
+ parsed_uri = urlparse(uri)
2597
+
2598
+ params = parse_qs(parsed_uri.query)
2599
+ company_id = params.get("company_id")
2600
+ client_id = params.get("client_id")
2601
+ client_secret = params.get("client_secret")
2602
+ refresh_token = params.get("refresh_token")
2603
+ environment = params.get("environment", ["production"])
2604
+ minor_version = params.get("minor_version", [None])
2605
+
2606
+ if not client_id or not client_id[0].strip():
2607
+ raise MissingValueError("client_id", "QuickBooks")
2608
+
2609
+ if not client_secret or not client_secret[0].strip():
2610
+ raise MissingValueError("client_secret", "QuickBooks")
2611
+
2612
+ if not refresh_token or not refresh_token[0].strip():
2613
+ raise MissingValueError("refresh_token", "QuickBooks")
2614
+
2615
+ if not company_id or not company_id[0].strip():
2616
+ raise MissingValueError("company_id", "QuickBooks")
2617
+
2618
+ if environment[0] not in ["production", "sandbox"]:
2619
+ raise ValueError(
2620
+ "Invalid environment. Must be either 'production' or 'sandbox'."
2621
+ )
2622
+
2623
+ from ingestr.src.quickbooks import quickbooks_source
2624
+
2625
+ table_name = table.replace(" ", "")
2626
+ table_mapping = {
2627
+ "customers": "customer",
2628
+ "invoices": "invoice",
2629
+ "accounts": "account",
2630
+ "vendors": "vendor",
2631
+ "payments": "payment",
2632
+ }
2633
+ if table_name in table_mapping:
2634
+ table_name = table_mapping[table_name]
2635
+
2636
+ start_date = kwargs.get("interval_start")
2637
+ if start_date is None:
2638
+ start_date = ensure_pendulum_datetime("2025-01-01").in_tz("UTC")
2639
+ else:
2640
+ start_date = ensure_pendulum_datetime(start_date).in_tz("UTC")
2641
+
2642
+ end_date = kwargs.get("interval_end")
2643
+
2644
+ if end_date is not None:
2645
+ end_date = ensure_pendulum_datetime(end_date).in_tz("UTC")
2646
+
2647
+ return quickbooks_source(
2648
+ company_id=company_id[0],
2649
+ start_date=start_date,
2650
+ end_date=end_date,
2651
+ client_id=client_id[0],
2652
+ client_secret=client_secret[0],
2653
+ refresh_token=refresh_token[0],
2654
+ environment=environment[0],
2655
+ minor_version=minor_version[0],
2656
+ object=table_name,
2657
+ ).with_resources(table_name)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ingestr
3
- Version: 0.13.52
3
+ Version: 0.13.54
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
@@ -26,6 +26,7 @@ Requires-Dist: asn1crypto==1.5.1
26
26
  Requires-Dist: asynch==0.2.4
27
27
  Requires-Dist: attrs==25.1.0
28
28
  Requires-Dist: backoff==2.2.1
29
+ Requires-Dist: bcrypt==4.3.0
29
30
  Requires-Dist: beautifulsoup4==4.13.3
30
31
  Requires-Dist: boto3==1.37.1
31
32
  Requires-Dist: botocore==1.37.1
@@ -39,6 +40,7 @@ Requires-Dist: clickhouse-connect==0.8.14
39
40
  Requires-Dist: clickhouse-driver==0.2.9
40
41
  Requires-Dist: clickhouse-sqlalchemy==0.2.7
41
42
  Requires-Dist: confluent-kafka==2.8.0
43
+ Requires-Dist: crate==2.0.0
42
44
  Requires-Dist: cryptography==44.0.2
43
45
  Requires-Dist: curlify==2.2.1
44
46
  Requires-Dist: databricks-sql-connector==2.9.3
@@ -50,15 +52,19 @@ Requires-Dist: dlt==1.10.0
50
52
  Requires-Dist: dnspython==2.7.0
51
53
  Requires-Dist: duckdb-engine==0.17.0
52
54
  Requires-Dist: duckdb==1.2.1
55
+ Requires-Dist: ecdsa==0.19.1
53
56
  Requires-Dist: elastic-transport==8.17.1
54
57
  Requires-Dist: elasticsearch==8.10.1
58
+ Requires-Dist: enum-compat==0.0.3
55
59
  Requires-Dist: et-xmlfile==2.0.0
56
60
  Requires-Dist: facebook-business==20.0.0
57
61
  Requires-Dist: filelock==3.17.0
58
62
  Requires-Dist: flatten-json==0.1.14
59
63
  Requires-Dist: frozenlist==1.5.0
60
64
  Requires-Dist: fsspec==2025.3.2
65
+ Requires-Dist: future==1.0.0
61
66
  Requires-Dist: gcsfs==2025.3.2
67
+ Requires-Dist: geojson==3.2.0
62
68
  Requires-Dist: gitdb==4.0.12
63
69
  Requires-Dist: gitpython==3.1.44
64
70
  Requires-Dist: giturlparse==0.12.0
@@ -77,7 +83,7 @@ Requires-Dist: google-cloud-storage==3.1.0
77
83
  Requires-Dist: google-crc32c==1.6.0
78
84
  Requires-Dist: google-resumable-media==2.7.2
79
85
  Requires-Dist: googleapis-common-protos==1.69.0
80
- Requires-Dist: greenlet==3.2.2
86
+ Requires-Dist: greenlet==3.2.3
81
87
  Requires-Dist: grpc-google-iam-v1==0.14.2
82
88
  Requires-Dist: grpc-interceptor==0.15.4
83
89
  Requires-Dist: grpcio-status==1.62.3
@@ -90,6 +96,7 @@ Requires-Dist: ibm-db-sa==0.4.1
90
96
  Requires-Dist: ibm-db==3.2.6
91
97
  Requires-Dist: idna==3.10
92
98
  Requires-Dist: inflection==0.5.1
99
+ Requires-Dist: intuit-oauth==1.2.4
93
100
  Requires-Dist: isodate==0.7.2
94
101
  Requires-Dist: jmespath==1.0.1
95
102
  Requires-Dist: jsonpath-ng==1.7.0
@@ -113,6 +120,7 @@ Requires-Dist: openpyxl==3.1.5
113
120
  Requires-Dist: orjson==3.10.15
114
121
  Requires-Dist: packaging==24.2
115
122
  Requires-Dist: pandas==2.2.3
123
+ Requires-Dist: paramiko==3.5.1
116
124
  Requires-Dist: pathvalidate==3.2.3
117
125
  Requires-Dist: pendulum==3.0.0
118
126
  Requires-Dist: platformdirs==4.3.6
@@ -137,13 +145,17 @@ Requires-Dist: pygments==2.19.1
137
145
  Requires-Dist: pyjwt==2.10.1
138
146
  Requires-Dist: pymongo==4.11.1
139
147
  Requires-Dist: pymysql==1.1.1
148
+ Requires-Dist: pynacl==1.5.0
140
149
  Requires-Dist: pyopenssl==25.0.0
141
150
  Requires-Dist: pyparsing==3.2.1
142
151
  Requires-Dist: pyrate-limiter==3.7.0
143
152
  Requires-Dist: python-dateutil==2.9.0.post0
144
153
  Requires-Dist: python-dotenv==1.0.1
154
+ Requires-Dist: python-jose==3.5.0
155
+ Requires-Dist: python-quickbooks==0.9.2
145
156
  Requires-Dist: pytz==2025.1
146
157
  Requires-Dist: pyyaml==6.0.2
158
+ Requires-Dist: rauth==0.7.3
147
159
  Requires-Dist: redshift-connector==2.1.5
148
160
  Requires-Dist: requests-file==2.1.0
149
161
  Requires-Dist: requests-oauthlib==1.3.1
@@ -170,6 +182,7 @@ Requires-Dist: snowflake-sqlalchemy==1.6.1
170
182
  Requires-Dist: sortedcontainers==2.4.0
171
183
  Requires-Dist: soupsieve==2.6
172
184
  Requires-Dist: sqlalchemy-bigquery==1.12.1
185
+ Requires-Dist: sqlalchemy-cratedb==0.42.0.dev2
173
186
  Requires-Dist: sqlalchemy-hana==2.0.0
174
187
  Requires-Dist: sqlalchemy-redshift==0.8.14
175
188
  Requires-Dist: sqlalchemy-spanner==1.11.0
@@ -192,6 +205,7 @@ Requires-Dist: tzdata==2025.1
192
205
  Requires-Dist: tzlocal==5.3
193
206
  Requires-Dist: uritemplate==4.1.1
194
207
  Requires-Dist: urllib3==2.3.0
208
+ Requires-Dist: verlib2==0.3.1
195
209
  Requires-Dist: wrapt==1.17.2
196
210
  Requires-Dist: yarl==1.18.3
197
211
  Requires-Dist: zeep==4.3.1
@@ -292,11 +306,21 @@ Pull requests are welcome. However, please open an issue first to discuss what y
292
306
  <td>✅</td>
293
307
  <td>✅</td>
294
308
  </tr>
309
+ <tr>
310
+ <td>CrateDB</td>
311
+ <td>✅</td>
312
+ <td>❌</td>
313
+ </tr>
295
314
  <tr>
296
315
  <td>Databricks</td>
297
316
  <td>✅</td>
298
317
  <td>✅</td>
299
318
  </tr>
319
+ <tr>
320
+ <td>IBM Db2</td>
321
+ <td>✅</td>
322
+ <td>-</td>
323
+ </tr>
300
324
  <tr>
301
325
  <td>DuckDB</td>
302
326
  <td>✅</td>
@@ -307,6 +331,16 @@ Pull requests are welcome. However, please open an issue first to discuss what y
307
331
  <td>✅</td>
308
332
  <td>-</td>
309
333
  </tr>
334
+ <tr>
335
+ <td>Elasticsearch</td>
336
+ <td>✅</td>
337
+ <td>-</td>
338
+ </tr>
339
+ <tr>
340
+ <td>GCP Spanner</td>
341
+ <td>✅</td>
342
+ <td>-</td>
343
+ </tr>
310
344
  <tr>
311
345
  <td>Local CSV file</td>
312
346
  <td>✅</td>
@@ -393,6 +427,11 @@ Pull requests are welcome. However, please open an issue first to discuss what y
393
427
  <td>✅</td>
394
428
  <td>-</td>
395
429
  </tr>
430
+ <tr>
431
+ <td>Attio</td>
432
+ <td>✅</td>
433
+ <td>-</td>
434
+ </tr>
396
435
  <tr>
397
436
  <td>Chess.com</td>
398
437
  <td>✅</td>
@@ -404,7 +443,7 @@ Pull requests are welcome. However, please open an issue first to discuss what y
404
443
  <td>-</td>
405
444
  </tr>
406
445
  <tr>
407
- <td>Github</td>
446
+ <td>GitHub</td>
408
447
  <td>✅</td>
409
448
  <td>-</td>
410
449
  </tr>
@@ -447,6 +486,16 @@ Pull requests are welcome. However, please open an issue first to discuss what y
447
486
  <td>Personio</td>
448
487
  <td>✅</td>
449
488
  <td>-</td>
489
+ </tr>
490
+ <tr>
491
+ <td>Phantombuster</td>
492
+ <td>✅</td>
493
+ <td>-</td>
494
+ </tr>
495
+ <tr>
496
+ <td>Pipedrive</td>
497
+ <td>✅</td>
498
+ <td>-</td>
450
499
  </tr>
451
500
  <tr>
452
501
  <td>S3</td>
@@ -469,7 +518,12 @@ Pull requests are welcome. However, please open an issue first to discuss what y
469
518
  <td>-</td>
470
519
  </tr>
471
520
  <tr>
472
- <td>Smartsheet</td>
521
+ <td>Smartsheets</td>
522
+ <td>✅</td>
523
+ <td>-</td>
524
+ </tr>
525
+ <tr>
526
+ <td>Solidgate</td>
473
527
  <td>✅</td>
474
528
  <td>-</td>
475
529
  </tr>
@@ -2,16 +2,16 @@ ingestr/conftest.py,sha256=Q03FIJIZpLBbpj55cfCHIKEjc1FCvWJhMF2cidUJKQU,1748
2
2
  ingestr/main.py,sha256=GkC1hdq8AVGrvolc95zMfjmibI95p2pmFkbgCOVf-Og,26311
3
3
  ingestr/src/.gitignore,sha256=8cX1AZTSI0TcdZFGTmS_oyBjpfCzhOEt0DdAo2dFIY8,203
4
4
  ingestr/src/blob.py,sha256=onMe5ZHxPXTdcB_s2oGNdMo-XQJ3ajwOsWE9eSTGFmc,1495
5
- ingestr/src/buildinfo.py,sha256=qcKqSPI861BBapk7rbcYkc9M_18QQnT44YM1Zr_Sg80,21
6
- ingestr/src/destinations.py,sha256=axSm0GLM_bKQmWYb74fnHebNktWgtfAyyqdHV-zBOsA,16405
5
+ ingestr/src/buildinfo.py,sha256=5aqjzKmdujvelIHbUBjqDKLXcEB2hEdLvQ6BJyTyZ1Q,21
6
+ ingestr/src/destinations.py,sha256=TcxM2rcwHfgMMP6U0yRNcfWKlEzkBbZbqCIDww7lkTY,16882
7
7
  ingestr/src/errors.py,sha256=Ufs4_DfE77_E3vnA1fOQdi6cmuLVNm7_SbFLkL1XPGk,686
8
- ingestr/src/factory.py,sha256=5yrg-XkaixuCkiTz3B7mraE8LaANXtzItenbx8TdPrE,5863
8
+ ingestr/src/factory.py,sha256=mcjgbmrZr6TvP9fCMQxo-aMGcrb2PqToRcSLp5nldww,6138
9
9
  ingestr/src/filters.py,sha256=LLecXe9QkLFkFLUZ92OXNdcANr1a8edDxrflc2ko_KA,1452
10
10
  ingestr/src/http_client.py,sha256=bxqsk6nJNXCo-79gW04B53DQO-yr25vaSsqP0AKtjx4,732
11
11
  ingestr/src/loader.py,sha256=9NaWAyfkXdqAZSS-N72Iwo36Lbx4PyqIfaaH1dNdkFs,1712
12
12
  ingestr/src/partition.py,sha256=BrIP6wFJvyR7Nus_3ElnfxknUXeCipK_E_bB8kZowfc,969
13
13
  ingestr/src/resource.py,sha256=ZqmZxFQVGlF8rFPhBiUB08HES0yoTj8sZ--jKfaaVps,1164
14
- ingestr/src/sources.py,sha256=SnFxil6gU5RIYiSUfE6Upq2se0YeRSuTIKK3Jxn-YKI,86587
14
+ ingestr/src/sources.py,sha256=Nwx66jQwqjovLomZIVwIVNLSBy8KgqGhu4-spPHp7ZM,91785
15
15
  ingestr/src/table_definition.py,sha256=REbAbqdlmUMUuRh8nEQRreWjPVOQ5ZcfqGkScKdCrmk,390
16
16
  ingestr/src/time.py,sha256=H_Fk2J4ShXyUM-EMY7MqCLZQhlnZMZvO952bmZPc4yE,254
17
17
  ingestr/src/version.py,sha256=J_2xgZ0mKlvuHcjdKCx2nlioneLH0I47JiU_Slr_Nwc,189
@@ -82,6 +82,8 @@ ingestr/src/klaviyo/helpers.py,sha256=_i-SHffhv25feLDcjy6Blj1UxYLISCwVCMgGtrlnYH
82
82
  ingestr/src/linkedin_ads/__init__.py,sha256=CAPWFyV24loziiphbLmODxZUXZJwm4JxlFkr56q0jfo,1855
83
83
  ingestr/src/linkedin_ads/dimension_time_enum.py,sha256=EmHRdkFyTAfo4chGjThrwqffWJxmAadZMbpTvf0xkQc,198
84
84
  ingestr/src/linkedin_ads/helpers.py,sha256=eUWudRVlXl4kqIhfXQ1eVsUpZwJn7UFqKSpnbLfxzds,4498
85
+ ingestr/src/mixpanel/__init__.py,sha256=s1QtqMP0BTGW6YtdCabJFWj7lEn7KujzELwGpBOQgfs,1796
86
+ ingestr/src/mixpanel/client.py,sha256=c_reouegOVYBOwHLfgYFwpmkba0Sxro1Zkml07NCYf0,3602
85
87
  ingestr/src/mongodb/__init__.py,sha256=T-RYPS_skl_2gNVfYWWXan2bVQYmm0bFBcCCqG5ejvg,7275
86
88
  ingestr/src/mongodb/helpers.py,sha256=H0GpOK3bPBhFWBEhJZOjywUBdzih6MOpmyVO_cKSN14,24178
87
89
  ingestr/src/notion/__init__.py,sha256=36wUui8finbc85ObkRMq8boMraXMUehdABN_AMe_hzA,1834
@@ -99,6 +101,7 @@ ingestr/src/pipedrive/typing.py,sha256=lEMXu4hhAA3XkhVSlBUa-juqyupisd3c-qSQKxFvz
99
101
  ingestr/src/pipedrive/helpers/__init__.py,sha256=UX1K_qnGXB0ShtnBOfp2XuVbK8RRoCK8TdEmIjRckgg,710
100
102
  ingestr/src/pipedrive/helpers/custom_fields_munger.py,sha256=rZ4AjdITHfJE2NNomCR7vMBS1KnWpEGVF6fADwsIHUE,4488
101
103
  ingestr/src/pipedrive/helpers/pages.py,sha256=Klpjw2OnMuhzit3PpiHKsfzGcJ3rQPSQBl3HhE3-6eA,3358
104
+ ingestr/src/quickbooks/__init__.py,sha256=cZUuVCOTGPHTscRj6i0DytO63_fWF-4ieMxoU4PcyTg,3727
102
105
  ingestr/src/salesforce/__init__.py,sha256=2hik5pRrxVODdDTlUEMoyccNC07zozjnxkMHcjMT1qA,4558
103
106
  ingestr/src/salesforce/helpers.py,sha256=QTdazBt-qRTBbCQMZnyclIaDQFmBixBy_RDKD00Lt-8,2492
104
107
  ingestr/src/shopify/__init__.py,sha256=PF_6VQnS065Br1UzSIekTVXBu3WtrMQL_v5CfbfaX5Y,63151
@@ -135,8 +138,8 @@ ingestr/testdata/merge_expected.csv,sha256=DReHqWGnQMsf2PBv_Q2pfjsgvikYFnf1zYcQZ
135
138
  ingestr/testdata/merge_part1.csv,sha256=Pw8Z9IDKcNU0qQHx1z6BUf4rF_-SxKGFOvymCt4OY9I,185
136
139
  ingestr/testdata/merge_part2.csv,sha256=T_GiWxA81SN63_tMOIuemcvboEFeAmbKc7xRXvL9esw,287
137
140
  ingestr/tests/unit/test_smartsheets.py,sha256=eiC2CCO4iNJcuN36ONvqmEDryCA1bA1REpayHpu42lk,5058
138
- ingestr-0.13.52.dist-info/METADATA,sha256=jfSWJ6mE6vPz-5XsIIjqdLxd45-OrOUKbhbwJqiZlSc,13983
139
- ingestr-0.13.52.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
140
- ingestr-0.13.52.dist-info/entry_points.txt,sha256=oPJy0KBnPWYjDtP1k8qwAihcTLHSZokSQvRAw_wtfJM,46
141
- ingestr-0.13.52.dist-info/licenses/LICENSE.md,sha256=cW8wIhn8HFE-KLStDF9jHQ1O_ARWP3kTpk_-eOccL24,1075
142
- ingestr-0.13.52.dist-info/RECORD,,
141
+ ingestr-0.13.54.dist-info/METADATA,sha256=5RaRSmJX-SiZV9iFYz3cSkiuZp0GXQ2ij9whkbQHFs8,15131
142
+ ingestr-0.13.54.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
143
+ ingestr-0.13.54.dist-info/entry_points.txt,sha256=oPJy0KBnPWYjDtP1k8qwAihcTLHSZokSQvRAw_wtfJM,46
144
+ ingestr-0.13.54.dist-info/licenses/LICENSE.md,sha256=cW8wIhn8HFE-KLStDF9jHQ1O_ARWP3kTpk_-eOccL24,1075
145
+ ingestr-0.13.54.dist-info/RECORD,,