Skip to content

Commit 7813ca4

Browse files
authored
chore: add missing nox sessions and polish dependencies in sqlalchemy-spanner (googleapis#17285)
## Description This PR implements missing nox sessions and refactors dependency management inside `packages/sqlalchemy-spanner/noxfile.py` to align with google-cloud standard practices. --- ## Key Changes ### 1. Constants Architecture All dependencies and version definitions have been concentrated at the top of `noxfile.py` in nestled constants: * `UNIT_TEST_STANDARD_DEPENDENCIES`: Standard testing frameworks (`mock`, `pytest`). * `UNIT_TEST_EXTERNAL_DEPENDENCIES`: Telemetry requirements (fully unpinned: `setuptools`, `opentelemetry-api`, `opentelemetry-sdk`, `opentelemetry-instrumentation` to delegate version resolution strictly to `setup.py` and `constraints`). * `UNIT_TEST_DEPENDENCIES`: SQLAlchemy version specifications under test (`sqlalchemy>=2.0`). ### 2. Expired database management Updated the `create_test_database.py` file to add or tweak code to ensure that expired databases are quickly deleted. During rapid development cycles, the Spanner database count would quickly reach the Spanner limit of 100 and system tests would fail. There were two conditions that contributed being unable to quickly retest. In some cases the system would not delete old databases at all because certain triggers were not created. And in other cases, you needed to wait 4 hours before the databases would age off and you could run the system tests again. ### NOTES: * **`core_deps_from_source` & `prerelease_deps`**: Target packages are cleanly overwritten with local source paths/pre-releases using pip's native `--ignore-installed --no-deps` flags. * **`mypy` & `docs` & `docfx`**: Configured to skip gracefully with standard todos indicating that typehints and docs folders are not present in this package. * **`cover`**: Added standard coverage session which skips gracefully if no `.coverage` database file has been created yet. * **`format`**: Implemented Ruff format and check sessions, and reformatted imports and code to team standards. Fixes googleapis#17048 🦖
1 parent 522d192 commit 7813ca4

80 files changed

Lines changed: 943 additions & 676 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/sqlalchemy-spanner/create_test_database.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# limitations under the License.
1616

1717
import os
18+
import re
1819
import time
1920

2021
from google.api_core import datetime_helpers
@@ -68,19 +69,30 @@ def delete_stale_test_instances():
6869

6970

7071
def delete_stale_test_databases():
71-
"""Delete test databases that are older than four hours."""
72-
cutoff = (int(time.time()) - 4 * 60 * 60) * 1000
72+
"""Delete test databases that are older than 10 minutes.
73+
74+
In this test suite, active databases typically finish running in ~5 minutes.
75+
To prevent concurrent Kokoro runs from accidentally deleting each other's
76+
active databases we use a 10-minute safety threshold. Without an aggressive
77+
cutoff we quickly bump up against Cloud Spanner's limit of 100 databases per instance.
78+
"""
79+
cutoff = (int(time.time()) - 10 * 60) * 1000
7380
instance = CLIENT.instance("sqlalchemy-dialect-test")
7481
if not instance.exists():
7582
return
7683
database_pbs = instance.list_databases()
7784
for database_pb in database_pbs:
7885
database = Database.from_pb(database_pb, instance)
79-
# The emulator does not return a create_time for databases.
80-
if database.create_time is None:
81-
continue
82-
create_time = datetime_helpers.to_milliseconds(database_pb.create_time)
83-
if create_time > cutoff:
86+
# Parse creation time from database ID first (e.g. "sqlalchemy-test-1779989493809")
87+
# to be 100% independent of emulator metadata or GCP Client API create_time gaps!
88+
create_time = None
89+
match = re.match(r"sqlalchemy-test-(\d+)", database.database_id)
90+
if match:
91+
create_time = int(match.group(1))
92+
elif database_pb.create_time is not None:
93+
create_time = datetime_helpers.to_milliseconds(database_pb.create_time)
94+
95+
if create_time is None or create_time > cutoff:
8496
continue
8597
try:
8698
database.drop()

packages/sqlalchemy-spanner/google/cloud/sqlalchemy_spanner/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
# limitations under the License.
1414

1515
from .sqlalchemy_spanner import SpannerDialect
16-
1716
from .version import __version__
1817

19-
2018
__all__ = (SpannerDialect, __version__)

packages/sqlalchemy-spanner/google/cloud/sqlalchemy_spanner/_opentelemetry_tracing.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
import collections
1818
import os
19-
2019
from contextlib import contextmanager
2120

2221
from google.api_core.exceptions import GoogleAPICallError

packages/sqlalchemy-spanner/google/cloud/sqlalchemy_spanner/requirements.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
# limitations under the License.
1414

1515
from sqlalchemy.testing import exclusions
16-
from sqlalchemy.testing.requirements import SuiteRequirements
1716
from sqlalchemy.testing.exclusions import against, only_on
17+
from sqlalchemy.testing.requirements import SuiteRequirements
1818

1919

2020
class Requirements(SuiteRequirements): # pragma: no cover

packages/sqlalchemy-spanner/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py

Lines changed: 20 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
import base64
15-
1615
import re
1716

17+
import sqlalchemy
1818
from alembic.ddl.base import (
1919
ColumnNullable,
2020
ColumnType,
@@ -26,32 +26,30 @@
2626
from google.api_core.client_options import ClientOptions
2727
from google.auth.credentials import AnonymousCredentials
2828
from google.cloud.spanner_v1 import Client, TransactionOptions
29-
from sqlalchemy.exc import NoSuchTableError
30-
from sqlalchemy.sql import elements
31-
from sqlalchemy import ForeignKeyConstraint, types, TypeDecorator, PickleType
29+
from google.cloud.spanner_v1.data_types import JsonObject
30+
from sqlalchemy import ForeignKeyConstraint, PickleType, TypeDecorator, types
3231
from sqlalchemy.engine.base import Engine
3332
from sqlalchemy.engine.default import DefaultDialect, DefaultExecutionContext
3433
from sqlalchemy.event import listens_for
34+
from sqlalchemy.exc import NoSuchTableError
3535
from sqlalchemy.ext.compiler import compiles
3636
from sqlalchemy.pool import Pool
37+
from sqlalchemy.sql import elements, expression
3738
from sqlalchemy.sql.compiler import (
38-
selectable,
39+
OPERATORS,
40+
RESERVED_WORDS,
3941
DDLCompiler,
4042
GenericTypeCompiler,
4143
IdentifierPreparer,
4244
SQLCompiler,
43-
OPERATORS,
44-
RESERVED_WORDS,
45+
selectable,
4546
)
4647
from sqlalchemy.sql.default_comparator import operator_lookup
4748
from sqlalchemy.sql.operators import json_getitem_op
48-
from sqlalchemy.sql import expression
4949

50-
from google.cloud.spanner_v1.data_types import JsonObject
5150
from google.cloud import spanner_dbapi
52-
from google.cloud.sqlalchemy_spanner._opentelemetry_tracing import trace_call
5351
from google.cloud.sqlalchemy_spanner import version as sqlalchemy_spanner_version
54-
import sqlalchemy
52+
from google.cloud.sqlalchemy_spanner._opentelemetry_tracing import trace_call
5553

5654
USING_SQLACLCHEMY_20 = False
5755
if sqlalchemy.__version__.split(".")[0] == "2":
@@ -223,9 +221,9 @@ def pre_exec(self):
223221
if ignore_transaction_warnings is not None:
224222
conn = self._dbapi_connection.connection
225223
if conn is not None and hasattr(conn, "_connection_variables"):
226-
conn._connection_variables[
227-
"ignore_transaction_warnings"
228-
] = ignore_transaction_warnings
224+
conn._connection_variables["ignore_transaction_warnings"] = (
225+
ignore_transaction_warnings
226+
)
229227

230228
def fire_sequence(self, seq, type_):
231229
"""Builds a statement for fetching next value of the sequence."""
@@ -416,7 +414,7 @@ def limit_clause(self, select, **kw):
416414
text += "\n LIMIT " + self.process(select._limit_clause, **kw)
417415
if select._offset_clause is not None:
418416
if select._limit_clause is None:
419-
text += f"\n LIMIT {9223372036854775807-select._offset}"
417+
text += f"\n LIMIT {9223372036854775807 - select._offset}"
420418
text += " OFFSET " + self.process(select._offset_clause, **kw)
421419
return text
422420

@@ -1025,9 +1023,7 @@ def get_view_names(self, connection, schema=None, **kw):
10251023
SELECT table_name
10261024
FROM information_schema.views
10271025
WHERE TABLE_SCHEMA='{}'
1028-
""".format(
1029-
schema or ""
1030-
)
1026+
""".format(schema or "")
10311027

10321028
all_views = []
10331029
with connection.connection.database.snapshot() as snap:
@@ -1056,9 +1052,7 @@ def get_sequence_names(self, connection, schema=None, **kw):
10561052
SELECT name
10571053
FROM information_schema.sequences
10581054
WHERE SCHEMA='{}'
1059-
""".format(
1060-
schema or ""
1061-
)
1055+
""".format(schema or "")
10621056
all_sequences = []
10631057
with connection.connection.database.snapshot() as snap:
10641058
rows = list(snap.execute_sql(sql))
@@ -1087,9 +1081,7 @@ def get_view_definition(self, connection, view_name, schema=None, **kw):
10871081
SELECT view_definition
10881082
FROM information_schema.views
10891083
WHERE TABLE_SCHEMA='{schema_name}' AND TABLE_NAME='{view_name}'
1090-
""".format(
1091-
schema_name=schema or "", view_name=view_name
1092-
)
1084+
""".format(schema_name=schema or "", view_name=view_name)
10931085

10941086
with connection.connection.database.snapshot() as snap:
10951087
rows = list(snap.execute_sql(sql))
@@ -1623,9 +1615,7 @@ def get_table_names(self, connection, schema=None, **kw):
16231615
SELECT table_name
16241616
FROM information_schema.tables
16251617
WHERE table_type = 'BASE TABLE' AND table_schema = '{schema}'
1626-
""".format(
1627-
schema=schema or ""
1628-
)
1618+
""".format(schema=schema or "")
16291619

16301620
table_names = []
16311621
with connection.connection.database.snapshot() as snap:
@@ -1661,9 +1651,7 @@ def get_unique_constraints(self, connection, table_name, schema=None, **kw):
16611651
AND tc.TABLE_SCHEMA="{table_schema}"
16621652
AND tc.CONSTRAINT_TYPE = "UNIQUE"
16631653
AND tc.CONSTRAINT_NAME IS NOT NULL
1664-
""".format(
1665-
table_schema=schema or "", table_name=table_name
1666-
)
1654+
""".format(table_schema=schema or "", table_name=table_name)
16671655

16681656
cols = []
16691657
with connection.connection.database.snapshot() as snap:
@@ -1696,9 +1684,7 @@ def has_table(self, connection, table_name, schema=None, **kw):
16961684
FROM INFORMATION_SCHEMA.TABLES
16971685
WHERE TABLE_SCHEMA="{table_schema}" AND TABLE_NAME="{table_name}"
16981686
LIMIT 1
1699-
""".format(
1700-
table_schema=schema or "", table_name=table_name
1701-
)
1687+
""".format(table_schema=schema or "", table_name=table_name)
17021688
)
17031689

17041690
for _ in rows:
@@ -1723,9 +1709,7 @@ def has_sequence(self, connection, sequence_name, schema=None, **kw):
17231709
WHERE NAME="{sequence_name}"
17241710
AND SCHEMA="{schema}"
17251711
LIMIT 1
1726-
""".format(
1727-
sequence_name=sequence_name, schema=schema or ""
1728-
)
1712+
""".format(sequence_name=sequence_name, schema=schema or "")
17291713
)
17301714

17311715
for _ in rows:

0 commit comments

Comments
 (0)