Skip to content

Commit bde1c0e

Browse files
authored
fully lint project with ruff (#254)
1 parent 33c584e commit bde1c0e

File tree

4 files changed

+59
-45
lines changed

4 files changed

+59
-45
lines changed

meltano.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ plugins:
1616
- about
1717
- stream-maps
1818
config:
19-
start_date: "2014-01-01T00:00:00Z"
20-
client_id: $TAP_EXACT_CLIENT_ID_V2
21-
client_secret: $TAP_EXACT_CLIENT_SECRET_V2
19+
start_date: "2025-03-25T00:00:00Z"
20+
client_id: $TAP_EXACT_CLIENT_ID
21+
client_secret: $TAP_EXACT_CLIENT_SECRET
2222
tokens_s3_bucket: $TAP_EXACT_TOKENS_S3_BUCKET
2323
tokens_s3_key: $TAP_EXACT_TOKENS_S3_KEY
2424
divisions: ["3490573", "2542158", "2119843", "2140191", "2140277", "2603668"]
2525
select:
26-
- gl_accounts.*
26+
- *.*
2727

2828
loaders:
2929
- name: target-jsonl

tap_exact/auth.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,21 @@
22

33
from __future__ import annotations
44

5-
from singer_sdk.authenticators import OAuthAuthenticator, SingletonMeta
6-
from singer_sdk.helpers._util import utc_now
7-
from singer_sdk.streams import RESTStream
8-
import backoff
9-
import requests
105
import json
6+
from typing import TYPE_CHECKING
7+
8+
import backoff
119
import boto3
10+
import requests
11+
from singer_sdk.authenticators import OAuthAuthenticator, SingletonMeta
12+
from singer_sdk.helpers._util import utc_now
13+
14+
if TYPE_CHECKING:
15+
from singer_sdk.streams import RESTStream
1216

1317

1418
class EmptyResponseError(Exception):
15-
"""Raised when the response is empty"""
19+
"""Raised when the response is empty."""
1620

1721

1822
# The SingletonMeta metaclass makes your streams reuse the same authenticator instance.
@@ -35,6 +39,7 @@ def __init__(self, stream: RESTStream) -> None:
3539
self.access_token = tokens["access_token"]
3640

3741
def download_tokens_from_s3(self) -> dict:
42+
"""Download tokens from S3."""
3843
return json.loads(
3944
self.s3.get_object(
4045
Bucket=self._config["tokens_s3_bucket"], Key=self._config["tokens_s3_key"] + "/tokens.json"
@@ -44,6 +49,7 @@ def download_tokens_from_s3(self) -> dict:
4449
)
4550

4651
def put_tokens_in_s3(self, tokens: dict) -> None:
52+
"""Put tokens in S3."""
4753
self.s3.put_object(
4854
Bucket=self._config["tokens_s3_bucket"],
4955
Key=self._config["tokens_s3_key"] + "/tokens.json",
@@ -66,6 +72,7 @@ def oauth_request_body(self) -> dict:
6672

6773
@backoff.on_exception(backoff.expo, EmptyResponseError, max_tries=5, factor=2)
6874
def update_access_token(self) -> None:
75+
"""Update the access token using the refresh token."""
6976
request_time = utc_now()
7077
auth_request_payload = self.oauth_request_payload
7178
token_response = requests.post(
@@ -77,9 +84,10 @@ def update_access_token(self) -> None:
7784

7885
try:
7986
if token_response.json().get("error_description") == "Rate limit exceeded: access_token not expired":
80-
return None
87+
return
8188
except Exception as e:
82-
raise EmptyResponseError(f"Failed converting response to a json, because response is empty")
89+
msg = "Failed converting response to a json, because response is empty"
90+
raise EmptyResponseError(msg) from e
8391

8492
try:
8593
token_response.raise_for_status()

tap_exact/client.py

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,25 @@
22

33
from __future__ import annotations
44

5+
import json
56
import sys
6-
from pathlib import Path
7-
from typing import Any, Callable, Iterable, Optional, Dict
87
import typing
9-
from datetime import datetime
8+
from pathlib import Path
9+
from typing import Any, Callable, Iterable
1010

1111
import requests
12-
from singer_sdk.helpers.jsonpath import extract_jsonpath
13-
from singer_sdk.pagination import BaseOffsetPaginator # noqa: TCH002
14-
from singer_sdk.streams import RESTStream
15-
from lxml import etree
16-
import json
1712
import xmltodict
13+
from lxml import etree
1814
from pendulum import parse
15+
from singer_sdk.helpers.jsonpath import extract_jsonpath
16+
from singer_sdk.pagination import BaseOffsetPaginator
17+
from singer_sdk.streams import RESTStream
1918

2019
from tap_exact.auth import ExactAuthenticator
2120

2221
if typing.TYPE_CHECKING:
22+
from datetime import datetime
23+
2324
from requests import Response
2425

2526
TPageToken = typing.TypeVar("TPageToken")
@@ -34,11 +35,12 @@
3435

3536

3637
class ExactPaginator(BaseOffsetPaginator):
37-
def __init__(self, stream, start_value, page_size) -> None:
38+
"""Exact pagination helper class."""
39+
def __init__(self, stream:ExactStream, start_value:int, page_size:int) -> None: # noqa: D107
3840
super().__init__(start_value=start_value, page_size=page_size)
3941
self.stream = stream
4042

41-
def has_more(self, response: Response) -> bool: # noqa: ARG002
43+
def has_more(self, response: Response) -> bool:
4244
"""Override this method to check if the endpoint has any pages left.
4345
4446
Args:
@@ -49,8 +51,9 @@ def has_more(self, response: Response) -> bool: # noqa: ARG002
4951
"""
5052
data = self.stream.xml_to_dict(response)
5153
link = data.get("feed", {}).get("link", [])
52-
if type(link) == list:
54+
if type(link) is list:
5355
return "next" in [item.get("@rel", "") for item in link]
56+
return None
5457

5558
def get_next(self, response: Response) -> TPageToken | None:
5659
"""Get the next pagination token or index from the API response.
@@ -65,24 +68,26 @@ def get_next(self, response: Response) -> TPageToken | None:
6568
data = self.stream.xml_to_dict(response)
6669
link = data.get("feed", {}).get("link", [])
6770
if "next" in [item.get("@rel", "") for item in link]:
68-
next_link = [item["@href"] for item in link if item["@rel"] == "next"][0]
69-
next_page_token = next_link.split("&")[-1].split("=")[-1]
70-
return next_page_token
71+
next_link = next(item["@href"] for item in link if item["@rel"] == "next")
72+
return next_link.split("&")[-1].split("=")[-1]
73+
return None
7174

7275

7376
class ExactStream(RESTStream):
7477
"""Exact stream class."""
7578

7679
@property
7780
def partitions(self) -> list[dict] | None:
81+
"""Return a list of partitions, or None if the stream is not partitioned."""
7882
return [{"division": division} for division in self.config["divisions"]]
7983

8084
@property
8185
def url_base(self) -> str:
8286
"""Return the API URL root, configurable via tap settings."""
83-
return f"https://start.exactonline.nl/api/v1"
87+
return "https://start.exactonline.nl/api/v1"
8488

8589
def get_url(self, context: dict | None) -> str:
90+
"""Return the URL for the API request."""
8691
return f"{self.url_base}/{context['division']}{self.path}"
8792

8893
@cached_property
@@ -94,14 +99,16 @@ def authenticator(self) -> _Auth:
9499
"""
95100
return ExactAuthenticator(self)
96101

97-
def get_starting_time(self, context):
102+
def get_starting_time(self, context:dict) -> datetime:
103+
"""Return the starting timestamp for the stream."""
98104
start_date = self.config.get("start_date")
99105
if start_date:
100106
start_date = parse(self.config.get("start_date"))
101107
replication_key = self.get_starting_timestamp(context)
102108
return replication_key or start_date
103109

104-
def get_url_params(self, context: Optional[dict], next_page_token) -> Dict[str, Any]:
110+
def get_url_params(self, context: dict | None, next_page_token:str) -> dict[str, Any]:
111+
"""Return a dictionary of parameters to use in the API request."""
105112
params: dict = {}
106113
if self.select:
107114
params["$select"] = self.select
@@ -117,15 +124,16 @@ def get_new_paginator(self) -> BaseOffsetPaginator:
117124
"""Create a new pagination helper instance."""
118125
return ExactPaginator(self, start_value=None, page_size=60)
119126

120-
def xml_to_dict(self, response):
127+
def xml_to_dict(self, response: requests.Response) -> dict:
128+
"""Convert xml response to dict."""
121129
try:
122130
# clean invalid xml characters
123131
my_parser = etree.XMLParser(recover=True)
124-
xml = etree.fromstring(response.content, parser=my_parser)
132+
xml = etree.fromstring(response.content, parser=my_parser) # noqa: S320
125133
cleaned_xml_string = etree.tostring(xml)
126134
# parse xml to dict
127135
data = json.loads(json.dumps(xmltodict.parse(cleaned_xml_string)))
128-
except:
136+
except: # noqa: E722
129137
data = json.loads(json.dumps(xmltodict.parse(response.content.decode("utf-8-sig").encode("utf-8"))))
130138
return data
131139

@@ -159,7 +167,7 @@ def post_process(
159167
new_content = {}
160168
for key, value in content.items():
161169
new_key = key[2:]
162-
if type(value) == str:
170+
if type(value) is str:
163171
new_content[new_key] = value
164172
elif not value or value.get("@m:null") == "true":
165173
new_content[new_key] = None
@@ -176,11 +184,11 @@ def post_process(
176184
new_content[new_key] = float(value.get("#text"))
177185
else:
178186
new_content[new_key] = value.get("#text", None)
179-
row = new_content
180-
return row
187+
return new_content
181188

182189
@property
183-
def select(self):
190+
def select(self) -> str:
191+
"""Return the select query parameter."""
184192
return ",".join(self.schema["properties"].keys())
185193

186194

@@ -191,22 +199,21 @@ def get_new_paginator(self) -> BaseOffsetPaginator:
191199
"""Create a new pagination helper instance."""
192200
return ExactPaginator(self, start_value=None, page_size=1000)
193201

194-
def get_starting_time(self, context):
202+
def get_starting_time(self, context:str) -> int:
203+
"""Return the starting timestamp for the stream."""
195204
state = self.get_context_state(context)
196205
rep_key = None
197-
if "replication_key_value" in state.keys():
206+
if "replication_key_value" in state:
198207
rep_key = state["replication_key_value"]
199208
return rep_key or 1
200209

201-
def get_url_params(self, context: Optional[dict], next_page_token) -> Dict[str, Any]:
210+
def get_url_params(self, context: dict | None, next_page_token:int) -> dict[str, Any]:
211+
"""Return a dictionary of parameters to use in the API request."""
202212
params: dict = {}
203213
if self.select:
204214
params["$select"] = self.select
205215
start_timestamp = self.get_starting_time(context)
206-
if start_timestamp == 1:
207-
date_filter = f"Timestamp gt {start_timestamp}"
208-
else:
209-
date_filter = f"Timestamp gt {start_timestamp}L"
216+
date_filter = f"Timestamp gt {start_timestamp}" if start_timestamp == 1 else f"Timestamp gt {start_timestamp}L"
210217
params["$filter"] = date_filter
211218
if next_page_token:
212219
params["$skiptoken"] = next_page_token

tap_exact/tap.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
from __future__ import annotations
44

55
from singer_sdk import Tap
6-
from singer_sdk.typing import DateTimeType, StringType, Property, PropertiesList, ArrayType
6+
from singer_sdk.typing import ArrayType, DateTimeType, PropertiesList, Property, StringType
77

8-
# TODO: Import your custom stream types here:
98
from tap_exact import streams
109

1110

0 commit comments

Comments
 (0)