ingestr 0.13.64__py3-none-any.whl → 0.13.65__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.
@@ -20,13 +20,15 @@ def attio_source(
20
20
  "created_at": {"data_type": "timestamp", "partition": True},
21
21
  },
22
22
  )
23
+ # https://docs.attio.com/rest-api/endpoint-reference/objects/list-objects - does not support pagination
23
24
  def fetch_objects() -> Iterator[dict]:
24
25
  if len(params) != 0:
25
26
  raise ValueError("Objects table must be in the format `objects`")
26
27
 
27
28
  path = "objects"
28
- yield attio_client.fetch_data(path, "get")
29
+ yield attio_client.fetch_all(path, "get")
29
30
 
31
+ # https://docs.attio.com/rest-api/endpoint-reference/records/list-records
30
32
  @dlt.resource(
31
33
  name="records",
32
34
  write_disposition="replace",
@@ -39,12 +41,12 @@ def attio_source(
39
41
  raise ValueError(
40
42
  "Records table must be in the format `records:{object_api_slug}`"
41
43
  )
42
-
43
44
  object_id = params[0]
44
45
  path = f"objects/{object_id}/records/query"
45
46
 
46
- yield attio_client.fetch_data(path, "post")
47
+ yield attio_client.fetch_paginated(path, "post")
47
48
 
49
+ # https://docs.attio.com/rest-api/endpoint-reference/lists/list-all-lists -- does not support pagination
48
50
  @dlt.resource(
49
51
  name="lists",
50
52
  write_disposition="replace",
@@ -54,8 +56,9 @@ def attio_source(
54
56
  )
55
57
  def fetch_lists() -> Iterator[dict]:
56
58
  path = "lists"
57
- yield attio_client.fetch_data(path, "get")
59
+ yield attio_client.fetch_all(path, "get")
58
60
 
61
+ # https://docs.attio.com/rest-api/endpoint-reference/entries/list-entries
59
62
  @dlt.resource(
60
63
  name="list_entries",
61
64
  write_disposition="replace",
@@ -70,7 +73,7 @@ def attio_source(
70
73
  )
71
74
  path = f"lists/{params[0]}/entries/query"
72
75
 
73
- yield attio_client.fetch_data(path, "post")
76
+ yield attio_client.fetch_paginated(path, "post")
74
77
 
75
78
  @dlt.resource(
76
79
  name="all_list_entries",
@@ -85,10 +88,10 @@ def attio_source(
85
88
  "All list entries table must be in the format `all_list_entries:{object_api_slug}`"
86
89
  )
87
90
  path = "lists"
88
- for lst in attio_client.fetch_data(path, "get"):
91
+ for lst in attio_client.fetch_all(path, "get"):
89
92
  if params[0] in lst["parent_object"]:
90
93
  path = f"lists/{lst['id']['list_id']}/entries/query"
91
- yield from attio_client.fetch_data(path, "post")
94
+ yield from attio_client.fetch_paginated(path, "post")
92
95
 
93
96
  return (
94
97
  fetch_objects,
@@ -10,42 +10,53 @@ class AttioClient:
10
10
  }
11
11
  self.client = create_client()
12
12
 
13
- def fetch_data(self, path: str, method: str, limit: int = 1000, params=None):
13
+ def fetch_paginated(self, path: str, method: str, limit: int = 1000, params=None):
14
14
  url = f"{self.base_url}/{path}"
15
15
  if params is None:
16
16
  params = {}
17
17
  offset = 0
18
18
  while True:
19
- query_params = {**params, "limit": limit, "offset": offset}
19
+ query_params = {"limit": limit, "offset": offset, **params}
20
20
  if method == "get":
21
21
  response = self.client.get(
22
22
  url, headers=self.headers, params=query_params
23
23
  )
24
24
  else:
25
- response = self.client.post(
26
- url, headers=self.headers, params=query_params
27
- )
25
+ json_body = {**params, "limit": limit, "offset": offset}
26
+ response = self.client.post(url, headers=self.headers, json=json_body)
28
27
 
29
28
  if response.status_code != 200:
30
29
  raise Exception(f"HTTP {response.status_code} error: {response.text}")
31
30
 
32
31
  response_data = response.json()
33
32
  if "data" not in response_data:
34
- print(f"API Response: {response_data}")
35
33
  raise Exception(
36
34
  "Attio API returned a response without the expected data"
37
35
  )
38
36
 
39
37
  data = response_data["data"]
40
-
41
38
  for item in data:
42
39
  flat_item = flatten_item(item)
43
40
  yield flat_item
44
-
45
41
  if len(data) < limit:
46
42
  break
43
+
47
44
  offset += limit
48
45
 
46
+ def fetch_all(self, path: str, method: str = "get", params=None):
47
+ url = f"{self.base_url}/{path}"
48
+ params = params or {}
49
+
50
+ if method == "get":
51
+ response = self.client.get(url, headers=self.headers, params=params)
52
+ else:
53
+ response = self.client.post(url, headers=self.headers, json=params)
54
+
55
+ response.raise_for_status()
56
+ data = response.json().get("data", [])
57
+ for item in data:
58
+ yield flatten_item(item)
59
+
49
60
 
50
61
  def flatten_item(item: dict) -> dict:
51
62
  if "id" in item:
ingestr/src/buildinfo.py CHANGED
@@ -1 +1 @@
1
- version = "v0.13.64"
1
+ version = "v0.13.65"
@@ -1,38 +1,9 @@
1
- from typing import Any, Dict, Iterable, Iterator, Optional
1
+ from typing import Any, Dict, Iterable, Iterator
2
2
 
3
3
  import dlt
4
4
  import pendulum
5
- import requests
6
-
7
- LINEAR_GRAPHQL_ENDPOINT = "https://api.linear.app/graphql"
8
-
9
-
10
- def _graphql(
11
- api_key: str, query: str, variables: Optional[Dict[str, Any]] = None
12
- ) -> Dict[str, Any]:
13
- headers = {"Authorization": api_key, "Content-Type": "application/json"}
14
- response = requests.post(
15
- LINEAR_GRAPHQL_ENDPOINT,
16
- json={"query": query, "variables": variables or {}},
17
- headers=headers,
18
- )
19
- response.raise_for_status()
20
- payload = response.json()
21
- if "errors" in payload:
22
- raise ValueError(str(payload["errors"]))
23
- return payload["data"]
24
-
25
-
26
- def _paginate(api_key: str, query: str, root: str) -> Iterator[Dict[str, Any]]:
27
- cursor: Optional[str] = None
28
- while True:
29
- data = _graphql(api_key, query, {"cursor": cursor})[root]
30
- for item in data["nodes"]:
31
- yield item
32
- if not data["pageInfo"]["hasNextPage"]:
33
- break
34
- cursor = data["pageInfo"]["endCursor"]
35
5
 
6
+ from .helpers import _paginate, _normalize_issue, _normalize_team
36
7
 
37
8
  ISSUES_QUERY = """
38
9
  query Issues($cursor: String) {
@@ -43,6 +14,17 @@ query Issues($cursor: String) {
43
14
  description
44
15
  createdAt
45
16
  updatedAt
17
+ creator { id }
18
+ assignee { id}
19
+ state { id}
20
+ labels { nodes { id } }
21
+ cycle { id}
22
+ project { id }
23
+ subtasks: children { nodes { id title } }
24
+ comments(first: 250) { nodes { id body } }
25
+ priority
26
+ attachments { nodes { id } }
27
+ subscribers { nodes { id } }
46
28
  }
47
29
  pageInfo { hasNextPage endCursor }
48
30
  }
@@ -58,6 +40,10 @@ query Projects($cursor: String) {
58
40
  description
59
41
  createdAt
60
42
  updatedAt
43
+ health
44
+ priority
45
+ targetDate
46
+ lead { id }
61
47
  }
62
48
  pageInfo { hasNextPage endCursor }
63
49
  }
@@ -72,6 +58,11 @@ query Teams($cursor: String) {
72
58
  name
73
59
  key
74
60
  description
61
+ updatedAt
62
+ createdAt
63
+ memberships { nodes { id } }
64
+ members { nodes { id } }
65
+ projects { nodes { id } }
75
66
  }
76
67
  pageInfo { hasNextPage endCursor }
77
68
  }
@@ -124,7 +115,7 @@ def linear_source(
124
115
  for item in _paginate(api_key, ISSUES_QUERY, "issues"):
125
116
  if pendulum.parse(item["updatedAt"]) >= current_start_date:
126
117
  if pendulum.parse(item["updatedAt"]) <= current_end_date:
127
- yield item
118
+ yield _normalize_issue(item)
128
119
 
129
120
  @dlt.resource(name="projects", primary_key="id", write_disposition="merge")
130
121
  def projects(
@@ -152,8 +143,29 @@ def linear_source(
152
143
  yield item
153
144
 
154
145
  @dlt.resource(name="teams", primary_key="id", write_disposition="merge")
155
- def teams() -> Iterator[Dict[str, Any]]:
156
- yield from _paginate(api_key, TEAMS_QUERY, "teams")
146
+ def teams( updated_at: dlt.sources.incremental[str] = dlt.sources.incremental(
147
+ "updatedAt",
148
+ initial_value=start_date.isoformat(),
149
+ end_value=end_date.isoformat() if end_date else None,
150
+ range_start="closed",
151
+ range_end="closed",
152
+ ),) -> Iterator[Dict[str, Any]]:
153
+ print(start_date)
154
+ if updated_at.last_value:
155
+ current_start_date = pendulum.parse(updated_at.last_value)
156
+ else:
157
+ current_start_date = pendulum.parse(start_date)
158
+ print(current_start_date)
159
+
160
+ if updated_at.end_value:
161
+ current_end_date = pendulum.parse(updated_at.end_value)
162
+ else:
163
+ current_end_date = pendulum.now(tz="UTC")
164
+
165
+ for item in _paginate(api_key, TEAMS_QUERY, "teams"):
166
+ if pendulum.parse(item["updatedAt"]) >= current_start_date:
167
+ if pendulum.parse(item["updatedAt"]) <= current_end_date:
168
+ yield _normalize_team(item)
157
169
 
158
170
  @dlt.resource(name="users", primary_key="id", write_disposition="merge")
159
171
  def users(
@@ -0,0 +1,60 @@
1
+ import json
2
+ from typing import Any, Dict, Iterator, Optional
3
+
4
+ import requests
5
+
6
+ LINEAR_GRAPHQL_ENDPOINT = "https://api.linear.app/graphql"
7
+
8
+ def _graphql(
9
+ api_key: str, query: str, variables: Optional[Dict[str, Any]] = None
10
+ ) -> Dict[str, Any]:
11
+ headers = {"Authorization": api_key, "Content-Type": "application/json"}
12
+ response = requests.post(
13
+ LINEAR_GRAPHQL_ENDPOINT,
14
+ json={"query": query, "variables": variables or {}},
15
+ headers=headers,
16
+ )
17
+ response.raise_for_status()
18
+ payload = response.json()
19
+ if "errors" in payload:
20
+ raise ValueError(str(payload["errors"]))
21
+ return payload["data"]
22
+
23
+ def _paginate(api_key: str, query: str, root: str) -> Iterator[Dict[str, Any]]:
24
+ cursor: Optional[str] = None
25
+ while True:
26
+ data = _graphql(api_key, query, {"cursor": cursor})[root]
27
+ for item in data["nodes"]:
28
+ yield item
29
+ if not data["pageInfo"]["hasNextPage"]:
30
+ break
31
+ cursor = data["pageInfo"]["endCursor"]
32
+
33
+ def _normalize_issue(item: Dict[str, Any]) -> Dict[str, Any]:
34
+ field_mapping = {
35
+ "assignee": "assignee_id",
36
+ "creator": "creator_id",
37
+ "state": "state_id",
38
+ "cycle": "cycle_id",
39
+ "project": "project_id",
40
+ }
41
+ for key, value in field_mapping.items():
42
+ if item.get(key):
43
+ item[value] = item[key]["id"]
44
+ del item[key]
45
+ else:
46
+ item[value] = None
47
+ del item[key]
48
+ json_fields = ["comments", "subscribers", "attachments", "labels", "subtasks","projects", "memberships", "members"]
49
+ for field in json_fields:
50
+ if item.get(field):
51
+ item[f"{field}"] = item[field].get("nodes", [])
52
+
53
+ return item
54
+
55
+ def _normalize_team(item: Dict[str, Any]) -> Dict[str, Any]:
56
+ json_fields = ["memberships", "members", "projects"]
57
+ for field in json_fields:
58
+ if item.get(field):
59
+ item[f"{field}"] = item[field].get("nodes", [])
60
+ return item
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ingestr
3
- Version: 0.13.64
3
+ Version: 0.13.65
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,7 +2,7 @@ ingestr/conftest.py,sha256=Q03FIJIZpLBbpj55cfCHIKEjc1FCvWJhMF2cidUJKQU,1748
2
2
  ingestr/main.py,sha256=taDyHyaVSpB17iNLl8zA0gmr4CqDO-MSTQX1CaRBB9U,26364
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=PuIV-cer5xdwwdtvFJ-yA_CjyfoO-SckFPSRHGfPM4I,21
5
+ ingestr/src/buildinfo.py,sha256=RDAMEy23q-LmXSYODsQMAghvn5syzLPD4mQO_GpxC0c,21
6
6
  ingestr/src/destinations.py,sha256=ZJTbTn1K9oXinL19dTGQDUrft5C9fjrpSlTw1CLQhuM,21749
7
7
  ingestr/src/errors.py,sha256=Ufs4_DfE77_E3vnA1fOQdi6cmuLVNm7_SbFLkL1XPGk,686
8
8
  ingestr/src/factory.py,sha256=AJCvlK4M1sIpAAks1K-xsR_uxziIxru74mj572zixhg,6546
@@ -31,8 +31,8 @@ ingestr/src/arrow/__init__.py,sha256=8fEntgHseKjFMiPQIzxYzw_raicNsEgnveLi1IzBca0
31
31
  ingestr/src/asana_source/__init__.py,sha256=QwQTCb5PXts8I4wLHG9UfRP-5ChfjSe88XAVfxMV5Ag,8183
32
32
  ingestr/src/asana_source/helpers.py,sha256=PukcdDQWIGqnGxuuobbLw4hUy4-t6gxXg_XywR7Lg9M,375
33
33
  ingestr/src/asana_source/settings.py,sha256=-2tpdkwh04RvLKFvwQodnFLYn9MaxOO1hsebGnDQMTU,2829
34
- ingestr/src/attio/__init__.py,sha256=D21EK02HQxDtHoJHVHtM01sU4ZSK26WzFjLqqpVDdK0,2859
35
- ingestr/src/attio/helpers.py,sha256=QvB-0BV_Z-cvMTBZDwOCuhxY1cB5PraPdrDkNyQ5TsM,1715
34
+ ingestr/src/attio/__init__.py,sha256=CLejJjp5vGkt6r18nfNNZ-Xjc1SZgQ5IlcBW5XFQR90,3243
35
+ ingestr/src/attio/helpers.py,sha256=fCySmG5E6Iyh3Nm9a-HGbHNedxPH_2_otXYMTQsCibw,2185
36
36
  ingestr/src/chess/__init__.py,sha256=y0Q8aKBigeKf3N7wuB_gadMQjVJzBPUT8Jhp1ObEWjk,6812
37
37
  ingestr/src/chess/helpers.py,sha256=v1HTImOMjAF7AzZUPDIuHu00e7ut0o5y1kWcVYo4QZw,549
38
38
  ingestr/src/chess/settings.py,sha256=p0RlCGgtXUacPDEvZmwzSWmzX0Apj1riwfz-nrMK89k,158
@@ -81,7 +81,8 @@ ingestr/src/kinesis/helpers.py,sha256=SO2cFmWNGcykUYmjHdfxWsOQSkLQXyhFtfWnkcUOM0
81
81
  ingestr/src/klaviyo/__init__.py,sha256=o_noUgbxLk36s4f9W56_ibPorF0n7kVapPUlV0p-jfA,7875
82
82
  ingestr/src/klaviyo/client.py,sha256=tPj79ia7AW0ZOJhzlKNPCliGbdojRNwUFp8HvB2ym5s,7434
83
83
  ingestr/src/klaviyo/helpers.py,sha256=_i-SHffhv25feLDcjy6Blj1UxYLISCwVCMgGtrlnYHk,496
84
- ingestr/src/linear/__init__.py,sha256=ITMLsuLjrGYx3bTsEK1cdPUkowJYCdAII_ucci_lGDQ,5422
84
+ ingestr/src/linear/__init__.py,sha256=attlRyodShvAZ5dmDJXgoKrYhwElpLMQTSaRaAGEqC0,5941
85
+ ingestr/src/linear/helpers.py,sha256=VR_CBgTfMVTH6ULcSLKyrssGoJpJx8VFZrmBeYZzFfc,1995
85
86
  ingestr/src/linkedin_ads/__init__.py,sha256=CAPWFyV24loziiphbLmODxZUXZJwm4JxlFkr56q0jfo,1855
86
87
  ingestr/src/linkedin_ads/dimension_time_enum.py,sha256=EmHRdkFyTAfo4chGjThrwqffWJxmAadZMbpTvf0xkQc,198
87
88
  ingestr/src/linkedin_ads/helpers.py,sha256=eUWudRVlXl4kqIhfXQ1eVsUpZwJn7UFqKSpnbLfxzds,4498
@@ -146,8 +147,8 @@ ingestr/testdata/merge_expected.csv,sha256=DReHqWGnQMsf2PBv_Q2pfjsgvikYFnf1zYcQZ
146
147
  ingestr/testdata/merge_part1.csv,sha256=Pw8Z9IDKcNU0qQHx1z6BUf4rF_-SxKGFOvymCt4OY9I,185
147
148
  ingestr/testdata/merge_part2.csv,sha256=T_GiWxA81SN63_tMOIuemcvboEFeAmbKc7xRXvL9esw,287
148
149
  ingestr/tests/unit/test_smartsheets.py,sha256=eiC2CCO4iNJcuN36ONvqmEDryCA1bA1REpayHpu42lk,5058
149
- ingestr-0.13.64.dist-info/METADATA,sha256=QaBM1vQbQPYKgzDga2AY6uKibQbd1SkavE3dw92Pw-o,15027
150
- ingestr-0.13.64.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
151
- ingestr-0.13.64.dist-info/entry_points.txt,sha256=oPJy0KBnPWYjDtP1k8qwAihcTLHSZokSQvRAw_wtfJM,46
152
- ingestr-0.13.64.dist-info/licenses/LICENSE.md,sha256=cW8wIhn8HFE-KLStDF9jHQ1O_ARWP3kTpk_-eOccL24,1075
153
- ingestr-0.13.64.dist-info/RECORD,,
150
+ ingestr-0.13.65.dist-info/METADATA,sha256=PWjju7xvb3O9Ya0IRwj-zti34_sN6sGSY3YbROP3KKs,15027
151
+ ingestr-0.13.65.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
152
+ ingestr-0.13.65.dist-info/entry_points.txt,sha256=oPJy0KBnPWYjDtP1k8qwAihcTLHSZokSQvRAw_wtfJM,46
153
+ ingestr-0.13.65.dist-info/licenses/LICENSE.md,sha256=cW8wIhn8HFE-KLStDF9jHQ1O_ARWP3kTpk_-eOccL24,1075
154
+ ingestr-0.13.65.dist-info/RECORD,,