Skip to content

Commit 3a7609c

Browse files
Merge branch 'main' into edgarrmondragon/refactor/capabilities
2 parents a89a2bc + edcbb1a commit 3a7609c

File tree

32 files changed

+544
-91
lines changed

32 files changed

+544
-91
lines changed

.github/ISSUE_TEMPLATE/bug.yml

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,18 +67,10 @@ body:
6767
id: what-happened
6868
attributes:
6969
label: Description
70-
description: Describe what you were trying to get done
70+
description: Describe what you were trying to get done, what you expected to happen, and what actually happened. Include any error messages you received, or a snippet of the code that caused the issue.
7171
placeholder: Tell us what happened, what went wrong, and what you expected to happen
7272
validations:
7373
required: true
74-
- type: textarea
75-
id: failing-code
76-
attributes:
77-
label: Code
78-
description: Paste the failing code and/or traceback, if applicable
79-
render: Python
80-
validations:
81-
required: false
8274
- type: input
8375
id: slack_or_linen
8476
attributes:

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@ repos:
3737
- id: trailing-whitespace
3838

3939
- repo: https://github.com/python-jsonschema/check-jsonschema
40-
rev: 0.29.4
40+
rev: 0.30.0
4141
hooks:
4242
- id: check-dependabot
4343
- id: check-github-workflows
4444
- id: check-readthedocs
4545

4646
- repo: https://github.com/astral-sh/ruff-pre-commit
47-
rev: v0.8.0
47+
rev: v0.8.1
4848
hooks:
4949
- id: ruff
5050
args: [--fix, --exit-non-zero-on-fix, --show-fixes]

cookiecutter/tap-template/{{cookiecutter.tap_id}}/{{cookiecutter.library_name}}/graphql-client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import decimal
56
import typing as t
67

78
import requests # noqa: TCH002
@@ -61,7 +62,7 @@ def parse_response(self, response: requests.Response) -> t.Iterable[dict]:
6162
Each record from the source.
6263
"""
6364
# TODO: Parse response body and return a set of records.
64-
resp_json = response.json()
65+
resp_json = response.json(parse_float=decimal.Decimal)
6566
yield from resp_json.get("<TODO>")
6667

6768
def post_process(

cookiecutter/tap-template/{{cookiecutter.tap_id}}/{{cookiecutter.library_name}}/rest-client.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import decimal
56
import typing as t
67
{% if cookiecutter.auth_method in ("OAuth2", "JWT") -%}
78
from functools import cached_property
@@ -204,7 +205,10 @@ def parse_response(self, response: requests.Response) -> t.Iterable[dict]:
204205
Each record from the source.
205206
"""
206207
# TODO: Parse response body and return a set of records.
207-
yield from extract_jsonpath(self.records_jsonpath, input=response.json())
208+
yield from extract_jsonpath(
209+
self.records_jsonpath,
210+
input=response.json(parse_float=decimal.Decimal),
211+
)
208212

209213
def post_process(
210214
self,

docs/guides/sql-tap.md

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,19 @@ from my_sqlalchemy_dialect import VectorType
2424

2525

2626
class CustomSQLToJSONSchema(SQLToJSONSchema):
27-
@SQLToJSONSchema.to_jsonschema.register
27+
@functools.singledispatchmethod
28+
def to_jsonschema(self, column_type):
29+
return super().to_jsonschema(column_type)
30+
31+
@to_jsonschema.register
2832
def custom_number_to_jsonschema(self, column_type: Numeric):
2933
"""Override the default mapping for NUMERIC columns.
3034
3135
For example, a scale of 4 translates to a multipleOf 0.0001.
3236
"""
3337
return {"type": ["number"], "multipleOf": 10**-column_type.scale}
3438

35-
@SQLToJSONSchema.to_jsonschema.register(VectorType)
39+
@to_jsonschema.register(VectorType)
3640
def vector_to_json_schema(self, column_type):
3741
"""Custom vector to JSON schema."""
3842
return th.ArrayType(th.NumberType()).to_dict()
@@ -42,7 +46,7 @@ class CustomSQLToJSONSchema(SQLToJSONSchema):
4246
You can also use a type annotation to specify the type of the column when registering a new method:
4347
4448
```python
45-
@SQLToJSONSchema.to_jsonschema.register
49+
@to_jsonschema.register
4650
def vector_to_json_schema(self, column_type: VectorType):
4751
return th.ArrayType(th.NumberType()).to_dict()
4852
```
@@ -52,7 +56,23 @@ Then, you need to use your custom type mapping in your connector:
5256

5357
```python
5458
class MyConnector(SQLConnector):
55-
@functools.cached_property
56-
def sql_to_jsonschema(self):
57-
return CustomSQLToJSONSchema()
59+
sql_to_jsonschema_converter = CustomSQLToJSONSchema
60+
```
61+
62+
### Adapting the type mapping based on user configuration
63+
64+
65+
If your type mapping depends on some user-defined configuration, you can also override the `from_config` method to pass the configuration to your custom type mapping:
66+
67+
```python
68+
class ConfiguredSQLToJSONSchema(SQLToJSONSchema):
69+
def __init__(self, *, my_custom_setting: str, **kwargs):
70+
super().__init__(**kwargs)
71+
self.my_custom_setting = my_custom_setting
72+
73+
@classmethod
74+
def from_config(cls, config: dict):
75+
return cls(my_custom_setting=config.get("my_custom_setting", "default_value"))
5876
```
77+
78+
Then, you can use your custom type mapping in your connector as in the previous example.

samples/sample_tap_gitlab/gitlab_rest_streams.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def partitions(self) -> list[dict]:
7474
if "{project_id}" in self.path:
7575
return [
7676
{"project_id": pid}
77-
for pid in t.cast(list, self.config.get("project_ids"))
77+
for pid in t.cast("list", self.config.get("project_ids"))
7878
]
7979
if "{group_id}" in self.path:
8080
if "group_ids" not in self.config:
@@ -84,7 +84,8 @@ def partitions(self) -> list[dict]:
8484
)
8585
raise ValueError(msg)
8686
return [
87-
{"group_id": gid} for gid in t.cast(list, self.config.get("group_ids"))
87+
{"group_id": gid}
88+
for gid in t.cast("list", self.config.get("group_ids"))
8889
]
8990
msg = (
9091
f"Could not detect partition type for Gitlab stream '{self.name}' "

samples/sample_target_sqlite/__init__.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
import datetime
6+
import sqlite3
57
import typing as t
68

79
from singer_sdk import SQLConnector, SQLSink, SQLTarget
@@ -10,6 +12,46 @@
1012
DB_PATH_CONFIG = "path_to_db"
1113

1214

15+
def adapt_date_iso(val):
16+
"""Adapt datetime.date to ISO 8601 date."""
17+
return val.isoformat()
18+
19+
20+
def adapt_datetime_iso(val):
21+
"""Adapt datetime.datetime to timezone-naive ISO 8601 date."""
22+
return val.isoformat()
23+
24+
25+
def adapt_datetime_epoch(val):
26+
"""Adapt datetime.datetime to Unix timestamp."""
27+
return int(val.timestamp())
28+
29+
30+
sqlite3.register_adapter(datetime.date, adapt_date_iso)
31+
sqlite3.register_adapter(datetime.datetime, adapt_datetime_iso)
32+
sqlite3.register_adapter(datetime.datetime, adapt_datetime_epoch)
33+
34+
35+
def convert_date(val):
36+
"""Convert ISO 8601 date to datetime.date object."""
37+
return datetime.date.fromisoformat(val.decode())
38+
39+
40+
def convert_datetime(val):
41+
"""Convert ISO 8601 datetime to datetime.datetime object."""
42+
return datetime.datetime.fromisoformat(val.decode())
43+
44+
45+
def convert_timestamp(val):
46+
"""Convert Unix epoch timestamp to datetime.datetime object."""
47+
return datetime.datetime.fromtimestamp(int(val), tz=datetime.timezone.utc)
48+
49+
50+
sqlite3.register_converter("date", convert_date)
51+
sqlite3.register_converter("datetime", convert_datetime)
52+
sqlite3.register_converter("timestamp", convert_timestamp)
53+
54+
1355
class SQLiteConnector(SQLConnector):
1456
"""The connector for SQLite.
1557

singer_sdk/connectors/sql.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,37 @@ class SQLToJSONSchema:
121121
This class provides a mapping from SQLAlchemy types to JSON Schema types.
122122
123123
.. versionadded:: 0.41.0
124+
.. versionchanged:: 0.43.0
125+
Added the :meth:`singer_sdk.connectors.sql.SQLToJSONSchema.from_config` class
126+
method.
124127
"""
125128

129+
@classmethod
130+
def from_config(cls: type[SQLToJSONSchema], config: dict) -> SQLToJSONSchema: # noqa: ARG003
131+
"""Create a new instance from a configuration dictionary.
132+
133+
Override this to instantiate this converter with values from the tap's
134+
configuration dictionary.
135+
136+
.. code-block:: python
137+
138+
class CustomSQLToJSONSchema(SQLToJSONSchema):
139+
def __init__(self, *, my_custom_option, **kwargs):
140+
super().__init__(**kwargs)
141+
self.my_custom_option = my_custom_option
142+
143+
@classmethod
144+
def from_config(cls, config):
145+
return cls(my_custom_option=config.get("my_custom_option"))
146+
147+
Args:
148+
config: The configuration dictionary.
149+
150+
Returns:
151+
A new instance of the class.
152+
"""
153+
return cls()
154+
126155
@functools.singledispatchmethod
127156
def to_jsonschema(self, column_type: sa.types.TypeEngine) -> dict: # noqa: ARG002, D102, PLR6301
128157
return th.StringType.type_dict # type: ignore[no-any-return]
@@ -453,6 +482,11 @@ class SQLConnector: # noqa: PLR0904
453482
#: The absolute maximum length for VARCHAR columns that the database supports.
454483
max_varchar_length: int | None = None
455484

485+
#: The SQL-to-JSON type mapper class for this SQL connector. Override this property
486+
#: with a subclass of :class:`~singer_sdk.connectors.sql.SQLToJSONSchema` to provide
487+
#: a custom mapping for your SQL dialect.
488+
sql_to_jsonschema_converter: type[SQLToJSONSchema] = SQLToJSONSchema
489+
456490
def __init__(
457491
self,
458492
config: dict | None = None,
@@ -493,7 +527,7 @@ def sql_to_jsonschema(self) -> SQLToJSONSchema:
493527
494528
.. versionadded:: 0.41.0
495529
"""
496-
return SQLToJSONSchema()
530+
return self.sql_to_jsonschema_converter.from_config(self.config)
497531

498532
@functools.cached_property
499533
def jsonschema_to_sql(self) -> JSONSchemaToSQL:
@@ -607,7 +641,7 @@ def get_sqlalchemy_url(self, config: dict[str, t.Any]) -> str: # noqa: PLR6301
607641
msg = "Could not find or create 'sqlalchemy_url' for connection."
608642
raise ConfigValidationError(msg)
609643

610-
return t.cast(str, config["sqlalchemy_url"])
644+
return t.cast("str", config["sqlalchemy_url"])
611645

612646
def to_jsonschema_type(
613647
self,
@@ -1395,7 +1429,7 @@ def _get_type_sort_key(
13951429

13961430
_len = int(getattr(sql_type, "length", 0) or 0)
13971431

1398-
_pytype = t.cast(type, sql_type.python_type)
1432+
_pytype = t.cast("type", sql_type.python_type)
13991433
if issubclass(_pytype, (str, bytes)):
14001434
return 900, _len
14011435
if issubclass(_pytype, datetime):

0 commit comments

Comments
 (0)