ingestr 0.13.24__py3-none-any.whl → 0.13.26__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.24"
1
+ version = "v0.13.26"
ingestr/src/factory.py CHANGED
@@ -28,6 +28,7 @@ from ingestr.src.sources import (
28
28
  ChessSource,
29
29
  DynamoDBSource,
30
30
  FacebookAdsSource,
31
+ FrankfurterSource,
31
32
  GCSSource,
32
33
  GitHubSource,
33
34
  GoogleAdsSource,
@@ -146,6 +147,7 @@ class SourceDestinationFactory:
146
147
  "personio": PersonioSource,
147
148
  "kinesis": KinesisSource,
148
149
  "pipedrive": PipedriveSource,
150
+ "frankfurter": FrankfurterSource,
149
151
  }
150
152
  destinations: Dict[str, Type[DestinationProtocol]] = {
151
153
  "bigquery": BigQueryDestination,
@@ -0,0 +1,142 @@
1
+ from typing import Any, Iterator, Optional
2
+
3
+ import dlt
4
+ from dlt.common.pendulum import pendulum
5
+ from dlt.common.time import ensure_pendulum_datetime
6
+ from dlt.common.typing import TAnyDateTime
7
+
8
+ from ingestr.src.frankfurter.helpers import get_path_with_retry
9
+
10
+
11
+ @dlt.source(
12
+ name="frankfurter",
13
+ max_table_nesting=0,
14
+ )
15
+ def frankfurter_source(
16
+ table: str,
17
+ start_date: Optional[TAnyDateTime] = None,
18
+ end_date: Optional[TAnyDateTime] = None,
19
+ ) -> Any:
20
+ """
21
+ A dlt source for the frankfurter.dev API. It groups several resources (in this case frankfurter.dev API endpoints) containing
22
+ various types of data: currencies, latest rates, historical rates.
23
+
24
+ Returns the appropriate resource based on the provided parameters.
25
+ """
26
+ # Determine which resource to return based on the `table` parameter
27
+ if table == "currencies":
28
+ return currencies()
29
+
30
+ elif table == "latest":
31
+ return latest()
32
+
33
+ elif table == "exchange_rates":
34
+ return exchange_rates(start_date=start_date, end_date=end_date)
35
+
36
+
37
+ @dlt.resource(
38
+ write_disposition="replace",
39
+ columns={
40
+ "currency_code": {"data_type": "text"},
41
+ "currency_name": {"data_type": "text"},
42
+ },
43
+ )
44
+ def currencies() -> Iterator[dict]:
45
+ """
46
+ Yields each currency as a separate row with two columns: currency_code and currency_name.
47
+ """
48
+ # Retrieve the list of currencies from the API
49
+ currencies_data = get_path_with_retry("currencies")
50
+
51
+ for currency_code, currency_name in currencies_data.items():
52
+ yield {"currency_code": currency_code, "currency_name": currency_name}
53
+
54
+
55
+ @dlt.resource(
56
+ write_disposition="replace",
57
+ columns={
58
+ "date": {"data_type": "text"},
59
+ "currency_name": {"data_type": "text"},
60
+ "rate": {"data_type": "double"},
61
+ },
62
+ primary_key=["date", "currency_name"], # Composite primary key
63
+ )
64
+ def latest() -> Iterator[dict]:
65
+ """
66
+ Fetches the latest exchange rates and yields them as rows.
67
+ """
68
+ # Base URL
69
+ url = "latest?"
70
+
71
+ # Fetch data
72
+ latest_data = get_path_with_retry(url)
73
+
74
+ # Extract rates and base currency
75
+ rates = latest_data["rates"]
76
+
77
+ # Prepare the date
78
+ date = pendulum.now().to_date_string()
79
+
80
+ # Add the base currency (EUR) with a rate of 1.0
81
+ yield {
82
+ "date": date,
83
+ "currency_name": "EUR",
84
+ "rate": 1.0,
85
+ }
86
+
87
+ # Add all currencies and their rates
88
+ for currency_name, rate in rates.items():
89
+ yield {
90
+ "date": date,
91
+ "currency_name": currency_name,
92
+ "rate": rate,
93
+ }
94
+
95
+
96
+ @dlt.resource(
97
+ write_disposition="replace",
98
+ columns={
99
+ "date": {"data_type": "text"},
100
+ "currency_name": {"data_type": "text"},
101
+ "rate": {"data_type": "double"},
102
+ },
103
+ primary_key=["date", "currency_name"], # Composite primary key
104
+ )
105
+ def exchange_rates(
106
+ start_date: TAnyDateTime,
107
+ end_date: TAnyDateTime,
108
+ ) -> Iterator[dict]:
109
+ """
110
+ Fetches exchange rates for a specified date range.
111
+ If only start_date is provided, fetches data for that date.
112
+ If both start_date and end_date are provided, fetches data for each day in the range.
113
+ """
114
+ start_date_str = ensure_pendulum_datetime(start_date).format("YYYY-MM-DD")
115
+ end_date_str = ensure_pendulum_datetime(end_date).format("YYYY-MM-DD")
116
+
117
+ # Compose the URL
118
+ url = f"{start_date_str}..{end_date_str}?"
119
+
120
+ # Fetch data from the API
121
+ data = get_path_with_retry(url)
122
+
123
+ # Extract base currency and rates from the API response
124
+ base_currency = data["base"]
125
+ rates = data["rates"]
126
+
127
+ # Iterate over the rates dictionary (one entry per date)
128
+ for date, daily_rates in rates.items():
129
+ # Add the base currency with a rate of 1.0
130
+ yield {
131
+ "date": date,
132
+ "currency_name": base_currency,
133
+ "rate": 1.0,
134
+ }
135
+
136
+ # Add all other currencies and their rates
137
+ for currency_name, rate in daily_rates.items():
138
+ yield {
139
+ "date": date,
140
+ "currency_name": currency_name,
141
+ "rate": rate,
142
+ }
@@ -0,0 +1,32 @@
1
+ from datetime import datetime
2
+
3
+ from dlt.common.pendulum import pendulum
4
+ from dlt.common.typing import StrAny
5
+ from dlt.sources.helpers import requests
6
+
7
+ FRANKFURTER_API_URL = "https://api.frankfurter.dev/v1/"
8
+
9
+
10
+ def get_url_with_retry(url: str) -> StrAny:
11
+ r = requests.get(url)
12
+ return r.json() # type: ignore
13
+
14
+
15
+ def get_path_with_retry(path: str) -> StrAny:
16
+ return get_url_with_retry(f"{FRANKFURTER_API_URL}{path}")
17
+
18
+
19
+ def validate_dates(start_date: datetime, end_date: datetime) -> None:
20
+ current_date = pendulum.now()
21
+
22
+ # Check if start_date is in the future
23
+ if start_date > current_date:
24
+ raise ValueError("Interval-start cannot be in the future.")
25
+
26
+ # Check if end_date is in the future
27
+ if end_date > current_date:
28
+ raise ValueError("Interval-end cannot be in the future.")
29
+
30
+ # Check if start_date is before end_date
31
+ if start_date > end_date:
32
+ raise ValueError("Interval-end cannot be before interval-start.")
@@ -36,6 +36,7 @@ from .helpers import _get_property_names, fetch_data, fetch_property_history
36
36
  from .settings import (
37
37
  ALL,
38
38
  CRM_OBJECT_ENDPOINTS,
39
+ CRM_SCHEMAS_ENDPOINT,
39
40
  DEFAULT_COMPANY_PROPS,
40
41
  DEFAULT_CONTACT_PROPS,
41
42
  DEFAULT_DEAL_PROPS,
@@ -161,6 +162,13 @@ def hubspot(
161
162
  props,
162
163
  include_custom_props,
163
164
  )
165
+
166
+ @dlt.resource(name="schemas", write_disposition="merge", primary_key="id")
167
+ def schemas(
168
+ api_key: str = api_key,
169
+ ) -> Iterator[TDataItems]:
170
+ """Hubspot schemas resource"""
171
+ yield from fetch_data(CRM_SCHEMAS_ENDPOINT, api_key)
164
172
 
165
173
  @dlt.resource(name="quotes", write_disposition="replace")
166
174
  def quotes(
@@ -178,7 +186,7 @@ def hubspot(
178
186
  include_custom_props,
179
187
  )
180
188
 
181
- return companies, contacts, deals, tickets, products, quotes
189
+ return companies, contacts, deals, tickets, products, quotes, schemas
182
190
 
183
191
 
184
192
  def crm_objects(
@@ -132,16 +132,27 @@ def fetch_data(
132
132
  if "results" in _data:
133
133
  _objects: List[Dict[str, Any]] = []
134
134
  for _result in _data["results"]:
135
- _obj = _result.get("properties", _result)
136
- if "id" not in _obj and "id" in _result:
137
- # Move id from properties to top level
138
- _obj["id"] = _result["id"]
139
- if "associations" in _result:
140
- for association in _result["associations"]:
141
- __values = [
142
- {
143
- "value": _obj["hs_object_id"],
144
- f"{association}_id": __r["id"],
135
+ if endpoint == "/crm/v3/schemas":
136
+ _objects.append({
137
+ "name": _result["labels"].get("singular", ""),
138
+ "objectTypeId": _result.get("objectTypeId", ""),
139
+ "id": _result.get("id", ""),
140
+ "fullyQualifiedName": _result.get("fullyQualifiedName", ""),
141
+ "properties": _result.get("properties", ""),
142
+ "createdAt": _result.get("createdAt", ""),
143
+ "updatedAt": _result.get("updatedAt", "")
144
+ })
145
+ else:
146
+ _obj = _result.get("properties", _result)
147
+ if "id" not in _obj and "id" in _result:
148
+ # Move id from properties to top level
149
+ _obj["id"] = _result["id"]
150
+ if "associations" in _result:
151
+ for association in _result["associations"]:
152
+ __values = [
153
+ {
154
+ "value": _obj["hs_object_id"],
155
+ f"{association}_id": __r["id"],
145
156
  }
146
157
  for __r in _result["associations"][association]["results"]
147
158
  ]
@@ -152,7 +163,7 @@ def fetch_data(
152
163
  ]
153
164
 
154
165
  _obj[association] = __values
155
- _objects.append(_obj)
166
+ _objects.append(_obj)
156
167
  yield _objects
157
168
 
158
169
  # Follow pagination links if they exist
@@ -14,6 +14,7 @@ CRM_DEALS_ENDPOINT = "/crm/v3/objects/deals"
14
14
  CRM_PRODUCTS_ENDPOINT = "/crm/v3/objects/products"
15
15
  CRM_TICKETS_ENDPOINT = "/crm/v3/objects/tickets"
16
16
  CRM_QUOTES_ENDPOINT = "/crm/v3/objects/quotes"
17
+ CRM_SCHEMAS_ENDPOINT = "/crm/v3/schemas"
17
18
 
18
19
  CRM_OBJECT_ENDPOINTS = {
19
20
  "contact": CRM_CONTACTS_ENDPOINT,
ingestr/src/sources.py CHANGED
@@ -67,6 +67,8 @@ from ingestr.src.errors import (
67
67
  from ingestr.src.facebook_ads import facebook_ads_source, facebook_insights_source
68
68
  from ingestr.src.filesystem import readers
69
69
  from ingestr.src.filters import table_adapter_exclude_columns
70
+ from ingestr.src.frankfurter import frankfurter_source
71
+ from ingestr.src.frankfurter.helpers import validate_dates
70
72
  from ingestr.src.github import github_reactions, github_repo_events, github_stargazers
71
73
  from ingestr.src.google_ads import google_ads
72
74
  from ingestr.src.google_analytics import google_analytics
@@ -801,7 +803,7 @@ class HubspotSource:
801
803
  raise ValueError("api_key in the URI is required to connect to Hubspot")
802
804
 
803
805
  endpoint = None
804
- if table in ["contacts", "companies", "deals", "tickets", "products", "quotes"]:
806
+ if table in ["contacts", "companies", "deals", "tickets", "products", "quotes", "schemas"]:
805
807
  endpoint = table
806
808
  else:
807
809
  raise ValueError(
@@ -2041,3 +2043,40 @@ class PipedriveSource:
2041
2043
  return pipedrive_source(
2042
2044
  pipedrive_api_key=api_key, since_timestamp=start_date
2043
2045
  ).with_resources(table)
2046
+
2047
+
2048
+ class FrankfurterSource:
2049
+ def handles_incrementality(self) -> bool:
2050
+ return True
2051
+
2052
+ def dlt_source(self, uri: str, table: str, **kwargs):
2053
+ # start and end dates only assigned and validated for exchange_rates table
2054
+ # Note: if an end date but no start date is provided, start date and end date will be set to current date
2055
+ if table == "exchange_rates":
2056
+ if kwargs.get("interval_start"):
2057
+ start_date = ensure_pendulum_datetime(str(kwargs.get("interval_start")))
2058
+ if kwargs.get("interval_end"):
2059
+ end_date = ensure_pendulum_datetime(str(kwargs.get("interval_end")))
2060
+ else:
2061
+ end_date = start_date
2062
+ else:
2063
+ start_date = pendulum.now()
2064
+ end_date = pendulum.now()
2065
+ validate_dates(start_date=start_date, end_date=end_date)
2066
+
2067
+ # For currencies and latest tables, set start and end dates to current date
2068
+ else:
2069
+ start_date = pendulum.now()
2070
+ end_date = pendulum.now()
2071
+
2072
+ # Validate table
2073
+ if table not in ["currencies", "latest", "exchange_rates"]:
2074
+ raise ValueError(
2075
+ f"Table '{table}' is not supported for Frankfurter source."
2076
+ )
2077
+
2078
+ return frankfurter_source(
2079
+ table=table,
2080
+ start_date=start_date,
2081
+ end_date=end_date,
2082
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ingestr
3
- Version: 0.13.24
3
+ Version: 0.13.26
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
@@ -2,15 +2,15 @@ ingestr/conftest.py,sha256=Q03FIJIZpLBbpj55cfCHIKEjc1FCvWJhMF2cidUJKQU,1748
2
2
  ingestr/main.py,sha256=wvbRCJ2--M0Zw2cYtSH874TxTtlD0wadHREeLG3anOY,25618
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=x-bxDOuFDQ1rgDJf03eHD1bXb9Yfo3wX39XyaBE0LkU,21
5
+ ingestr/src/buildinfo.py,sha256=VzL8INFpli0urCbtG-JAzjTMJgmPVwIKFd9agl-cDsY,21
6
6
  ingestr/src/destinations.py,sha256=vrGij4qMPCdXTMIimROWBJFqzOqCM4DFmgyubgSHejA,11279
7
7
  ingestr/src/errors.py,sha256=Ufs4_DfE77_E3vnA1fOQdi6cmuLVNm7_SbFLkL1XPGk,686
8
- ingestr/src/factory.py,sha256=1jqcLv_QUUGeyg1OYN3ywrRdcDZyDRtMOongwyjDapU,5268
8
+ ingestr/src/factory.py,sha256=659h_sVRBhtPv2dvtOK8tf3PtUhlK3KsWLrb20_iQKw,5333
9
9
  ingestr/src/filters.py,sha256=5LNpBgm8FJXdrFHGyM7dLVyphKykSpPk7yuQAZ8GML4,1133
10
10
  ingestr/src/loader.py,sha256=9NaWAyfkXdqAZSS-N72Iwo36Lbx4PyqIfaaH1dNdkFs,1712
11
11
  ingestr/src/partition.py,sha256=E0WHqh1FTheQAIVK_-jWUx0dgyYZCD1VxlAm362gao4,964
12
12
  ingestr/src/resource.py,sha256=XG-sbBapFVEM7OhHQFQRTdTLlh-mHB-N4V1t8F8Tsww,543
13
- ingestr/src/sources.py,sha256=kGsgFWf8Ghha0-HlC6PlDIIKX2Lriah4UmAseziGdr4,72035
13
+ ingestr/src/sources.py,sha256=pF0o1xi84hDo_fdM2o_Wmhcrye_mr55dMQ1BvL8sZZA,73582
14
14
  ingestr/src/table_definition.py,sha256=REbAbqdlmUMUuRh8nEQRreWjPVOQ5ZcfqGkScKdCrmk,390
15
15
  ingestr/src/time.py,sha256=H_Fk2J4ShXyUM-EMY7MqCLZQhlnZMZvO952bmZPc4yE,254
16
16
  ingestr/src/version.py,sha256=J_2xgZ0mKlvuHcjdKCx2nlioneLH0I47JiU_Slr_Nwc,189
@@ -41,6 +41,8 @@ ingestr/src/facebook_ads/settings.py,sha256=1IxZeP_4rN3IBvAncNHOoqpzAirx0Hz-MUK_
41
41
  ingestr/src/filesystem/__init__.py,sha256=zkIwbRr0ir0EUdniI25p2zGiVc-7M9EmR351AjNb0eA,4163
42
42
  ingestr/src/filesystem/helpers.py,sha256=bg0muSHZr3hMa8H4jN2-LGWzI-SUoKlQNiWJ74-YYms,3211
43
43
  ingestr/src/filesystem/readers.py,sha256=a0fKkaRpnAOGsXI3EBNYZa7x6tlmAOsgRzb883StY30,3987
44
+ ingestr/src/frankfurter/__init__.py,sha256=xJUicENGYtOPsGznKP8IA_5Jt-_gJP29onrByBgUf-g,4259
45
+ ingestr/src/frankfurter/helpers.py,sha256=RSqI-WAAJfunWnLqiBRmPuonRg7rDOqmY76beb8a6rM,967
44
46
  ingestr/src/github/__init__.py,sha256=xVijF-Wi4p88hkVJnKH-oTixismjD3aUcGqGa6Wr4e4,5889
45
47
  ingestr/src/github/helpers.py,sha256=rpv_3HzuOl4PQ-FUeA66pev-pgze9SaE8RUHIPYfZ_A,6759
46
48
  ingestr/src/github/queries.py,sha256=W34C02jUEdjFmOE7f7u9xvYyBNDMfVZAu0JIRZI2mkU,2302
@@ -59,9 +61,9 @@ ingestr/src/google_sheets/helpers/api_calls.py,sha256=RiVfdacbaneszhmuhYilkJnkc9
59
61
  ingestr/src/google_sheets/helpers/data_processing.py,sha256=RNt2MYfdJhk4bRahnQVezpNg2x9z0vx60YFq2ukZ8vI,11004
60
62
  ingestr/src/gorgias/__init__.py,sha256=_mFkMYwlY5OKEY0o_FK1OKol03A-8uk7bm1cKlmt5cs,21432
61
63
  ingestr/src/gorgias/helpers.py,sha256=DamuijnvhGY9hysQO4txrVMf4izkGbh5qfBKImdOINE,5427
62
- ingestr/src/hubspot/__init__.py,sha256=NYgSIAPXQh2Qp1eKun7TgcerKogq6pWtNkr-_f0FXbI,9464
63
- ingestr/src/hubspot/helpers.py,sha256=PTn-UHJv1ENIvA5azUTaHCmFXgmHLJC1tUatQ1N-KFE,6727
64
- ingestr/src/hubspot/settings.py,sha256=9P1OKiRL88kl_m8n1HhuG-Qpq9VGbqPLn5Q0QYneToU,2193
64
+ ingestr/src/hubspot/__init__.py,sha256=nHjMvqUAUNVy9dX-1YiaY6PeplDnzc_LjQl25DdVrYg,9765
65
+ ingestr/src/hubspot/helpers.py,sha256=vJPaF9qU71-w-LUe4NaRowioIJG3GfX4MObyitHJC6U,7388
66
+ ingestr/src/hubspot/settings.py,sha256=hXnIhE4_FlhtgUiCjMLGGZJ_lBlBQJ0YWi6lHJ6Q3WU,2234
65
67
  ingestr/src/kafka/__init__.py,sha256=wMCXdiraeKd1Kssi9WcVCGZaNGm2tJEtnNyuB4aR5_k,3541
66
68
  ingestr/src/kafka/helpers.py,sha256=V9WcVn3PKnEpggArHda4vnAcaV8VDuh__dSmRviJb5Y,7502
67
69
  ingestr/src/kinesis/__init__.py,sha256=u5ThH1y8uObZKXgIo71em1UnX6MsVHWOjcf1jKqKbE8,6205
@@ -119,8 +121,8 @@ ingestr/testdata/delete_insert_part2.csv,sha256=B_KUzpzbNdDY_n7wWop1mT2cz36TmayS
119
121
  ingestr/testdata/merge_expected.csv,sha256=DReHqWGnQMsf2PBv_Q2pfjsgvikYFnf1zYcQZ7ZqYN0,276
120
122
  ingestr/testdata/merge_part1.csv,sha256=Pw8Z9IDKcNU0qQHx1z6BUf4rF_-SxKGFOvymCt4OY9I,185
121
123
  ingestr/testdata/merge_part2.csv,sha256=T_GiWxA81SN63_tMOIuemcvboEFeAmbKc7xRXvL9esw,287
122
- ingestr-0.13.24.dist-info/METADATA,sha256=qzKyEOTPIb6cTD49Q6zC-bqSn7ax45JmCKcGh0jtJRw,13659
123
- ingestr-0.13.24.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
124
- ingestr-0.13.24.dist-info/entry_points.txt,sha256=oPJy0KBnPWYjDtP1k8qwAihcTLHSZokSQvRAw_wtfJM,46
125
- ingestr-0.13.24.dist-info/licenses/LICENSE.md,sha256=cW8wIhn8HFE-KLStDF9jHQ1O_ARWP3kTpk_-eOccL24,1075
126
- ingestr-0.13.24.dist-info/RECORD,,
124
+ ingestr-0.13.26.dist-info/METADATA,sha256=cPGZ4OGDE-jWfIm8D5gEOLCGyMOioafvTC27zlxeHvo,13659
125
+ ingestr-0.13.26.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
126
+ ingestr-0.13.26.dist-info/entry_points.txt,sha256=oPJy0KBnPWYjDtP1k8qwAihcTLHSZokSQvRAw_wtfJM,46
127
+ ingestr-0.13.26.dist-info/licenses/LICENSE.md,sha256=cW8wIhn8HFE-KLStDF9jHQ1O_ARWP3kTpk_-eOccL24,1075
128
+ ingestr-0.13.26.dist-info/RECORD,,