Skip to content

Commit 493ba22

Browse files
Merge branch 'main' into http-connector
2 parents fe06ab2 + b0a8621 commit 493ba22

29 files changed

+355
-211
lines changed

.github/workflows/constraints.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
pip==23.2
1+
pip==23.2.1
22
poetry==1.5.1
33
pre-commit==3.3.3
44
nox==2023.4.22

.github/workflows/test.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ env:
3737

3838
jobs:
3939
tests:
40-
name: Test on ${{ matrix.python-version }} (${{ matrix.session }}) / ${{ matrix.os }}
40+
name: "Test on ${{ matrix.python-version }} (${{ matrix.session }}) / ${{ matrix.os }} / SQLAlchemy: ${{ matrix.sqlalchemy }}"
4141
runs-on: ${{ matrix.os }}
4242
env:
4343
NOXSESSION: ${{ matrix.session }}
@@ -47,9 +47,11 @@ jobs:
4747
session: [tests]
4848
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
4949
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
50+
sqlalchemy: ["2.*"]
5051
include:
51-
- { session: doctest, python-version: "3.10", os: "ubuntu-latest" }
52-
- { session: mypy, python-version: "3.8", os: "ubuntu-latest" }
52+
- { session: tests, python-version: "3.11", os: "ubuntu-latest", sqlalchemy: "1.*" }
53+
- { session: doctest, python-version: "3.10", os: "ubuntu-latest", sqlalchemy: "2.*" }
54+
- { session: mypy, python-version: "3.8", os: "ubuntu-latest", sqlalchemy: "2.*" }
5355

5456
steps:
5557
- name: Check out the repository
@@ -86,6 +88,8 @@ jobs:
8688
nox --version
8789
8890
- name: Run Nox
91+
env:
92+
SQLALCHEMY_VERSION: ${{ matrix.sqlalchemy }}
8993
run: |
9094
nox --python=${{ matrix.python-version }}
9195

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@ repos:
3636
)$
3737
3838
- repo: https://github.com/python-jsonschema/check-jsonschema
39-
rev: 0.23.2
39+
rev: 0.23.3
4040
hooks:
4141
- id: check-dependabot
4242
- id: check-github-workflows
4343
- id: check-readthedocs
4444

4545
- repo: https://github.com/astral-sh/ruff-pre-commit
46-
rev: v0.0.277
46+
rev: v0.0.280
4747
hooks:
4848
- id: ruff
4949
args: [--fix, --exit-non-zero-on-fix, --show-fixes]

docs/deprecation.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ incompatible way, following their deprecation, as indicated in the
1111
[`RESTStream.get_new_paginator`](singer_sdk.RESTStream.get_new_paginator).
1212

1313
See the [migration guide](./guides/pagination-classes.md) for more information.
14+
15+
- The `singer_sdk.testing.get_standard_tap_tests` and `singer_sdk.testing.get_standard_target_tests` functions will be removed. Replace them with `singer_sdk.testing.get_tap_test_class` and `singer_sdk.testing.get_target_test_class` functions respective to generate a richer test suite.

noxfile.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,14 @@ def tests(session: Session) -> None:
8686
session.install(".[s3]")
8787
session.install(*test_dependencies)
8888

89+
sqlalchemy_version = os.environ.get("SQLALCHEMY_VERSION")
90+
if sqlalchemy_version:
91+
# Bypass nox-poetry use of --constraint so we can install a version of
92+
# SQLAlchemy that doesn't match what's in poetry.lock.
93+
session.poetry.session.install( # type: ignore[attr-defined]
94+
f"sqlalchemy=={sqlalchemy_version}",
95+
)
96+
8997
try:
9098
session.run(
9199
"coverage",
@@ -96,9 +104,6 @@ def tests(session: Session) -> None:
96104
"-v",
97105
"--durations=10",
98106
*session.posargs,
99-
env={
100-
"SQLALCHEMY_WARN_20": "1",
101-
},
102107
)
103108
finally:
104109
if session.interactive:

poetry.lock

Lines changed: 122 additions & 132 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ memoization = ">=0.3.2,<0.5.0"
5252
jsonpath-ng = "^1.5.3"
5353
joblib = "^1.0.1"
5454
inflection = "^0.5.1"
55-
sqlalchemy = "^1.4"
55+
sqlalchemy = ">=1.4,<3.0"
5656
python-dotenv = ">=0.20,<0.22"
5757
typing-extensions = "^4.2.0"
5858
simplejson = "^3.17.6"
@@ -109,7 +109,6 @@ numpy = [
109109
{ version = ">=1.22", python = ">=3.8" },
110110
]
111111
requests-mock = "^1.10.0"
112-
sqlalchemy2-stubs = {version = "^0.0.2a32", allow-prereleases = true}
113112
types-jsonschema = "^4.17.0.6"
114113
types-python-dateutil = "^2.8.19"
115114
types-pytz = ">=2022.7.1.2,<2024.0.0.0"
@@ -133,9 +132,6 @@ exclude = ".*simpleeval.*"
133132

134133
[tool.pytest.ini_options]
135134
addopts = '-vvv --ignore=singer_sdk/helpers/_simpleeval.py -m "not external"'
136-
filterwarnings = [
137-
"error::sqlalchemy.exc.RemovedIn20Warning",
138-
]
139135
markers = [
140136
"external: Tests relying on external resources",
141137
"windows: Tests that only run on Windows",
@@ -191,9 +187,6 @@ fail_under = 82
191187
[tool.mypy]
192188
exclude = "tests"
193189
files = "singer_sdk"
194-
plugins = [
195-
"sqlalchemy.ext.mypy.plugin",
196-
]
197190
python_version = "3.8"
198191
warn_unused_configs = true
199192
warn_unused_ignores = true
@@ -216,11 +209,13 @@ requires = ["poetry-core>=1.0.0"]
216209
build-backend = "poetry.core.masonry.api"
217210

218211
[tool.poetry.scripts]
219-
pytest11 = { callable = "singer_sdk:testing.pytest_plugin", extras = ["testing"] }
212+
pytest11 = { reference = "singer_sdk:testing.pytest_plugin", extras = ["testing"], type = "console" }
220213

221214
[tool.ruff]
222215
exclude = [
223216
"cookiecutter/*",
217+
"singer_sdk/helpers/_simpleeval.py",
218+
"tests/core/test_simpleeval.py",
224219
]
225220
ignore = [
226221
"ANN101", # Missing type annotation for `self` in method

singer_sdk/connectors/sql.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ def get_sqlalchemy_url(self, config: t.Mapping[str, t.Any]) -> str:
197197
@staticmethod
198198
def to_jsonschema_type(
199199
sql_type: (
200-
str
200+
str # noqa: ANN401
201201
| sqlalchemy.types.TypeEngine
202202
| type[sqlalchemy.types.TypeEngine]
203203
| t.Any

singer_sdk/helpers/_compat.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,9 @@
1111
from importlib import metadata
1212
from typing import Protocol, final # noqa: ICN003
1313

14-
__all__ = ["metadata", "final", "Protocol"]
14+
if sys.version_info < (3, 9):
15+
import importlib_resources as resources
16+
else:
17+
from importlib import resources
18+
19+
__all__ = ["metadata", "final", "resources", "Protocol"]

singer_sdk/helpers/_flattening.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -252,15 +252,15 @@ def _flatten_schema( # noqa: C901
252252
else:
253253
items.append((new_key, v))
254254
elif len(v.values()) > 0:
255-
if list(v.values())[0][0]["type"] == "string":
256-
list(v.values())[0][0]["type"] = ["null", "string"]
257-
items.append((new_key, list(v.values())[0][0]))
258-
elif list(v.values())[0][0]["type"] == "array":
259-
list(v.values())[0][0]["type"] = ["null", "array"]
260-
items.append((new_key, list(v.values())[0][0]))
261-
elif list(v.values())[0][0]["type"] == "object":
262-
list(v.values())[0][0]["type"] = ["null", "object"]
263-
items.append((new_key, list(v.values())[0][0]))
255+
if next(iter(v.values()))[0]["type"] == "string":
256+
next(iter(v.values()))[0]["type"] = ["null", "string"]
257+
items.append((new_key, next(iter(v.values()))[0]))
258+
elif next(iter(v.values()))[0]["type"] == "array":
259+
next(iter(v.values()))[0]["type"] = ["null", "array"]
260+
items.append((new_key, next(iter(v.values()))[0]))
261+
elif next(iter(v.values()))[0]["type"] == "object":
262+
next(iter(v.values()))[0]["type"] = ["null", "object"]
263+
items.append((new_key, next(iter(v.values()))[0]))
264264

265265
# Sort and check for duplicates
266266
def _key_func(item):

singer_sdk/sinks/core.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,9 @@ def datetime_error_treatment(self) -> DatetimeErrorTreatmentEnum:
215215
def key_properties(self) -> list[str]:
216216
"""Return key properties.
217217
218+
Override this method to return a list of key properties in a format that is
219+
compatible with the target.
220+
218221
Returns:
219222
A list of stream key properties.
220223
"""
@@ -331,10 +334,10 @@ def _singer_validate_message(self, record: dict) -> None:
331334
Raises:
332335
MissingKeyPropertiesError: If record is missing one or more key properties.
333336
"""
334-
if not all(key_property in record for key_property in self.key_properties):
337+
if any(key_property not in record for key_property in self._key_properties):
335338
msg = (
336339
f"Record is missing one or more key_properties. \n"
337-
f"Key Properties: {self.key_properties}, "
340+
f"Key Properties: {self._key_properties}, "
338341
f"Record Keys: {list(record.keys())}"
339342
)
340343
raise MissingKeyPropertiesError(

singer_sdk/sinks/sql.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -322,15 +322,20 @@ def bulk_insert_records(
322322
if isinstance(insert_sql, str):
323323
insert_sql = sqlalchemy.text(insert_sql)
324324

325-
conformed_records = (
326-
[self.conform_record(record) for record in records]
327-
if isinstance(records, list)
328-
else (self.conform_record(record) for record in records)
329-
)
325+
conformed_records = [self.conform_record(record) for record in records]
326+
property_names = list(self.conform_schema(schema)["properties"].keys())
327+
328+
# Create new record dicts with missing properties filled in with None
329+
new_records = [
330+
{name: record.get(name) for name in property_names}
331+
for record in conformed_records
332+
]
333+
330334
self.logger.info("Inserting with SQL: %s", insert_sql)
331335
with self.connector.connect() as conn, conn.begin():
332-
conn.execute(insert_sql, conformed_records)
333-
return len(conformed_records) if isinstance(conformed_records, list) else None
336+
result = conn.execute(insert_sql, new_records)
337+
338+
return result.rowcount
334339

335340
def merge_upsert_from_table(
336341
self,

singer_sdk/streams/core.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,10 @@ def is_timestamp_replication_key(self) -> bool:
217217
type_dict = self.schema.get("properties", {}).get(self.replication_key)
218218
return is_datetime_type(type_dict)
219219

220-
def get_starting_replication_key_value(self, context: dict | None) -> t.Any | None:
220+
def get_starting_replication_key_value(
221+
self,
222+
context: dict | None,
223+
) -> t.Any | None: # noqa: ANN401
221224
"""Get starting replication key.
222225
223226
Will return the value of the stream's replication key when `--state` is passed.
@@ -385,7 +388,7 @@ def _write_starting_replication_value(self, context: dict | None) -> None:
385388
def get_replication_key_signpost(
386389
self,
387390
context: dict | None, # noqa: ARG002
388-
) -> datetime.datetime | t.Any | None:
391+
) -> datetime.datetime | t.Any | None: # noqa: ANN401
389392
"""Get the replication signpost.
390393
391394
For timestamp-based replication keys, this defaults to `utc_now()`. For
@@ -1255,7 +1258,7 @@ def get_child_context(self, record: dict, context: dict | None) -> dict | None:
12551258
12561259
Raises:
12571260
NotImplementedError: If the stream has children but this method is not
1258-
overriden.
1261+
overridden.
12591262
"""
12601263
if context is None:
12611264
for child_stream in self.child_streams:

singer_sdk/streams/graphql.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
from singer_sdk.helpers._classproperty import classproperty
99
from singer_sdk.streams.rest import RESTStream
1010

11+
_TToken = t.TypeVar("_TToken")
1112

12-
class GraphQLStream(RESTStream, metaclass=abc.ABCMeta):
13+
14+
class GraphQLStream(RESTStream, t.Generic[_TToken], metaclass=abc.ABCMeta):
1315
"""Abstract base class for API-type streams.
1416
1517
GraphQL streams inherit from the class `GraphQLStream`, which in turn inherits from
@@ -43,7 +45,7 @@ def query(self) -> str:
4345
def prepare_request_payload(
4446
self,
4547
context: dict | None,
46-
next_page_token: t.Any | None,
48+
next_page_token: _TToken | None,
4749
) -> dict | None:
4850
"""Prepare the data payload for the GraphQL API request.
4951

singer_sdk/tap_base.py

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -612,37 +612,17 @@ class SQLTap(Tap):
612612
# Stream class used to initialize new SQL streams from their catalog declarations.
613613
default_stream_class: type[SQLStream]
614614

615-
def __init__(
616-
self,
617-
*,
618-
config: dict | PurePath | str | list[PurePath | str] | None = None,
619-
catalog: PurePath | str | dict | None = None,
620-
state: PurePath | str | dict | None = None,
621-
parse_env_config: bool = False,
622-
validate_config: bool = True,
623-
) -> None:
615+
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
624616
"""Initialize the SQL tap.
625617
626618
The SQLTap initializer additionally creates a cache variable for _catalog_dict.
627619
628620
Args:
629-
config: Tap configuration. Can be a dictionary, a single path to a
630-
configuration file, or a list of paths to multiple configuration
631-
files.
632-
catalog: Tap catalog. Can be a dictionary or a path to the catalog file.
633-
state: Tap state. Can be dictionary or a path to the state file.
634-
parse_env_config: Whether to look for configuration values in environment
635-
variables.
636-
validate_config: True to require validation of config settings.
621+
*args: Positional arguments for the Tap initializer.
622+
**kwargs: Keyword arguments for the Tap initializer.
637623
"""
638624
self._catalog_dict: dict | None = None
639-
super().__init__(
640-
config=config,
641-
catalog=catalog,
642-
state=state,
643-
parse_env_config=parse_env_config,
644-
validate_config=validate_config,
645-
)
625+
super().__init__(*args, **kwargs)
646626

647627
@property
648628
def catalog_dict(self) -> dict:

singer_sdk/testing/__init__.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,57 @@
22

33
from __future__ import annotations
44

5+
import typing as t
6+
import warnings
7+
58
from .config import SuiteConfig
69
from .factory import get_tap_test_class, get_target_test_class
710
from .legacy import (
811
_get_tap_catalog,
912
_select_all,
10-
get_standard_tap_tests,
11-
get_standard_target_tests,
1213
sync_end_to_end,
1314
tap_sync_test,
1415
tap_to_target_sync_test,
1516
target_sync_test,
1617
)
1718
from .runners import SingerTestRunner, TapTestRunner, TargetTestRunner
1819

20+
21+
def __getattr__(name: str) -> t.Any: # noqa: ANN401
22+
if name == "get_standard_tap_tests":
23+
warnings.warn(
24+
"The function singer_sdk.testing.get_standard_tap_tests is deprecated "
25+
"and will be removed in a future release. Use get_tap_test_class instead.",
26+
DeprecationWarning,
27+
stacklevel=2,
28+
)
29+
30+
from .legacy import get_standard_tap_tests
31+
32+
return get_standard_tap_tests
33+
34+
if name == "get_standard_target_tests":
35+
warnings.warn(
36+
"The function singer_sdk.testing.get_standard_target_tests is deprecated "
37+
"and will be removed in a future release. Use get_target_test_class "
38+
"instead.",
39+
DeprecationWarning,
40+
stacklevel=2,
41+
)
42+
43+
from .legacy import get_standard_target_tests
44+
45+
return get_standard_target_tests
46+
47+
msg = f"module {__name__} has no attribute {name}"
48+
raise AttributeError(msg)
49+
50+
1951
__all__ = [
2052
"get_tap_test_class",
2153
"get_target_test_class",
2254
"_get_tap_catalog",
2355
"_select_all",
24-
"get_standard_tap_tests",
25-
"get_standard_target_tests",
2656
"sync_end_to_end",
2757
"tap_sync_test",
2858
"tap_to_target_sync_test",

0 commit comments

Comments
 (0)