Skip to content

Commit d502057

Browse files
committed
spanner utils, conftest, write_batch
1 parent 68223f2 commit d502057

10 files changed

Lines changed: 559 additions & 959 deletions

tools/spanner/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM python:3.13-bookworm
1+
FROM python:3.14-bookworm
22

33
ENV PYTHONUNBUFFERED=1 \
44
PYTHONDONTWRITEBYTECODE=1 \

tools/spanner/count_expired_rows.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
# License, v. 2.0. If a copy of the MPL was not distributed with this
66
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
77

8+
from __future__ import annotations
9+
810
import sys
911
import logging
12+
from typing import Any
1013
from statsd.defaults.env import statsd
1114

12-
from google.cloud import spanner
15+
from google.cloud import spanner # type: ignore[attr-defined]
1316
from tools.spanner.utils import ids_from_env
1417

1518
# set up logger
@@ -20,7 +23,7 @@
2023
)
2124

2225
# Change these to match your install.
23-
client = spanner.Client()
26+
client: Any = spanner.Client()
2427

2528

2629
def spanner_read_data(query: str, table: str) -> None:

tools/spanner/poetry.lock

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

tools/spanner/purge_ttl.py

Lines changed: 46 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@
44
# License, v. 2.0. If a copy of the MPL was not distributed with this
55
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
66

7+
from __future__ import annotations
8+
79
import argparse
810
import logging
911
import os
1012
import sys
1113
from datetime import datetime
12-
from typing import List, Optional
14+
from typing import Any
1315

1416

15-
from google.cloud import spanner
16-
from google.cloud.spanner_v1.database import Database
17-
from google.cloud.spanner_v1 import param_types
17+
from google.cloud import spanner # type: ignore[attr-defined]
18+
from google.cloud.spanner_v1 import param_types as param_types
1819
from statsd.defaults.env import statsd
1920

2021
from tools.spanner.utils import ids_from_env, Mode
@@ -31,14 +32,15 @@
3132

3233

3334
def deleter(
34-
database: Database,
35+
database: Any,
3536
name: str,
3637
query: str,
37-
prefix: Optional[str] = None,
38-
params: Optional[dict] = None,
39-
param_types: Optional[dict] = None,
40-
dryrun: Optional[bool] = False,
41-
):
38+
prefix: str | None = None,
39+
params: dict[str, Any] | None = None,
40+
param_types: dict[str, Any] | None = None,
41+
dryrun: bool | None = False,
42+
) -> None:
43+
"""Execute a partitioned DML delete and emit statsd timing metrics."""
4244
with statsd.timer(f"syncstorage.purge_ttl.{name}_duration"):
4345
logging.info(f"Running: {query} :: {params}")
4446
start = datetime.now()
@@ -53,16 +55,23 @@ def deleter(
5355
)
5456

5557

56-
def add_conditions(args, query: str, prefix: Optional[str]):
57-
"""
58-
Add SQL conditions to a query.
59-
:param args: The program arguments
60-
:param query: The SQL query
61-
:param prefix: The current prefix, if given
62-
:return: The updated SQL query, and list of params
58+
def add_conditions(
59+
args: argparse.Namespace,
60+
query: str,
61+
prefix: str | None,
62+
) -> tuple[str, dict[str, Any], dict[str, Any]]:
63+
"""Add SQL conditions to a query based on collection IDs and UID prefix.
64+
65+
Args:
66+
args: Parsed command-line arguments.
67+
query: The base SQL query.
68+
prefix: Optional UID prefix to filter rows.
69+
70+
Returns:
71+
A 3-tuple of (updated query, params dict, param_types dict).
6372
"""
64-
params = {}
65-
types = {}
73+
params: dict[str, Any] = {}
74+
types: dict[str, Any] = {}
6675
if args.collection_ids:
6776
ids = list(filter(len, args.collection_ids))
6877
if ids:
@@ -84,12 +93,8 @@ def add_conditions(args, query: str, prefix: Optional[str]):
8493
return (query, params, types)
8594

8695

87-
def get_expiry_condition(args):
88-
"""
89-
Get the expiry SQL WHERE condition to use
90-
:param args: The program arguments
91-
:return: A SQL snippet to use in the WHERE clause
92-
"""
96+
def get_expiry_condition(args: argparse.Namespace) -> str:
97+
"""Return the expiry WHERE condition SQL snippet for the given expiry mode."""
9398
if args.expiry_mode == "now":
9499
return "expiry < CURRENT_TIMESTAMP()"
95100
elif args.expiry_mode == "midnight":
@@ -98,14 +103,13 @@ def get_expiry_condition(args):
98103
raise Exception(f"Invalid expiry mode: {args.expiry_mode}")
99104

100105

101-
def spanner_purge(args) -> None:
102-
"""
103-
Purges expired TTL records from Spanner based on the provided arguments.
106+
def spanner_purge(args: argparse.Namespace) -> None:
107+
"""Purge expired TTL records from Spanner based on the provided arguments.
104108
105-
This function connects to the specified Spanner instance and database,
106-
determines the expiry condition, and deletes expired records from the
107-
'batches' and/or 'bsos' tables according to the purge mode. Supports
108-
filtering by collection IDs and UID prefixes, and can operate in dry-run mode.
109+
Connects to the specified Spanner instance and database, determines the
110+
expiry condition, and deletes expired records from the 'batches' and/or
111+
'bsos' tables. Supports filtering by collection IDs and UID prefixes,
112+
and can operate in dry-run mode.
109113
110114
Args:
111115
args (argparse.Namespace): Parsed command-line arguments containing
@@ -161,10 +165,12 @@ def spanner_purge(args) -> None:
161165
)
162166

163167

164-
def get_args():
165-
"""
166-
Parses and returns command-line arguments for the Spanner TTL purge tool.
167-
If a DSN URL is provided, usually `SYNC_SYNCSTORAGE__DATABASE_URL`, its values override the corresponding arguments.
168+
def get_args() -> argparse.Namespace:
169+
"""Parse and return CLI arguments for the Spanner TTL purge tool.
170+
171+
If a DSN URL is provided via --sync_database_url or
172+
SYNC_SYNCSTORAGE__DATABASE_URL, its values override the corresponding
173+
instance_id, database_id, and project_id arguments.
168174
169175
Returns:
170176
argparse.Namespace: Parsed command-line arguments with the following attributes:
@@ -257,15 +263,14 @@ def get_args():
257263
return args
258264

259265

260-
def parse_args_list(args_list: str) -> List[str]:
261-
"""
262-
Parses a string representing a list of items into a list of strings.
266+
def parse_args_list(args_list: str) -> list[str]:
267+
"""Parse a bracketed comma-separated string into a list of strings.
263268
264269
Args:
265-
args_list (str): String to parse, e.g., "[item1,item2,item3]" or "item1".
270+
args_list: String to parse, e.g., "[item1,item2,item3]" or "item1".
266271
267272
Returns:
268-
List[str]: List of parsed string items.
273+
List of parsed string items.
269274
"""
270275
if args_list[0] != "[" or args_list[-1] != "]":
271276
# Assume it's a single item

tools/spanner/pyproject.toml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,19 @@ authors = [
77
]
88
license = "Mozilla Public License Version 2.0"
99
readme = "README.md"
10-
requires-python = ">=3.9.2"
10+
requires-python = ">=3.12,<4.0"
1111

1212
[tool.poetry]
13-
package-mode = false
13+
package-mode = false
1414

1515
[tool.poetry.dependencies]
16-
google-cloud-spanner = ">=1.16.0"
16+
google-cloud-spanner = ">=3.63.0"
17+
protobuf = ">=5.29,<7.0.0"
1718
statsd = "^4.0.1"
1819

1920
[tool.poetry.group.dev.dependencies]
2021
mypy = "^1.16.0"
22+
pytest = "^8.4.0"
2123
pydocstyle = "^6.3.0"
2224
ruff = "^0.12.7"
2325
black = "^25.1.0"
@@ -27,6 +29,14 @@ isort = "^6.0.1"
2729
[tool.poetry.requires-plugins]
2830
poetry-plugin-export = ">=1.8"
2931

32+
[tool.mypy]
33+
python_version = "3.14"
34+
ignore_missing_imports = true
35+
strict = true
36+
37+
[tool.pytest.ini_options]
38+
pythonpath = ["../.."]
39+
3040
[build-system]
3141
requires = ["poetry-core>=2.0.0,<3.0.0"]
3242
build-backend = "poetry.core.masonry.api"

tools/spanner/test_count_expired_rows.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1-
from unittest.mock import MagicMock
1+
from __future__ import annotations
2+
23
import logging
4+
from unittest.mock import MagicMock
5+
6+
import pytest
37

48
from tools.spanner import count_expired_rows
59

610

7-
def test_spanner_read_data_counts_and_logs(monkeypatch, caplog):
11+
def test_spanner_read_data_counts_and_logs(
12+
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
13+
) -> None:
14+
"""spanner_read_data logs row counts and emits statsd gauge and timer metrics."""
815
# Prepare mocks
916
mock_instance = MagicMock()
1017
mock_database = MagicMock()

0 commit comments

Comments
 (0)