Repository: meltano/sdk
Pull request #1832 (2023-07-11 06:29:14)

chore: pre-commit autoupdate
<!--pre-commit.ci start-->
updates:
- https://github.com/charliermarsh/ruff-pre-commit → https://github.com/astral-sh/ruff-pre-commit
- [github.com/astral-sh/ruff-pre-commit: v0.0.272 → v0.0.277](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.272...v0.0.277)
- [github.com/psf/black: 23.3.0 → 23.7.0](https://github.com/psf/black/compare/23.3.0...23.7.0)
<!--pre-commit.ci end-->

<!-- readthedocs-preview meltano-sdk start -->
----
:books: Documentation preview :books:: https://meltano-sdk--1832.org.readthedocs.build/en/1832/

<!-- readthedocs-preview meltano-sdk end -->


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 6ba7d32d..d0024c64 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -42,8 +42,8 @@ repos:
   - id: check-github-workflows
   - id: check-readthedocs
 
-- repo: https://github.com/charliermarsh/ruff-pre-commit
-  rev: v0.0.272
+- repo: https://github.com/astral-sh/ruff-pre-commit
+  rev: v0.0.277
   hooks:
   - id: ruff
     args: [--fix, --exit-non-zero-on-fix]
@@ -53,7 +53,7 @@ repos:
       )$
 
 - repo: https://github.com/psf/black
-  rev: 23.3.0
+  rev: 23.7.0
   hooks:
   - id: black
     exclude: |
diff --git a/samples/sample_tap_countries/countries_streams.py b/samples/sample_tap_countries/countries_streams.py
index 997eea78..708e1678 100644
--- a/samples/sample_tap_countries/countries_streams.py
+++ b/samples/sample_tap_countries/countries_streams.py
@@ -30,7 +30,7 @@ class CountriesStream(CountriesAPIStream):
     """Countries API stream."""
 
     name = "countries"
-    primary_keys = ["code"]
+    primary_keys = ("code",)
     query = """
         countries {
             code
@@ -81,7 +81,7 @@ class ContinentsStream(CountriesAPIStream):
     """Continents stream from the Countries API."""
 
     name = "continents"
-    primary_keys = ["code"]
+    primary_keys = ("code",)
     schema_filepath = SCHEMAS_DIR / "continents.json"
     query = """
         continents {
diff --git a/samples/sample_tap_gitlab/gitlab_graphql_streams.py b/samples/sample_tap_gitlab/gitlab_graphql_streams.py
index ab69cc6e..b29fbc13 100644
--- a/samples/sample_tap_gitlab/gitlab_graphql_streams.py
+++ b/samples/sample_tap_gitlab/gitlab_graphql_streams.py
@@ -34,7 +34,7 @@ class GraphQLCurrentUserStream(GitlabGraphQLStream):
     """Gitlab Current User stream."""
 
     name = "currentuser"
-    primary_keys = ["id"]
+    primary_keys = ("id",)
     replication_key = None
     schema_filepath = SCHEMAS_DIR / "currentuser.json"
     query = """
@@ -48,7 +48,7 @@ class GraphQLProjectsStream(GitlabGraphQLStream):
     """Gitlab Projects stream."""
 
     name = "projects"
-    primary_keys = ["id"]
+    primary_keys = ("id",)
     replication_key = None
     schema_filepath = SCHEMAS_DIR / "projects-graphql.json"
 
diff --git a/samples/sample_tap_gitlab/gitlab_rest_streams.py b/samples/sample_tap_gitlab/gitlab_rest_streams.py
index b34dd488..1480a017 100644
--- a/samples/sample_tap_gitlab/gitlab_rest_streams.py
+++ b/samples/sample_tap_gitlab/gitlab_rest_streams.py
@@ -97,7 +97,7 @@ class ProjectsStream(ProjectBasedStream):
 
     name = "projects"
     path = "/projects/{project_id}?statistics=1"
-    primary_keys = ["id"]
+    primary_keys = ("id",)
     replication_key = "last_activity_at"
     is_sorted = True
     schema_filepath = SCHEMAS_DIR / "projects.json"
@@ -108,7 +108,7 @@ class ReleasesStream(ProjectBasedStream):
 
     name = "releases"
     path = "/projects/{project_id}/releases"
-    primary_keys = ["project_id", "tag_name"]
+    primary_keys = ("project_id", "tag_name")
     replication_key = None
     schema_filepath = SCHEMAS_DIR / "releases.json"
 
@@ -118,7 +118,7 @@ class IssuesStream(ProjectBasedStream):
 
     name = "issues"
     path = "/projects/{project_id}/issues?scope=all&updated_after={start_date}"
-    primary_keys = ["id"]
+    primary_keys = ("id",)
     replication_key = "updated_at"
     is_sorted = False
     schema_filepath = SCHEMAS_DIR / "issues.json"
@@ -131,7 +131,7 @@ class CommitsStream(ProjectBasedStream):
     path = (
         "/projects/{project_id}/repository/commits?since={start_date}&with_stats=true"
     )
-    primary_keys = ["id"]
+    primary_keys = ("id",)
     replication_key = "created_at"
     is_sorted = False
     schema_filepath = SCHEMAS_DIR / "commits.json"
@@ -146,7 +146,7 @@ class EpicsStream(ProjectBasedStream):
 
     name = "epics"
     path = "/groups/{group_id}/epics?updated_after={start_date}"
-    primary_keys = ["id"]
+    primary_keys = ("id",)
     replication_key = "updated_at"
     is_sorted = True
     schema = PropertiesList(
@@ -187,7 +187,7 @@ class EpicIssuesStream(GitlabStream):
 
     name = "epic_issues"
     path = "/groups/{group_id}/epics/{epic_iid}/issues"
-    primary_keys = ["id"]
+    primary_keys = ("id",)
     replication_key = None
     schema_filepath = SCHEMAS_DIR / "epic_issues.json"
     parent_stream_type = EpicsStream  # Stream should wait for parents to complete.
diff --git a/samples/sample_tap_google_analytics/ga_tap_stream.py b/samples/sample_tap_google_analytics/ga_tap_stream.py
index 05199f76..8d1f0959 100644
--- a/samples/sample_tap_google_analytics/ga_tap_stream.py
+++ b/samples/sample_tap_google_analytics/ga_tap_stream.py
@@ -32,8 +32,8 @@ class SampleGoogleAnalyticsStream(RESTStream):
     rest_method = "POST"
 
     # Child class overrides:
-    dimensions: list[str] = []
-    metrics: list[str] = []
+    dimensions: tuple[str] = ()
+    metrics: tuple[str] = ()
 
     @property
     def authenticator(self) -> GoogleJWTAuthenticator:
@@ -86,5 +86,5 @@ class GASimpleSampleStream(SampleGoogleAnalyticsStream):
     name = "simple_sample"
     schema_filepath = SCHEMAS_DIR / "simple-sample.json"
 
-    dimensions = ["ga:date"]
-    metrics = ["ga:users", "ga:sessions"]
+    dimensions = ("ga:date",)
+    metrics = ("ga:users", "ga:sessions")
diff --git a/singer_sdk/helpers/_typing.py b/singer_sdk/helpers/_typing.py
index 42abb08e..d3df38a5 100644
--- a/singer_sdk/helpers/_typing.py
+++ b/singer_sdk/helpers/_typing.py
@@ -108,10 +108,13 @@ def is_object_type(property_schema: dict) -> bool | None:
     """Return true if the JSON Schema type is an object or None if detection fails."""
     if "anyOf" not in property_schema and "type" not in property_schema:
         return None  # Could not detect data type
-    for property_type in property_schema.get("anyOf", [property_schema.get("type")]):
-        if "object" in property_type or property_type == "object":
-            return True
-    return False
+    return any(
+        "object" in property_type or property_type == "object"
+        for property_type in property_schema.get(
+            "anyOf",
+            [property_schema.get("type")],
+        )
+    )
 
 
 def is_uniform_list(property_schema: dict) -> bool | None:
@@ -410,9 +413,7 @@ def _conform_record_data_types(  # noqa: PLR0912
         return input_object, unmapped_properties
 
     for property_name, elem in input_object.items():
-        property_path = (
-            property_name if parent is None else parent + "." + property_name
-        )
+        property_path = property_name if parent is None else f"{parent}.{property_name}"
         if property_name not in schema["properties"]:
             unmapped_properties.append(property_path)
             continue
@@ -466,7 +467,7 @@ def _conform_record_data_types(  # noqa: PLR0912
     return output_object, unmapped_properties
 
 
-def _conform_primitive_property(  # noqa: PLR0911, C901
+def _conform_primitive_property(  # noqa: PLR0911
     elem: t.Any,
     property_schema: dict,
 ) -> t.Any:
@@ -474,7 +475,7 @@ def _conform_primitive_property(  # noqa: PLR0911, C901
     if isinstance(elem, (datetime.datetime, pendulum.DateTime)):
         return to_json_compatible(elem)
     if isinstance(elem, datetime.date):
-        return elem.isoformat() + "T00:00:00+00:00"
+        return f"{elem.isoformat()}T00:00:00+00:00"
     if isinstance(elem, datetime.timedelta):
         epoch = datetime.datetime.fromtimestamp(0, UTC)
         timedelta_from_epoch = epoch + elem
@@ -485,19 +486,7 @@ def _conform_primitive_property(  # noqa: PLR0911, C901
         return str(elem)
     if isinstance(elem, bytes):
         # for BIT value, treat 0 as False and anything else as True
-        bit_representation: bool
-        if is_boolean_type(property_schema):
-            bit_representation = elem != b"\x00"
-            return bit_representation
-        return elem.hex()
+        return elem != b"\x00" if is_boolean_type(property_schema) else elem.hex()
     if is_boolean_type(property_schema):
-        boolean_representation: bool | None
-        if elem is None:
-            boolean_representation = None
-        elif elem == 0:
-            boolean_representation = False
-        else:
-            boolean_representation = True
-        return boolean_representation
-
+        return None if elem is None else elem != 0
     return elem
diff --git a/singer_sdk/plugin_base.py b/singer_sdk/plugin_base.py
index 7fffc41e..1edc34c7 100644
--- a/singer_sdk/plugin_base.py
+++ b/singer_sdk/plugin_base.py
@@ -74,7 +74,7 @@ class PluginBase(metaclass=abc.ABCMeta):
     #: The package name of the plugin. e.g meltanolabs-tap-foo
     package_name: str | None = None
 
-    config_jsonschema: dict = {}
+    config_jsonschema: t.ClassVar[dict] = {}
     # A JSON Schema object defining the config options that this tap will accept.
 
     _config: dict
diff --git a/singer_sdk/streams/core.py b/singer_sdk/streams/core.py
index a414123d..96abda44 100644
--- a/singer_sdk/streams/core.py
+++ b/singer_sdk/streams/core.py
@@ -97,9 +97,6 @@ class Stream(metaclass=abc.ABCMeta):
 
     ignore_parent_replication_key: bool = False
 
-    # Internal API cost aggregator
-    _sync_costs: dict[str, int] = {}
-
     selected_by_default: bool = True
     """Whether this stream is selected by default in the catalog."""
 
@@ -143,6 +140,7 @@ class Stream(metaclass=abc.ABCMeta):
         self._mask: singer.SelectionMask | None = None
         self._schema: dict
         self._is_state_flushed: bool = True
+        self._sync_costs: dict[str, int] = {}
         self.child_streams: list[Stream] = []
         if schema:
             if isinstance(schema, (PathLike, str)):
@@ -296,11 +294,10 @@ class Stream(metaclass=abc.ABCMeta):
         Returns:
             True if any child streams are selected, recursively.
         """
-        for child in self.child_streams or []:
-            if child.selected or child.has_selected_descendents:
-                return True
-
-        return False
+        return any(
+            child.selected or child.has_selected_descendents
+            for child in self.child_streams or []
+        )
 
     @final
     @property
@@ -405,10 +402,7 @@ class Stream(metaclass=abc.ABCMeta):
         Returns:
             Max allowable bookmark value for this stream's replication key.
         """
-        if self.is_timestamp_replication_key:
-            return utc_now()
-
-        return None
+        return utc_now() if self.is_timestamp_replication_key else None
 
     @property
     def schema_filepath(self) -> Path | None:
@@ -435,9 +429,7 @@ class Stream(metaclass=abc.ABCMeta):
         Returns:
             A list of primary key(s) for the stream.
         """
-        if not self._primary_keys:
-            return []
-        return self._primary_keys
+        return self._primary_keys or []
 
     @primary_keys.setter
     def primary_keys(self, new_value: list[str] | None) -> None:
@@ -479,9 +471,7 @@ class Stream(metaclass=abc.ABCMeta):
         Returns:
             Replication key for the stream.
         """
-        if not self._replication_key:
-            return None
-        return self._replication_key
+        return self._replication_key or None
 
     @replication_key.setter
     def replication_key(self, new_value: str | None) -> None:
@@ -706,11 +696,12 @@ class Stream(metaclass=abc.ABCMeta):
         Returns:
             A list of partition key dicts (if applicable), otherwise `None`.
         """
-        result: list[dict] = []
-        for partition_state in (
-            get_state_partitions_list(self.tap_state, self.name) or []
-        ):
-            result.append(partition_state["context"])
+        result: list[dict] = [
+            partition_state["context"]
+            for partition_state in (
+                get_state_partitions_list(self.tap_state, self.name) or []
+            )
+        ]
         return result or None
 
     # Private bookmarking methods
@@ -781,13 +772,12 @@ class Stream(metaclass=abc.ABCMeta):
                 # Don't emit schema if the stream's records are all ignored.
                 continue
 
-            schema_message = singer.SchemaMessage(
+            yield singer.SchemaMessage(
                 stream_map.stream_alias,
                 stream_map.transformed_schema,
                 stream_map.transformed_key_properties,
                 bookmark_keys,
             )
-            yield schema_message
 
     def _write_schema_message(self) -> None:
         """Write out a SCHEMA message with the stream schema."""
@@ -830,15 +820,13 @@ class Stream(metaclass=abc.ABCMeta):
             mapped_record = stream_map.transform(record)
             # Emit record if not filtered
             if mapped_record is not None:
-                record_message = singer.RecordMessage(
+                yield singer.RecordMessage(
                     stream=stream_map.stream_alias,
                     record=mapped_record,
                     version=None,
                     time_extracted=utc_now(),
                 )
 
-                yield record_message
-
     def _write_record_message(self, record: dict) -> None:
         """Write out a RECORD message.
 
diff --git a/singer_sdk/streams/rest.py b/singer_sdk/streams/rest.py
index a1c39576..563956a5 100644
--- a/singer_sdk/streams/rest.py
+++ b/singer_sdk/streams/rest.py
@@ -59,7 +59,7 @@ class RESTStream(Stream, t.Generic[_TToken], metaclass=abc.ABCMeta):
     records_jsonpath: str = "$[*]"
 
     #: Response code reference for rate limit retries
-    extra_retry_statuses: list[int] = [HTTPStatus.TOO_MANY_REQUESTS]
+    extra_retry_statuses: t.Sequence[int] = [HTTPStatus.TOO_MANY_REQUESTS]
 
     #: Optional JSONPath expression to extract a pagination token from the API response.
     #: Example: `"$.next_page"`


Test patch:
diff --git a/singer_sdk/testing/factory.py b/singer_sdk/testing/factory.py
index 99c9058c..c7611955 100644
--- a/singer_sdk/testing/factory.py
+++ b/singer_sdk/testing/factory.py
@@ -1,5 +1,4 @@
 """Test Class Factory."""
-
 from __future__ import annotations
 
 import typing as t
@@ -22,8 +21,8 @@ if t.TYPE_CHECKING:
 class BaseTestClass:
     """Base test class."""
 
-    params: dict = {}
-    param_ids: dict = {}
+    params: t.ClassVar[dict] = {}
+    param_ids: t.ClassVar[dict] = {}
 
 
 class TapTestClassFactory:
diff --git a/singer_sdk/testing/runners.py b/singer_sdk/testing/runners.py
index 16f2ba90..96416de9 100644
--- a/singer_sdk/testing/runners.py
+++ b/singer_sdk/testing/runners.py
@@ -17,12 +17,6 @@ from singer_sdk.testing.config import SuiteConfig
 class SingerTestRunner(metaclass=abc.ABCMeta):
     """Base Singer Test Runner."""
 
-    raw_messages: list[dict] = []
-    schema_messages: list[dict] = []
-    record_messages: list[dict] = []
-    state_messages: list[dict] = []
-    records: defaultdict = defaultdict(list)
-
     def __init__(
         self,
         singer_class: type[Tap] | type[Target],
@@ -44,6 +38,12 @@ class SingerTestRunner(metaclass=abc.ABCMeta):
         self.default_kwargs = kwargs
         self.suite_config = suite_config or SuiteConfig()
 
+        self.raw_messages: list[dict] = []
+        self.schema_messages: list[dict] = []
+        self.record_messages: list[dict] = []
+        self.state_messages: list[dict] = []
+        self.records: defaultdict = defaultdict(list)
+
     @staticmethod
     def _clean_sync_output(raw_records: str) -> list[dict]:
         """Clean sync output.
diff --git a/singer_sdk/testing/templates.py b/singer_sdk/testing/templates.py
index b0045252..b43d3783 100644
--- a/singer_sdk/testing/templates.py
+++ b/singer_sdk/testing/templates.py
@@ -156,7 +156,7 @@ class StreamTestTemplate(TestTemplate):
     """Base Tap Stream test template."""
 
     plugin_type = "stream"
-    required_kwargs = ["stream"]
+    required_kwargs: t.ClassVar[list[str]] = ["stream"]
 
     @property
     def id(self) -> str:  # noqa: A003
diff --git a/tests/core/rest/conftest.py b/tests/core/rest/conftest.py
index fade6e94..13d7eb9e 100644
--- a/tests/core/rest/conftest.py
+++ b/tests/core/rest/conftest.py
@@ -2,6 +2,8 @@
 
 from __future__ import annotations
 
+import typing as t
+
 import pytest
 from memoization.memoization import cached
 from requests.auth import HTTPProxyAuth
@@ -19,7 +21,7 @@ class SimpleRESTStream(RESTStream):
     """A REST stream for testing."""
 
     url_base = "https://example.com"
-    schema = {
+    schema: t.ClassVar[dict] = {
         "type": "object",
         "properties": {},
     }
diff --git a/tests/core/rest/test_authenticators.py b/tests/core/rest/test_authenticators.py
index fa42caef..0226c4ae 100644
--- a/tests/core/rest/test_authenticators.py
+++ b/tests/core/rest/test_authenticators.py
@@ -182,7 +182,7 @@ def test_oauth_jwt_authenticator_payload(
 ):
     class _FakeOAuthJWTAuthenticator(OAuthJWTAuthenticator):
         private_key = private_key_string
-        oauth_request_body = {"some": "payload"}
+        oauth_request_body = {"some": "payload"}  # noqa: RUF012
 
     authenticator = _FakeOAuthJWTAuthenticator(stream=rest_tap.streams["some_stream"])
 
diff --git a/tests/core/rest/test_backoff.py b/tests/core/rest/test_backoff.py
index 217fa32c..fbe391e5 100644
--- a/tests/core/rest/test_backoff.py
+++ b/tests/core/rest/test_backoff.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import json
+import typing as t
 
 try:
     from contextlib import nullcontext
@@ -21,7 +22,7 @@ class CustomResponseValidationStream(RESTStream):
 
     url_base = "https://badapi.test"
     name = "imabadapi"
-    schema = {"type": "object", "properties": {}}
+    schema: t.ClassVar[dict] = {"type": "object", "properties": {}}
     path = "/dummy"
 
     class StatusMessage(str, Enum):
diff --git a/tests/core/test_parent_child.py b/tests/core/test_parent_child.py
index a91726bc..7fd01a15 100644
--- a/tests/core/test_parent_child.py
+++ b/tests/core/test_parent_child.py
@@ -2,6 +2,7 @@ from __future__ import annotations
 
 import io
 import json
+import typing as t
 from contextlib import redirect_stdout
 
 import pytest
@@ -14,7 +15,7 @@ class Parent(Stream):
     """A parent stream."""
 
     name = "parent"
-    schema = {
+    schema: t.ClassVar[dict] = {
         "type": "object",
         "properties": {
             "id": {"type": "integer"},
@@ -42,7 +43,7 @@ class Child(Stream):
     """A child stream."""
 
     name = "child"
-    schema = {
+    schema: t.ClassVar[dict] = {
         "type": "object",
         "properties": {
             "id": {"type": "integer"},
diff --git a/tests/core/test_streams.py b/tests/core/test_streams.py
index 71789c1f..34bbc751 100644
--- a/tests/core/test_streams.py
+++ b/tests/core/test_streams.py
@@ -580,7 +580,7 @@ def test_stream_class_selection(input_catalog, selection):
     class SelectedStream(RESTStream):
         name = "selected_stream"
         url_base = "https://example.com"
-        schema = {"type": "object", "properties": {}}
+        schema = {"type": "object", "properties": {}}  # noqa: RUF012
 
     class UnselectedStream(SelectedStream):
         name = "unselected_stream"
diff --git a/tests/samples/test_tap_countries.py b/tests/samples/test_tap_countries.py
index e1910d0c..31e0b866 100644
--- a/tests/samples/test_tap_countries.py
+++ b/tests/samples/test_tap_countries.py
@@ -42,12 +42,12 @@ def test_countries_primary_key():
     metadata_root = countries_entry.metadata.root
     key_props_1 = metadata_root.table_key_properties
     key_props_2 = countries_entry.key_properties
-    assert key_props_1 == ["code"], (
+    assert key_props_1 == ("code",), (
         f"Incorrect 'table-key-properties' in catalog: ({key_props_1})\n\n"
         f"Root metadata was: {metadata_root}\n\n"
         f"Catalog entry was: {countries_entry}"
     )
-    assert key_props_2 == ["code"], (
+    assert key_props_2 == ("code",), (
         f"Incorrect 'key_properties' in catalog: ({key_props_2})\n\n"
         "Catalog entry was: {countries_entry}"
     )