diff --git a/.github/blunderbuss.yml b/.github/blunderbuss.yml index 68b2d1df..47964222 100644 --- a/.github/blunderbuss.yml +++ b/.github/blunderbuss.yml @@ -1,2 +1,2 @@ assign_issues: - - harshachinta + - olavloite diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index 23f28e19..d1627b90 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -10,7 +10,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 with: @@ -25,7 +25,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 with: @@ -43,7 +43,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 with: @@ -58,7 +58,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 with: @@ -74,13 +74,13 @@ jobs: services: emulator-0: - image: gcr.io/cloud-spanner-emulator/emulator:latest + image: gcr.io/cloud-spanner-emulator/emulator ports: - 9010:9010 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 with: @@ -98,13 +98,13 @@ jobs: services: emulator-0: - image: gcr.io/cloud-spanner-emulator/emulator:latest + image: gcr.io/cloud-spanner-emulator/emulator ports: - 9010:9010 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 with: @@ -123,13 +123,13 @@ jobs: services: emulator-0: - image: gcr.io/cloud-spanner-emulator/emulator:latest + image: gcr.io/cloud-spanner-emulator/emulator ports: - 9010:9010 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 with: @@ -153,7 +153,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 with: @@ -177,7 +177,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 with: @@ -201,7 +201,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 with: diff --git a/.gitignore b/.gitignore index 6cb13eed..e62c3479 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,7 @@ docs/_build docs.metadata # Virtual environment +.venv/ env/ coverage.xml sponge_log.xml @@ -59,4 +60,5 @@ system_tests/local_test_setup pylintrc pylintrc.test -test.cfg \ No newline at end of file +test.cfg +database diff --git a/CHANGELOG.md b/CHANGELOG.md index fc7918b0..1db3c76f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [1.15.0](https://github.com/googleapis/python-spanner-sqlalchemy/compare/v1.14.0...v1.15.0) (2025-08-19) + + +### Features + +* Add license metadata to setup.py ([#712](https://github.com/googleapis/python-spanner-sqlalchemy/issues/712)) ([8f2e97e](https://github.com/googleapis/python-spanner-sqlalchemy/commit/8f2e97e527b00bfb6db40d946a21f522177eab7b)) +* Enable SQLAlchemy 2.0's insertmany feature ([#721](https://github.com/googleapis/python-spanner-sqlalchemy/issues/721)) ([1fe9f4b](https://github.com/googleapis/python-spanner-sqlalchemy/commit/1fe9f4b0a2f94d66c925d1d60a1fd83fc45e9c89)) +* Support informational foreign keys ([#719](https://github.com/googleapis/python-spanner-sqlalchemy/issues/719)) ([c565ae1](https://github.com/googleapis/python-spanner-sqlalchemy/commit/c565ae12b1b429c66037e9cd0c4be427a60ab5b0)) + + +### Bug Fixes + +* Report column defaults in introspection ([#744](https://github.com/googleapis/python-spanner-sqlalchemy/issues/744)) ([309c641](https://github.com/googleapis/python-spanner-sqlalchemy/commit/309c64179d668dbe24881e6d7fb4783fb1d8bbf2)), closes [#730](https://github.com/googleapis/python-spanner-sqlalchemy/issues/730) +* Respect existing server default in alter column DDL ([#733](https://github.com/googleapis/python-spanner-sqlalchemy/issues/733)) ([1f8a25f](https://github.com/googleapis/python-spanner-sqlalchemy/commit/1f8a25f63286c1241141985d4f10f558e929a272)) + ## [1.14.0](https://github.com/googleapis/python-spanner-sqlalchemy/compare/v1.13.1...v1.14.0) (2025-06-27) diff --git a/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py b/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py index d868daf9..480747b0 100644 --- a/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py +++ b/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py @@ -20,6 +20,7 @@ ColumnType, alter_column, alter_table, + format_server_default, format_type, ) from google.api_core.client_options import ClientOptions @@ -425,7 +426,14 @@ def returning_clause(self, stmt, returning_cols, **kw): self._label_select_column( None, c, True, False, {"spanner_is_returning": True} ) - for c in expression._select_iterables(returning_cols) + for c in expression._select_iterables( + filter( + lambda col: not col.dialect_options.get("spanner", {}).get( + "exclude_from_returning", False + ), + returning_cols, + ) + ) ] return "THEN RETURN " + ", ".join(columns) @@ -572,7 +580,7 @@ def get_column_specification(self, column, **kwargs): elif has_identity: colspec += " " + self.process(column.identity) elif default is not None: - colspec += " DEFAULT (" + default + ")" + colspec += f" DEFAULT {default}" elif hasattr(column, "computed") and column.computed is not None: colspec += " " + self.process(column.computed) @@ -583,6 +591,13 @@ def get_column_specification(self, column, **kwargs): return colspec + def get_column_default_string(self, column): + default = super().get_column_default_string(column) + if default is not None: + return f"({default})" + + return default + def visit_computed_column(self, generated, **kw): """Computed column operator.""" text = "AS (%s) STORED" % self.sql_compiler.process( @@ -652,6 +667,12 @@ def visit_unique_constraint(self, constraint, **kw): "Create UNIQUE indexes instead." ) + def visit_foreign_key_constraint(self, constraint, **kw): + text = super().visit_foreign_key_constraint(constraint, **kw) + if constraint.dialect_options.get("spanner", {}).get("not_enforced", False): + text += " NOT ENFORCED" + return text + def post_create_table(self, table): """Build statements to be executed after CREATE TABLE. @@ -825,6 +846,7 @@ class SpannerDialect(DefaultDialect): update_returning = True delete_returning = True supports_multivalues_insert = True + use_insertmanyvalues = True ddl_compiler = SpannerDDLCompiler preparer = SpannerIdentifierPreparer @@ -1100,7 +1122,8 @@ def get_multi_columns( sql = """ SELECT col.table_schema, col.table_name, col.column_name, - col.spanner_type, col.is_nullable, col.generation_expression + col.spanner_type, col.is_nullable, col.generation_expression, + col.column_default FROM information_schema.columns as col JOIN information_schema.tables AS t USING (TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME) @@ -1128,7 +1151,7 @@ def get_multi_columns( "name": col[2], "type": self._designate_type(col[3]), "nullable": col[4] == "YES", - "default": None, + "default": col[6] if col[6] is not None else None, } if col[5] is not None: @@ -1846,11 +1869,14 @@ def do_execute_no_params(self, cursor, statement, context=None): def visit_column_nullable( element: "ColumnNullable", compiler: "SpannerDDLCompiler", **kw ) -> str: - return "%s %s %s %s" % ( - alter_table(compiler, element.table_name, element.schema), - alter_column(compiler, element.column_name), - format_type(compiler, element.existing_type), - "" if element.nullable else "NOT NULL", + return _format_alter_column( + compiler, + element.table_name, + element.schema, + element.column_name, + element.existing_type, + element.nullable, + element.existing_server_default, ) @@ -1859,9 +1885,34 @@ def visit_column_nullable( def visit_column_type( element: "ColumnType", compiler: "SpannerDDLCompiler", **kw ) -> str: - return "%s %s %s %s" % ( - alter_table(compiler, element.table_name, element.schema), - alter_column(compiler, element.column_name), - "%s" % format_type(compiler, element.type_), - "" if element.existing_nullable else "NOT NULL", + return _format_alter_column( + compiler, + element.table_name, + element.schema, + element.column_name, + element.type_, + element.existing_nullable, + element.existing_server_default, + ) + + +def _format_alter_column( + compiler, table_name, schema, column_name, type_, nullable, server_default +): + # Older versions of SQLAlchemy pass in a boolean to indicate whether there + # is an existing DEFAULT constraint, instead of the actual DEFAULT constraint + # expression. In those cases, we do not want to explicitly include the DEFAULT + # constraint in the expression that is generated here. + if isinstance(server_default, bool): + server_default = None + return "%s %s %s%s%s" % ( + alter_table(compiler, table_name, schema), + alter_column(compiler, column_name), + format_type(compiler, type_), + "" if nullable else " NOT NULL", + ( + "" + if server_default is None + else f" DEFAULT {format_server_default(compiler, server_default)}" + ), ) diff --git a/google/cloud/sqlalchemy_spanner/version.py b/google/cloud/sqlalchemy_spanner/version.py index af1f26ba..a55a585c 100644 --- a/google/cloud/sqlalchemy_spanner/version.py +++ b/google/cloud/sqlalchemy_spanner/version.py @@ -4,4 +4,4 @@ # license that can be found in the LICENSE file or at # https://developers.google.com/open-source/licenses/bsd -__version__ = "1.14.0" +__version__ = "1.15.0" diff --git a/requirements.txt b/requirements.txt index a4f6d24f..843c977b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,13 +4,13 @@ # # pip-compile --generate-hashes # -alembic==1.16.2 \ - --hash=sha256:5f42e9bd0afdbd1d5e3ad856c01754530367debdebf21ed6894e34af52b3bb03 \ - --hash=sha256:e53c38ff88dadb92eb22f8b150708367db731d58ad7e9d417c9168ab516cbed8 +alembic==1.16.4 \ + --hash=sha256:b05e51e8e82efc1abd14ba2af6392897e145930c3e0a2faf2b0da2f7f7fd660d \ + --hash=sha256:efab6ada0dd0fae2c92060800e0bf5c1dc26af15a10e02fb4babff164b4725e2 # via -r requirements.in -build==1.2.2.post1 \ - --hash=sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5 \ - --hash=sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7 +build==1.3.0 \ + --hash=sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397 \ + --hash=sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4 # via # -r requirements.in # pip-tools @@ -18,103 +18,90 @@ cachetools==6.1.0 \ --hash=sha256:1c7bb3cf9193deaf3508b7c5f2a79986c13ea38965c5adcff1f84519cf39163e \ --hash=sha256:b4c4f404392848db3ce7aac34950d17be4d864da4b8b66911008e430bc544587 # via google-auth -certifi==2025.6.15 \ - --hash=sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057 \ - --hash=sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b +certifi==2025.8.3 \ + --hash=sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407 \ + --hash=sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5 # via requests -charset-normalizer==3.4.2 \ - --hash=sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4 \ - --hash=sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45 \ - --hash=sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7 \ - --hash=sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0 \ - --hash=sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7 \ - --hash=sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d \ - --hash=sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d \ - --hash=sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0 \ - --hash=sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184 \ - --hash=sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db \ - --hash=sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b \ - --hash=sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64 \ - --hash=sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b \ - --hash=sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8 \ - --hash=sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff \ - --hash=sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344 \ - --hash=sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58 \ - --hash=sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e \ - --hash=sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471 \ - --hash=sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148 \ - --hash=sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a \ - --hash=sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836 \ - --hash=sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e \ - --hash=sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63 \ - --hash=sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c \ - --hash=sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1 \ - --hash=sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01 \ - --hash=sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366 \ - --hash=sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58 \ - --hash=sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5 \ - --hash=sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c \ - --hash=sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2 \ - --hash=sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a \ - --hash=sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597 \ - --hash=sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b \ - --hash=sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5 \ - --hash=sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb \ - --hash=sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f \ - --hash=sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0 \ - --hash=sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941 \ - --hash=sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0 \ - --hash=sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86 \ - --hash=sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7 \ - --hash=sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7 \ - --hash=sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455 \ - --hash=sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6 \ - --hash=sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4 \ - --hash=sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0 \ - --hash=sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3 \ - --hash=sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1 \ - --hash=sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6 \ - --hash=sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981 \ - --hash=sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c \ - --hash=sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980 \ - --hash=sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645 \ - --hash=sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7 \ - --hash=sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12 \ - --hash=sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa \ - --hash=sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd \ - --hash=sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef \ - --hash=sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f \ - --hash=sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2 \ - --hash=sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d \ - --hash=sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5 \ - --hash=sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02 \ - --hash=sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3 \ - --hash=sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd \ - --hash=sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e \ - --hash=sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214 \ - --hash=sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd \ - --hash=sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a \ - --hash=sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c \ - --hash=sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681 \ - --hash=sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba \ - --hash=sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f \ - --hash=sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a \ - --hash=sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28 \ - --hash=sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691 \ - --hash=sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82 \ - --hash=sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a \ - --hash=sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027 \ - --hash=sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7 \ - --hash=sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518 \ - --hash=sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf \ - --hash=sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b \ - --hash=sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9 \ - --hash=sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544 \ - --hash=sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da \ - --hash=sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509 \ - --hash=sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f \ - --hash=sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a \ - --hash=sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f +charset-normalizer==3.4.3 \ + --hash=sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91 \ + --hash=sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0 \ + --hash=sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154 \ + --hash=sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601 \ + --hash=sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884 \ + --hash=sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07 \ + --hash=sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c \ + --hash=sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64 \ + --hash=sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe \ + --hash=sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f \ + --hash=sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432 \ + --hash=sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc \ + --hash=sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa \ + --hash=sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9 \ + --hash=sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae \ + --hash=sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19 \ + --hash=sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d \ + --hash=sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e \ + --hash=sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4 \ + --hash=sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7 \ + --hash=sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312 \ + --hash=sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92 \ + --hash=sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31 \ + --hash=sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c \ + --hash=sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f \ + --hash=sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99 \ + --hash=sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b \ + --hash=sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15 \ + --hash=sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392 \ + --hash=sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f \ + --hash=sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8 \ + --hash=sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491 \ + --hash=sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0 \ + --hash=sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc \ + --hash=sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0 \ + --hash=sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f \ + --hash=sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a \ + --hash=sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40 \ + --hash=sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927 \ + --hash=sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849 \ + --hash=sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce \ + --hash=sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14 \ + --hash=sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05 \ + --hash=sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c \ + --hash=sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c \ + --hash=sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a \ + --hash=sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc \ + --hash=sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34 \ + --hash=sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9 \ + --hash=sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096 \ + --hash=sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14 \ + --hash=sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30 \ + --hash=sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b \ + --hash=sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b \ + --hash=sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942 \ + --hash=sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db \ + --hash=sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5 \ + --hash=sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b \ + --hash=sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce \ + --hash=sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669 \ + --hash=sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0 \ + --hash=sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018 \ + --hash=sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93 \ + --hash=sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe \ + --hash=sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049 \ + --hash=sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a \ + --hash=sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef \ + --hash=sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2 \ + --hash=sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca \ + --hash=sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16 \ + --hash=sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f \ + --hash=sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb \ + --hash=sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1 \ + --hash=sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557 \ + --hash=sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37 \ + --hash=sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7 \ + --hash=sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72 \ + --hash=sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c \ + --hash=sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9 # via requests click==8.2.1 \ --hash=sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202 \ @@ -144,9 +131,9 @@ google-cloud-core==2.4.3 \ --hash=sha256:1fab62d7102844b278fe6dead3af32408b1df3eb06f5c7e8634cbd40edc4da53 \ --hash=sha256:5130f9f4c14b4fafdff75c79448f9495cfade0d8775facf1b09c3bf67e027f6e # via google-cloud-spanner -google-cloud-spanner==3.55.0 \ - --hash=sha256:bec170c6619f667cc657e977f87391d76975559be70b155d90a2902613662b3c \ - --hash=sha256:fc9f717b612924f5e9bfae9514aa0d5cd30e6b40e8d472d030f84b16de2c18fe +google-cloud-spanner==3.57.0 \ + --hash=sha256:5b10b40bc646091f1b4cbb2e7e2e82ec66bcce52c7105f86b65070d34d6df86f \ + --hash=sha256:73f52f58617449fcff7073274a7f7a798f4f7b2788eda26de3b7f98ad857ab99 # via -r requirements.in googleapis-common-protos[grpc]==1.70.0 \ --hash=sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257 \ @@ -155,61 +142,61 @@ googleapis-common-protos[grpc]==1.70.0 \ # google-api-core # grpc-google-iam-v1 # grpcio-status -greenlet==3.2.3 \ - --hash=sha256:003c930e0e074db83559edc8705f3a2d066d4aa8c2f198aff1e454946efd0f26 \ - --hash=sha256:024571bbce5f2c1cfff08bf3fbaa43bbc7444f580ae13b0099e95d0e6e67ed36 \ - --hash=sha256:02b0df6f63cd15012bed5401b47829cfd2e97052dc89da3cfaf2c779124eb892 \ - --hash=sha256:0921ac4ea42a5315d3446120ad48f90c3a6b9bb93dd9b3cf4e4d84a66e42de83 \ - --hash=sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688 \ - --hash=sha256:1afd685acd5597349ee6d7a88a8bec83ce13c106ac78c196ee9dde7c04fe87be \ - --hash=sha256:22eb5ba839c4b2156f18f76768233fe44b23a31decd9cc0d4cc8141c211fd1b4 \ - --hash=sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d \ - --hash=sha256:29e184536ba333003540790ba29829ac14bb645514fbd7e32af331e8202a62a5 \ - --hash=sha256:2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b \ - --hash=sha256:2d8aa5423cd4a396792f6d4580f88bdc6efcb9205891c9d40d20f6e670992efb \ - --hash=sha256:3d04332dddb10b4a211b68111dabaee2e1a073663d117dc10247b5b1642bac86 \ - --hash=sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c \ - --hash=sha256:42efc522c0bd75ffa11a71e09cd8a399d83fafe36db250a87cf1dacfaa15dc64 \ - --hash=sha256:4532f0d25df67f896d137431b13f4cdce89f7e3d4a96387a41290910df4d3a57 \ - --hash=sha256:49c8cfb18fb419b3d08e011228ef8a25882397f3a859b9fe1436946140b6756b \ - --hash=sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad \ - --hash=sha256:5035d77a27b7c62db6cf41cf786cfe2242644a7a337a0e155c80960598baab95 \ - --hash=sha256:5195fb1e75e592dd04ce79881c8a22becdfa3e6f500e7feb059b1e6fdd54d3e3 \ - --hash=sha256:592c12fb1165be74592f5de0d70f82bc5ba552ac44800d632214b76089945147 \ - --hash=sha256:68671180e3849b963649254a882cd544a3c75bfcd2c527346ad8bb53494444db \ - --hash=sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb \ - --hash=sha256:72e77ed69312bab0434d7292316d5afd6896192ac4327d44f3d613ecb85b037c \ - --hash=sha256:731e154aba8e757aedd0781d4b240f1225b075b4409f1bb83b05ff410582cf00 \ - --hash=sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849 \ - --hash=sha256:751261fc5ad7b6705f5f76726567375bb2104a059454e0226e1eef6c756748ba \ - --hash=sha256:761917cac215c61e9dc7324b2606107b3b292a8349bdebb31503ab4de3f559ac \ - --hash=sha256:784ae58bba89fa1fa5733d170d42486580cab9decda3484779f4759345b29822 \ - --hash=sha256:7e70ea4384b81ef9e84192e8a77fb87573138aa5d4feee541d8014e452b434da \ - --hash=sha256:8186162dffde068a465deab08fc72c767196895c39db26ab1c17c0b77a6d8b97 \ - --hash=sha256:8324319cbd7b35b97990090808fdc99c27fe5338f87db50514959f8059999805 \ - --hash=sha256:83a8761c75312361aa2b5b903b79da97f13f556164a7dd2d5448655425bd4c34 \ - --hash=sha256:86c2d68e87107c1792e2e8d5399acec2487a4e993ab76c792408e59394d52141 \ - --hash=sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3 \ - --hash=sha256:873abe55f134c48e1f2a6f53f7d1419192a3d1a4e873bace00499a4e45ea6af0 \ - --hash=sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b \ - --hash=sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365 \ - --hash=sha256:8c37ef5b3787567d322331d5250e44e42b58c8c713859b8a04c6065f27efbf72 \ - --hash=sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a \ - --hash=sha256:93c0bb79844a367782ec4f429d07589417052e621aa39a5ac1fb99c5aa308edc \ - --hash=sha256:93d48533fade144203816783373f27a97e4193177ebaaf0fc396db19e5d61163 \ - --hash=sha256:96c20252c2f792defe9a115d3287e14811036d51e78b3aaddbee23b69b216302 \ - --hash=sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef \ - --hash=sha256:a433dbc54e4a37e4fff90ef34f25a8c00aed99b06856f0119dcf09fbafa16392 \ - --hash=sha256:aaa7aae1e7f75eaa3ae400ad98f8644bb81e1dc6ba47ce8a93d3f17274e08322 \ - --hash=sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d \ - --hash=sha256:be52af4b6292baecfa0f397f3edb3c6092ce071b499dd6fe292c9ac9f2c8f264 \ - --hash=sha256:c667c0bf9d406b77a15c924ef3285e1e05250948001220368e039b6aa5b5034b \ - --hash=sha256:ce539fb52fb774d0802175d37fcff5c723e2c7d249c65916257f0a940cee8904 \ - --hash=sha256:d2971d93bb99e05f8c2c0c2f4aa9484a18d98c4c3bd3c62b65b7e6ae33dfcfaf \ - --hash=sha256:d760f9bdfe79bff803bad32b4d8ffb2c1d2ce906313fc10a83976ffb73d64ca7 \ - --hash=sha256:ed6cfa9200484d234d8394c70f5492f144b20d4533f69262d530a1a082f6ee9a \ - --hash=sha256:efc6dc8a792243c31f2f5674b670b3a95d46fa1c6a912b8e310d6f542e7b0712 \ - --hash=sha256:f4bfbaa6096b1b7a200024784217defedf46a07c2eee1a498e94a1b5f8ec5728 +greenlet==3.2.4 \ + --hash=sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b \ + --hash=sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735 \ + --hash=sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079 \ + --hash=sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d \ + --hash=sha256:16458c245a38991aa19676900d48bd1a6f2ce3e16595051a4db9d012154e8433 \ + --hash=sha256:18d9260df2b5fbf41ae5139e1be4e796d99655f023a636cd0e11e6406cca7d58 \ + --hash=sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52 \ + --hash=sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31 \ + --hash=sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246 \ + --hash=sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f \ + --hash=sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671 \ + --hash=sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8 \ + --hash=sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d \ + --hash=sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f \ + --hash=sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0 \ + --hash=sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd \ + --hash=sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337 \ + --hash=sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0 \ + --hash=sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633 \ + --hash=sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b \ + --hash=sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa \ + --hash=sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31 \ + --hash=sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9 \ + --hash=sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b \ + --hash=sha256:671df96c1f23c4a0d4077a325483c1503c96a1b7d9db26592ae770daa41233d4 \ + --hash=sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc \ + --hash=sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c \ + --hash=sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98 \ + --hash=sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f \ + --hash=sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c \ + --hash=sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590 \ + --hash=sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3 \ + --hash=sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2 \ + --hash=sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9 \ + --hash=sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5 \ + --hash=sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02 \ + --hash=sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0 \ + --hash=sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1 \ + --hash=sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c \ + --hash=sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594 \ + --hash=sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5 \ + --hash=sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d \ + --hash=sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a \ + --hash=sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6 \ + --hash=sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b \ + --hash=sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df \ + --hash=sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945 \ + --hash=sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae \ + --hash=sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb \ + --hash=sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504 \ + --hash=sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb \ + --hash=sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01 \ + --hash=sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c \ + --hash=sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968 # via sqlalchemy grpc-google-iam-v1==0.14.2 \ --hash=sha256:a3171468459770907926d56a440b2bb643eec1d7ba215f48f3ecece42b4d8351 \ @@ -219,67 +206,67 @@ grpc-interceptor==0.15.4 \ --hash=sha256:0035f33228693ed3767ee49d937bac424318db173fef4d2d0170b3215f254d9d \ --hash=sha256:1f45c0bcb58b6f332f37c637632247c9b02bc6af0fdceb7ba7ce8d2ebbfb0926 # via google-cloud-spanner -grpcio==1.73.1 \ - --hash=sha256:052e28fe9c41357da42250a91926a3e2f74c046575c070b69659467ca5aa976b \ - --hash=sha256:07f08705a5505c9b5b0cbcbabafb96462b5a15b7236bbf6bbcc6b0b91e1cbd7e \ - --hash=sha256:0a9f3ea8dce9eae9d7cb36827200133a72b37a63896e0e61a9d5ec7d61a59ab1 \ - --hash=sha256:0ab860d5bfa788c5a021fba264802e2593688cd965d1374d31d2b1a34cacd854 \ - --hash=sha256:105492124828911f85127e4825d1c1234b032cb9d238567876b5515d01151379 \ - --hash=sha256:10af9f2ab98a39f5b6c1896c6fc2036744b5b41d12739d48bed4c3e15b6cf900 \ - --hash=sha256:1c0bf15f629b1497436596b1cbddddfa3234273490229ca29561209778ebe182 \ - --hash=sha256:1c502c2e950fc7e8bf05c047e8a14522ef7babac59abbfde6dbf46b7a0d9c71e \ - --hash=sha256:24e06a5319e33041e322d32c62b1e728f18ab8c9dbc91729a3d9f9e3ed336642 \ - --hash=sha256:277b426a0ed341e8447fbf6c1d6b68c952adddf585ea4685aa563de0f03df887 \ - --hash=sha256:2d70f4ddd0a823436c2624640570ed6097e40935c9194482475fe8e3d9754d55 \ - --hash=sha256:303c8135d8ab176f8038c14cc10d698ae1db9c480f2b2823f7a987aa2a4c5646 \ - --hash=sha256:3841a8a5a66830261ab6a3c2a3dc539ed84e4ab019165f77b3eeb9f0ba621f26 \ - --hash=sha256:42f0660bce31b745eb9d23f094a332d31f210dcadd0fc8e5be7e4c62a87ce86b \ - --hash=sha256:45cf17dcce5ebdb7b4fe9e86cb338fa99d7d1bb71defc78228e1ddf8d0de8cbb \ - --hash=sha256:4a68f8c9966b94dff693670a5cf2b54888a48a5011c5d9ce2295a1a1465ee84f \ - --hash=sha256:5b9b1805a7d61c9e90541cbe8dfe0a593dfc8c5c3a43fe623701b6a01b01d710 \ - --hash=sha256:610e19b04f452ba6f402ac9aa94eb3d21fbc94553368008af634812c4a85a99e \ - --hash=sha256:628c30f8e77e0258ab788750ec92059fc3d6628590fb4b7cea8c102503623ed7 \ - --hash=sha256:65b0458a10b100d815a8426b1442bd17001fdb77ea13665b2f7dc9e8587fdc6b \ - --hash=sha256:67a0468256c9db6d5ecb1fde4bf409d016f42cef649323f0a08a72f352d1358b \ - --hash=sha256:686231cdd03a8a8055f798b2b54b19428cdf18fa1549bee92249b43607c42668 \ - --hash=sha256:68b84d65bbdebd5926eb5c53b0b9ec3b3f83408a30e4c20c373c5337b4219ec5 \ - --hash=sha256:6957025a4608bb0a5ff42abd75bfbb2ed99eda29d5992ef31d691ab54b753643 \ - --hash=sha256:6a2b372e65fad38842050943f42ce8fee00c6f2e8ea4f7754ba7478d26a356ee \ - --hash=sha256:6a6037891cd2b1dd1406b388660522e1565ed340b1fea2955b0234bdd941a862 \ - --hash=sha256:6abfc0f9153dc4924536f40336f88bd4fe7bd7494f028675e2e04291b8c2c62a \ - --hash=sha256:75fc8e543962ece2f7ecd32ada2d44c0c8570ae73ec92869f9af8b944863116d \ - --hash=sha256:7fce2cd1c0c1116cf3850564ebfc3264fba75d3c74a7414373f1238ea365ef87 \ - --hash=sha256:83a6c2cce218e28f5040429835fa34a29319071079e3169f9543c3fbeff166d2 \ - --hash=sha256:89018866a096e2ce21e05eabed1567479713ebe57b1db7cbb0f1e3b896793ba4 \ - --hash=sha256:8f5a6df3fba31a3485096ac85b2e34b9666ffb0590df0cd044f58694e6a1f6b5 \ - --hash=sha256:921b25618b084e75d424a9f8e6403bfeb7abef074bb6c3174701e0f2542debcf \ - --hash=sha256:96c112333309493c10e118d92f04594f9055774757f5d101b39f8150f8c25582 \ - --hash=sha256:ad1d958c31cc91ab050bd8a91355480b8e0683e21176522bacea225ce51163f2 \ - --hash=sha256:ad5c958cc3d98bb9d71714dc69f1c13aaf2f4b53e29d4cc3f1501ef2e4d129b2 \ - --hash=sha256:b310824ab5092cf74750ebd8a8a8981c1810cb2b363210e70d06ef37ad80d4f9 \ - --hash=sha256:b3215f69a0670a8cfa2ab53236d9e8026bfb7ead5d4baabe7d7dc11d30fda967 \ - --hash=sha256:b4adc97d2d7f5c660a5498bda978ebb866066ad10097265a5da0511323ae9f50 \ - --hash=sha256:ba2cea9f7ae4bc21f42015f0ec98f69ae4179848ad744b210e7685112fa507a1 \ - --hash=sha256:bc5eccfd9577a5dc7d5612b2ba90cca4ad14c6d949216c68585fdec9848befb1 \ - --hash=sha256:c45a28a0cfb6ddcc7dc50a29de44ecac53d115c3388b2782404218db51cb2df3 \ - --hash=sha256:c54796ca22b8349cc594d18b01099e39f2b7ffb586ad83217655781a350ce4da \ - --hash=sha256:cce7265b9617168c2d08ae570fcc2af4eaf72e84f8c710ca657cc546115263af \ - --hash=sha256:d60588ab6ba0ac753761ee0e5b30a29398306401bfbceffe7d68ebb21193f9d4 \ - --hash=sha256:d74c3f4f37b79e746271aa6cdb3a1d7e4432aea38735542b23adcabaaee0c097 \ - --hash=sha256:dc7d7fd520614fce2e6455ba89791458020a39716951c7c07694f9dbae28e9c0 \ - --hash=sha256:de18769aea47f18e782bf6819a37c1c528914bfd5683b8782b9da356506190c8 \ - --hash=sha256:ed451a0e39c8e51eb1612b78686839efd1a920666d1666c1adfdb4fd51680c0f \ - --hash=sha256:f43ffb3bd415c57224c7427bfb9e6c46a0b6e998754bfa0d00f408e1873dcbb5 \ - --hash=sha256:f48e862aed925ae987eb7084409a80985de75243389dc9d9c271dd711e589918 +grpcio==1.74.0 \ + --hash=sha256:0f87bddd6e27fc776aacf7ebfec367b6d49cad0455123951e4488ea99d9b9b8f \ + --hash=sha256:136b53c91ac1d02c8c24201bfdeb56f8b3ac3278668cbb8e0ba49c88069e1bdc \ + --hash=sha256:1733969040989f7acc3d94c22f55b4a9501a30f6aaacdbccfaba0a3ffb255ab7 \ + --hash=sha256:176d60a5168d7948539def20b2a3adcce67d72454d9ae05969a2e73f3a0feee7 \ + --hash=sha256:1a2b06afe2e50ebfd46247ac3ba60cac523f54ec7792ae9ba6073c12daf26f0a \ + --hash=sha256:1bf949792cee20d2078323a9b02bacbbae002b9e3b9e2433f2741c15bdeba1c4 \ + --hash=sha256:22b834cef33429ca6cc28303c9c327ba9a3fafecbf62fae17e9a7b7163cc43ac \ + --hash=sha256:2918948864fec2a11721d91568effffbe0a02b23ecd57f281391d986847982f6 \ + --hash=sha256:2bc2d7d8d184e2362b53905cb1708c84cb16354771c04b490485fa07ce3a1d89 \ + --hash=sha256:2f609a39f62a6f6f05c7512746798282546358a37ea93c1fcbadf8b2fed162e3 \ + --hash=sha256:3601274bc0523f6dc07666c0e01682c94472402ac2fd1226fd96e079863bfa49 \ + --hash=sha256:3b03d8f2a07f0fea8c8f74deb59f8352b770e3900d143b3d1475effcb08eec20 \ + --hash=sha256:3d14e3c4d65e19d8430a4e28ceb71ace4728776fd6c3ce34016947474479683f \ + --hash=sha256:42f8fee287427b94be63d916c90399ed310ed10aadbf9e2e5538b3e497d269bc \ + --hash=sha256:4bc5fca10aaf74779081e16c2bcc3d5ec643ffd528d9e7b1c9039000ead73bae \ + --hash=sha256:4e4181bfc24413d1e3a37a0b7889bea68d973d4b45dd2bc68bb766c140718f82 \ + --hash=sha256:55b453812fa7c7ce2f5c88be3018fb4a490519b6ce80788d5913f3f9d7da8c7b \ + --hash=sha256:566b9395b90cc3d0d0c6404bc8572c7c18786ede549cdb540ae27b58afe0fb91 \ + --hash=sha256:5f251c355167b2360537cf17bea2cf0197995e551ab9da6a0a59b3da5e8704f9 \ + --hash=sha256:60d2d48b0580e70d2e1954d0d19fa3c2e60dd7cbed826aca104fff518310d1c5 \ + --hash=sha256:64229c1e9cea079420527fa8ac45d80fc1e8d3f94deaa35643c381fa8d98f362 \ + --hash=sha256:655726919b75ab3c34cdad39da5c530ac6fa32696fb23119e36b64adcfca174a \ + --hash=sha256:662456c4513e298db6d7bd9c3b8df6f75f8752f0ba01fb653e252ed4a59b5a5d \ + --hash=sha256:68c8ebcca945efff9d86d8d6d7bfb0841cf0071024417e2d7f45c5e46b5b08eb \ + --hash=sha256:69e1a8180868a2576f02356565f16635b99088da7df3d45aaa7e24e73a054e31 \ + --hash=sha256:6bab67d15ad617aff094c382c882e0177637da73cbc5532d52c07b4ee887a87b \ + --hash=sha256:7d95d71ff35291bab3f1c52f52f474c632db26ea12700c2ff0ea0532cb0b5854 \ + --hash=sha256:80d1f4fbb35b0742d3e3d3bb654b7381cd5f015f8497279a1e9c21ba623e01b1 \ + --hash=sha256:834988b6c34515545b3edd13e902c1acdd9f2465d386ea5143fb558f153a7176 \ + --hash=sha256:8533e6e9c5bd630ca98062e3a1326249e6ada07d05acf191a77bc33f8948f3d8 \ + --hash=sha256:85bd5cdf4ed7b2d6438871adf6afff9af7096486fcf51818a81b77ef4dd30907 \ + --hash=sha256:86ad489db097141a907c559988c29718719aa3e13370d40e20506f11b4de0d11 \ + --hash=sha256:885912559974df35d92219e2dc98f51a16a48395f37b92865ad45186f294096c \ + --hash=sha256:8efe72fde5500f47aca1ef59495cb59c885afe04ac89dd11d810f2de87d935d4 \ + --hash=sha256:8f7b5882fb50632ab1e48cb3122d6df55b9afabc265582808036b6e51b9fd6b7 \ + --hash=sha256:9e7c4389771855a92934b2846bd807fc25a3dfa820fd912fe6bd8136026b2707 \ + --hash=sha256:9e912d3c993a29df6c627459af58975b2e5c897d93287939b9d5065f000249b5 \ + --hash=sha256:a8f0302f9ac4e9923f98d8e243939a6fb627cd048f5cd38595c97e38020dffce \ + --hash=sha256:b6a73b2ba83e663b2480a90b82fdae6a7aa6427f62bf43b29912c0cfd1aa2bfa \ + --hash=sha256:c14e803037e572c177ba54a3e090d6eb12efd795d49327c5ee2b3bddb836bf01 \ + --hash=sha256:c3d7bd6e3929fd2ea7fbc3f562e4987229ead70c9ae5f01501a46701e08f1ad9 \ + --hash=sha256:c98e0b7434a7fa4e3e63f250456eaef52499fba5ae661c58cc5b5477d11e7182 \ + --hash=sha256:cce634b10aeab37010449124814b05a62fb5f18928ca878f1bf4750d1f0c815b \ + --hash=sha256:e154d230dc1bbbd78ad2fdc3039fa50ad7ffcf438e4eb2fa30bce223a70c7486 \ + --hash=sha256:e1ea6176d7dfd5b941ea01c2ec34de9531ba494d541fe2057c904e601879f249 \ + --hash=sha256:e759f9e8bc908aaae0412642afe5416c9f983a80499448fcc7fab8692ae044c3 \ + --hash=sha256:e8978003816c7b9eabe217f88c78bc26adc8f9304bf6a594b02e5a49b2ef9c11 \ + --hash=sha256:ecde9ab49f58433abe02f9ed076c7b5be839cf0153883a6d23995937a82392fa \ + --hash=sha256:f6ec94f0e50eb8fa1744a731088b966427575e40c2944a980049798b127a687e \ + --hash=sha256:fd3c71aeee838299c5887230b8a1822795325ddfea635edd82954c1eaa831e24 \ + --hash=sha256:fe0f540750a13fd8e5da4b3eaba91a785eea8dca5ccd2bc2ffe978caa403090e # via # google-api-core # googleapis-common-protos # grpc-google-iam-v1 # grpc-interceptor # grpcio-status -grpcio-status==1.73.1 \ - --hash=sha256:538595c32a6c819c32b46a621a51e9ae4ffcd7e7e1bce35f728ef3447e9809b6 \ - --hash=sha256:928f49ccf9688db5f20cd9e45c4578a1d01ccca29aeaabf066f2ac76aa886668 +grpcio-status==1.74.0 \ + --hash=sha256:52cdbd759a6760fc8f668098a03f208f493dd5c76bf8e02598bbbaf1f6fc2876 \ + --hash=sha256:c58c1b24aa454e30f1fc6a7e0dbbc194c54a408143971a94b5f4e40bb5831432 # via google-api-core idna==3.10 \ --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ @@ -356,9 +343,9 @@ markupsafe==3.0.2 \ --hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430 \ --hash=sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50 # via mako -opentelemetry-api==1.34.1 \ - --hash=sha256:64f0bd06d42824843731d05beea88d4d4b6ae59f9fe347ff7dfa2cc14233bbb3 \ - --hash=sha256:b7df4cb0830d5a6c29ad0c0691dbae874d8daefa934b8b1d642de48323d32a8c +opentelemetry-api==1.36.0 \ + --hash=sha256:02f20bcacf666e1333b6b1f04e647dc1d5111f86b8e510238fcc56d7762cda8c \ + --hash=sha256:9a72572b9c416d004d492cbc6e61962c0501eaf945ece9b5a0f56597d8348aa0 # via # -r requirements.in # opentelemetry-instrumentation @@ -368,13 +355,13 @@ opentelemetry-instrumentation==0.48b0 \ --hash=sha256:94929685d906380743a71c3970f76b5f07476eea1834abd5dd9d17abfe23cc35 \ --hash=sha256:a69750dc4ba6a5c3eb67986a337185a25b739966d80479befe37b546fc870b44 # via -r requirements.in -opentelemetry-sdk==1.34.1 \ - --hash=sha256:308effad4059562f1d92163c61c8141df649da24ce361827812c40abb2a1e96e \ - --hash=sha256:8091db0d763fcd6098d4781bbc80ff0971f94e260739aa6afe6fd379cdf3aa4d +opentelemetry-sdk==1.36.0 \ + --hash=sha256:19c8c81599f51b71670661ff7495c905d8fdf6976e41622d5245b791b06fa581 \ + --hash=sha256:19fe048b42e98c5c1ffe85b569b7073576ad4ce0bcb6e9b4c6a39e890a6c45fb # via -r requirements.in -opentelemetry-semantic-conventions==0.54b1 \ - --hash=sha256:29dab644a7e435b58d3a3918b58c333c92686236b30f7891d5e51f02933ca60d \ - --hash=sha256:d1cecedae15d19bdaafca1e56b29a66aa286f50b5d08f036a145c7f3e9ef9cee +opentelemetry-semantic-conventions==0.55b1 \ + --hash=sha256:5da81dfdf7d52e3d37f8fe88d5e771e191de924cfff5f550ab0b8f7b2409baed \ + --hash=sha256:ef95b1f009159c28d7a7849f5cbc71c4c34c845bb514d66adfdf1b3fff3598b3 # via opentelemetry-sdk packaging==25.0 \ --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ @@ -386,9 +373,9 @@ pep517==0.13.1 \ --hash=sha256:1b2fa2ffd3938bb4beffe5d6146cbcb2bda996a5a4da9f31abffd8b24e07b317 \ --hash=sha256:31b206f67165b3536dd577c5c3f1518e8fbaf38cbc57efff8369a392feff1721 # via -r requirements.in -pip-tools==7.4.1 \ - --hash=sha256:4c690e5fbae2f21e87843e89c26191f0d9454f362d8acdbd695716493ec8b3a9 \ - --hash=sha256:864826f5073864450e24dbeeb85ce3920cdfb09848a3d69ebf537b521f14bcc9 +pip-tools==7.5.0 \ + --hash=sha256:30639f50961bb09f49d22f4389e8d7d990709677c094ce1114186b1f2e9b5821 \ + --hash=sha256:69758e4e5a65f160e315d74db46246fdbb30d549f1ed0c4236d057122c9b0f18 # via -r requirements.in proto-plus==1.26.1 \ --hash=sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66 \ @@ -396,18 +383,16 @@ proto-plus==1.26.1 \ # via # google-api-core # google-cloud-spanner -protobuf==5.29.5 \ - --hash=sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079 \ - --hash=sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc \ - --hash=sha256:470f3af547ef17847a28e1f47200a1cbf0ba3ff57b7de50d22776607cd2ea353 \ - --hash=sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61 \ - --hash=sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5 \ - --hash=sha256:6f642dc9a61782fa72b90878af134c5afe1917c89a568cd3476d758d3c3a0736 \ - --hash=sha256:7318608d56b6402d2ea7704ff1e1e4597bee46d760e7e4dd42a3d45e24b87f2e \ - --hash=sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84 \ - --hash=sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671 \ - --hash=sha256:ef91363ad4faba7b25d844ef1ada59ff1604184c0bcd8b39b8a6bef15e1af238 \ - --hash=sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015 +protobuf==6.32.0 \ + --hash=sha256:15eba1b86f193a407607112ceb9ea0ba9569aed24f93333fe9a497cf2fda37d3 \ + --hash=sha256:501fe6372fd1c8ea2a30b4d9be8f87955a64d6be9c88a973996cef5ef6f0abf1 \ + --hash=sha256:75a2aab2bd1aeb1f5dc7c5f33bcb11d82ea8c055c9becbb41c26a8c43fd7092c \ + --hash=sha256:7db8ed09024f115ac877a1427557b838705359f047b2ff2f2b2364892d19dacb \ + --hash=sha256:84f9e3c1ff6fb0308dbacb0950d8aa90694b0d0ee68e75719cb044b7078fe741 \ + --hash=sha256:a81439049127067fc49ec1d36e25c6ee1d1a2b7be930675f919258d03c04e7d2 \ + --hash=sha256:a8bdbb2f009cfc22a36d031f22a625a38b615b5e19e558a7b756b3279723e68e \ + --hash=sha256:ba377e5b67b908c8f3072a57b63e2c6a4cbd18aea4ed98d2584350dbf46f2783 \ + --hash=sha256:d52691e5bee6c860fff9a1c86ad26a13afbeb4b168cd4445c922b7e2cf85aaf0 # via # google-api-core # google-cloud-spanner @@ -435,72 +420,72 @@ pyproject-hooks==1.2.0 \ # via # build # pip-tools -requests==2.32.4 \ - --hash=sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c \ - --hash=sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422 +requests==2.32.5 \ + --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ + --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf # via google-api-core rsa==4.9.1 \ --hash=sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762 \ --hash=sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75 # via google-auth -sqlalchemy==2.0.41 \ - --hash=sha256:023b3ee6169969beea3bb72312e44d8b7c27c75b347942d943cf49397b7edeb5 \ - --hash=sha256:03968a349db483936c249f4d9cd14ff2c296adfa1290b660ba6516f973139582 \ - --hash=sha256:05132c906066142103b83d9c250b60508af556982a385d96c4eaa9fb9720ac2b \ - --hash=sha256:087b6b52de812741c27231b5a3586384d60c353fbd0e2f81405a814b5591dc8b \ - --hash=sha256:0b3dbf1e7e9bc95f4bac5e2fb6d3fb2f083254c3fdd20a1789af965caf2d2348 \ - --hash=sha256:118c16cd3f1b00c76d69343e38602006c9cfb9998fa4f798606d28d63f23beda \ - --hash=sha256:1936af879e3db023601196a1684d28e12f19ccf93af01bf3280a3262c4b6b4e5 \ - --hash=sha256:1e3f196a0c59b0cae9a0cd332eb1a4bda4696e863f4f1cf84ab0347992c548c2 \ - --hash=sha256:23a8825495d8b195c4aa9ff1c430c28f2c821e8c5e2d98089228af887e5d7e29 \ - --hash=sha256:293cd444d82b18da48c9f71cd7005844dbbd06ca19be1ccf6779154439eec0b8 \ - --hash=sha256:32f9dc8c44acdee06c8fc6440db9eae8b4af8b01e4b1aee7bdd7241c22edff4f \ - --hash=sha256:34ea30ab3ec98355235972dadc497bb659cc75f8292b760394824fab9cf39826 \ - --hash=sha256:3d3549fc3e40667ec7199033a4e40a2f669898a00a7b18a931d3efb4c7900504 \ - --hash=sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae \ - --hash=sha256:4d44522480e0bf34c3d63167b8cfa7289c1c54264c2950cc5fc26e7850967e45 \ - --hash=sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443 \ - --hash=sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23 \ - --hash=sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576 \ - --hash=sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1 \ - --hash=sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0 \ - --hash=sha256:5e22575d169529ac3e0a120cf050ec9daa94b6a9597993d1702884f6954a7d71 \ - --hash=sha256:60c578c45c949f909a4026b7807044e7e564adf793537fc762b2489d522f3d11 \ - --hash=sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e \ - --hash=sha256:6375cd674fe82d7aa9816d1cb96ec592bac1726c11e0cafbf40eeee9a4516b5f \ - --hash=sha256:6854175807af57bdb6425e47adbce7d20a4d79bbfd6f6d6519cd10bb7109a7f8 \ - --hash=sha256:6ab60a5089a8f02009f127806f777fca82581c49e127f08413a66056bd9166dd \ - --hash=sha256:725875a63abf7c399d4548e686debb65cdc2549e1825437096a0af1f7e374814 \ - --hash=sha256:7492967c3386df69f80cf67efd665c0f667cee67032090fe01d7d74b0e19bb08 \ - --hash=sha256:81965cc20848ab06583506ef54e37cf15c83c7e619df2ad16807c03100745dea \ - --hash=sha256:81c24e0c0fde47a9723c81d5806569cddef103aebbf79dbc9fcbb617153dea30 \ - --hash=sha256:81eedafa609917040d39aa9332e25881a8e7a0862495fcdf2023a9667209deda \ - --hash=sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9 \ - --hash=sha256:8280856dd7c6a68ab3a164b4a4b1c51f7691f6d04af4d4ca23d6ecf2261b7923 \ - --hash=sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df \ - --hash=sha256:8b4af17bda11e907c51d10686eda89049f9ce5669b08fbe71a29747f1e876036 \ - --hash=sha256:90144d3b0c8b139408da50196c5cad2a6909b51b23df1f0538411cd23ffa45d3 \ - --hash=sha256:906e6b0d7d452e9a98e5ab8507c0da791856b2380fdee61b765632bb8698026f \ - --hash=sha256:90c11ceb9a1f482c752a71f203a81858625d8df5746d787a4786bca4ffdf71c6 \ - --hash=sha256:911cc493ebd60de5f285bcae0491a60b4f2a9f0f5c270edd1c4dbaef7a38fc04 \ - --hash=sha256:9a420a91913092d1e20c86a2f5f1fc85c1a8924dbcaf5e0586df8aceb09c9cc2 \ - --hash=sha256:9f8c9fdd15a55d9465e590a402f42082705d66b05afc3ffd2d2eb3c6ba919560 \ - --hash=sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70 \ - --hash=sha256:a373a400f3e9bac95ba2a06372c4fd1412a7cee53c37fc6c05f829bf672b8769 \ - --hash=sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1 \ - --hash=sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6 \ - --hash=sha256:b1f09b6821406ea1f94053f346f28f8215e293344209129a9c0fcc3578598d7b \ - --hash=sha256:b2ac41acfc8d965fb0c464eb8f44995770239668956dc4cdf502d1b1ffe0d747 \ - --hash=sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078 \ - --hash=sha256:b50eab9994d64f4a823ff99a0ed28a6903224ddbe7fef56a6dd865eec9243440 \ - --hash=sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f \ - --hash=sha256:c0b0e5e1b5d9f3586601048dd68f392dc0cc99a59bb5faf18aab057ce00d00b2 \ - --hash=sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d \ - --hash=sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc \ - --hash=sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a \ - --hash=sha256:dd5ec3aa6ae6e4d5b5de9357d2133c07be1aff6405b136dad753a16afb6717dd \ - --hash=sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9 \ - --hash=sha256:ff8e80c4c4932c10493ff97028decfdb622de69cae87e0f127a7ebe32b4069c6 +SQLAlchemy==2.0.43 \ + --hash=sha256:022e436a1cb39b13756cf93b48ecce7aa95382b9cfacceb80a7d263129dfd019 \ + --hash=sha256:03d73ab2a37d9e40dec4984d1813d7878e01dbdc742448d44a7341b7a9f408c7 \ + --hash=sha256:07097c0a1886c150ef2adba2ff7437e84d40c0f7dcb44a2c2b9c905ccfc6361c \ + --hash=sha256:11b9503fa6f8721bef9b8567730f664c5a5153d25e247aadc69247c4bc605227 \ + --hash=sha256:11f43c39b4b2ec755573952bbcc58d976779d482f6f832d7f33a8d869ae891bf \ + --hash=sha256:13194276e69bb2af56198fef7909d48fd34820de01d9c92711a5fa45497cc7ed \ + --hash=sha256:136063a68644eca9339d02e6693932116f6a8591ac013b0014479a1de664e40a \ + --hash=sha256:14111d22c29efad445cd5021a70a8b42f7d9152d8ba7f73304c4d82460946aaa \ + --hash=sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc \ + --hash=sha256:1a113da919c25f7f641ffbd07fbc9077abd4b3b75097c888ab818f962707eb48 \ + --hash=sha256:1c6d85327ca688dbae7e2b06d7d84cfe4f3fffa5b5f9e21bb6ce9d0e1a0e0e0a \ + --hash=sha256:20d81fc2736509d7a2bd33292e489b056cbae543661bb7de7ce9f1c0cd6e7f24 \ + --hash=sha256:21b27b56eb2f82653168cefe6cb8e970cdaf4f3a6cb2c5e3c3c1cf3158968ff9 \ + --hash=sha256:21ba7a08a4253c5825d1db389d4299f64a100ef9800e4624c8bf70d8f136e6ed \ + --hash=sha256:227119ce0a89e762ecd882dc661e0aa677a690c914e358f0dd8932a2e8b2765b \ + --hash=sha256:25b9fc27650ff5a2c9d490c13c14906b918b0de1f8fcbb4c992712d8caf40e83 \ + --hash=sha256:334f41fa28de9f9be4b78445e68530da3c5fa054c907176460c81494f4ae1f5e \ + --hash=sha256:413391b2239db55be14fa4223034d7e13325a1812c8396ecd4f2c08696d5ccad \ + --hash=sha256:4286a1139f14b7d70141c67a8ae1582fc2b69105f1b09d9573494eb4bb4b2687 \ + --hash=sha256:44337823462291f17f994d64282a71c51d738fc9ef561bf265f1d0fd9116a782 \ + --hash=sha256:46293c39252f93ea0910aababa8752ad628bcce3a10d3f260648dd472256983f \ + --hash=sha256:4bf0edb24c128b7be0c61cd17eef432e4bef507013292415f3fb7023f02b7d4b \ + --hash=sha256:4d3d9b904ad4a6b175a2de0738248822f5ac410f52c2fd389ada0b5262d6a1e3 \ + --hash=sha256:4e6aeb2e0932f32950cf56a8b4813cb15ff792fc0c9b3752eaf067cfe298496a \ + --hash=sha256:4fb1a8c5438e0c5ea51afe9c6564f951525795cf432bed0c028c1cb081276685 \ + --hash=sha256:529064085be2f4d8a6e5fab12d36ad44f1909a18848fcfbdb59cc6d4bbe48efe \ + --hash=sha256:52d9b73b8fb3e9da34c2b31e6d99d60f5f99fd8c1225c9dad24aeb74a91e1d29 \ + --hash=sha256:5cda6b51faff2639296e276591808c1726c4a77929cfaa0f514f30a5f6156921 \ + --hash=sha256:5d79f9fdc9584ec83d1b3c75e9f4595c49017f5594fee1a2217117647225d738 \ + --hash=sha256:61f964a05356f4bca4112e6334ed7c208174511bd56e6b8fc86dad4d024d4185 \ + --hash=sha256:6772e3ca8a43a65a37c88e2f3e2adfd511b0b1da37ef11ed78dea16aeae85bd9 \ + --hash=sha256:6e2bf13d9256398d037fef09fd8bf9b0bf77876e22647d10761d35593b9ac547 \ + --hash=sha256:70322986c0c699dca241418fcf18e637a4369e0ec50540a2b907b184c8bca069 \ + --hash=sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417 \ + --hash=sha256:7f1ac7828857fcedb0361b48b9ac4821469f7694089d15550bbcf9ab22564a1d \ + --hash=sha256:87accdbba88f33efa7b592dc2e8b2a9c2cdbca73db2f9d5c510790428c09c154 \ + --hash=sha256:8cee08f15d9e238ede42e9bbc1d6e7158d0ca4f176e4eab21f88ac819ae3bd7b \ + --hash=sha256:971ba928fcde01869361f504fcff3b7143b47d30de188b11c6357c0505824197 \ + --hash=sha256:9c2e02f06c68092b875d5cbe4824238ab93a7fa35d9c38052c033f7ca45daa18 \ + --hash=sha256:9c5a9da957c56e43d72126a3f5845603da00e0293720b03bde0aacffcf2dc04f \ + --hash=sha256:9df7126fd9db49e3a5a3999442cc67e9ee8971f3cb9644250107d7296cb2a164 \ + --hash=sha256:b3edaec7e8b6dc5cd94523c6df4f294014df67097c8217a89929c99975811414 \ + --hash=sha256:b535d35dea8bbb8195e7e2b40059e2253acb2b7579b73c1b432a35363694641d \ + --hash=sha256:bcf0724a62a5670e5718957e05c56ec2d6850267ea859f8ad2481838f889b42c \ + --hash=sha256:c00e7845d2f692ebfc7d5e4ec1a3fd87698e4337d09e58d6749a16aedfdf8612 \ + --hash=sha256:c379e37b08c6c527181a397212346be39319fb64323741d23e46abd97a400d34 \ + --hash=sha256:c5d1730b25d9a07727d20ad74bc1039bbbb0a6ca24e6769861c1aa5bf2c4c4a8 \ + --hash=sha256:c5e73ba0d76eefc82ec0219d2301cb33bfe5205ed7a2602523111e2e56ccbd20 \ + --hash=sha256:c697575d0e2b0a5f0433f679bda22f63873821d991e95a90e9e52aae517b2e32 \ + --hash=sha256:cdeff998cb294896a34e5b2f00e383e7c5c4ef3b4bfa375d9104723f15186443 \ + --hash=sha256:ceb5c832cc30663aeaf5e39657712f4c4241ad1f638d487ef7216258f6d41fe7 \ + --hash=sha256:d34c0f6dbefd2e816e8f341d0df7d4763d382e3f452423e752ffd1e213da2512 \ + --hash=sha256:db691fa174e8f7036afefe3061bc40ac2b770718be2862bfb03aabae09051aca \ + --hash=sha256:e7a903b5b45b0d9fa03ac6a331e1c1d6b7e0ab41c63b6217b3d10357b83c8b00 \ + --hash=sha256:e7c08f57f75a2bb62d7ee80a89686a5e5669f199235c6d1dac75cd59374091c3 \ + --hash=sha256:f42f23e152e4545157fa367b2435a1ace7571cab016ca26038867eb7df2c3631 \ + --hash=sha256:fe2b3b4927d0bc03d02ad883f402d5de201dbc8894ac87d2e981e7d87430e60d # via # -r requirements.in # alembic @@ -542,12 +527,14 @@ tomli==2.2.1 \ --hash=sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a \ --hash=sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7 # via -r requirements.in -typing-extensions==4.14.0 \ - --hash=sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4 \ - --hash=sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af +typing-extensions==4.14.1 \ + --hash=sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36 \ + --hash=sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76 # via # alembic + # opentelemetry-api # opentelemetry-sdk + # opentelemetry-semantic-conventions # sqlalchemy urllib3==2.5.0 \ --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ @@ -557,89 +544,89 @@ wheel==0.45.1 \ --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \ --hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248 # via pip-tools -wrapt==1.17.2 \ - --hash=sha256:08e7ce672e35efa54c5024936e559469436f8b8096253404faeb54d2a878416f \ - --hash=sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c \ - --hash=sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a \ - --hash=sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b \ - --hash=sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555 \ - --hash=sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c \ - --hash=sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b \ - --hash=sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6 \ - --hash=sha256:1e1fe0e6ab7775fd842bc39e86f6dcfc4507ab0ffe206093e76d61cde37225c8 \ - --hash=sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662 \ - --hash=sha256:2696993ee1eebd20b8e4ee4356483c4cb696066ddc24bd70bcbb80fa56ff9061 \ - --hash=sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998 \ - --hash=sha256:36ccae62f64235cf8ddb682073a60519426fdd4725524ae38874adf72b5f2aeb \ - --hash=sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62 \ - --hash=sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984 \ - --hash=sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392 \ - --hash=sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2 \ - --hash=sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306 \ - --hash=sha256:410a92fefd2e0e10d26210e1dfb4a876ddaf8439ef60d6434f21ef8d87efc5b7 \ - --hash=sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3 \ - --hash=sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9 \ - --hash=sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6 \ - --hash=sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192 \ - --hash=sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317 \ - --hash=sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f \ - --hash=sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda \ - --hash=sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563 \ - --hash=sha256:58455b79ec2661c3600e65c0a716955adc2410f7383755d537584b0de41b1d8a \ - --hash=sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f \ - --hash=sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d \ - --hash=sha256:5c803c401ea1c1c18de70a06a6f79fcc9c5acfc79133e9869e730ad7f8ad8ef9 \ - --hash=sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8 \ - --hash=sha256:612dff5db80beef9e649c6d803a8d50c409082f1fedc9dbcdfde2983b2025b82 \ - --hash=sha256:62c2caa1585c82b3f7a7ab56afef7b3602021d6da34fbc1cf234ff139fed3cd9 \ - --hash=sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845 \ - --hash=sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82 \ - --hash=sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125 \ - --hash=sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504 \ - --hash=sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b \ - --hash=sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7 \ - --hash=sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc \ - --hash=sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6 \ - --hash=sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40 \ - --hash=sha256:91bd7d1773e64019f9288b7a5101f3ae50d3d8e6b1de7edee9c2ccc1d32f0c0a \ - --hash=sha256:95c658736ec15602da0ed73f312d410117723914a5c91a14ee4cdd72f1d790b3 \ - --hash=sha256:99039fa9e6306880572915728d7f6c24a86ec57b0a83f6b2491e1d8ab0235b9a \ - --hash=sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72 \ - --hash=sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681 \ - --hash=sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438 \ - --hash=sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae \ - --hash=sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2 \ - --hash=sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb \ - --hash=sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5 \ - --hash=sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a \ - --hash=sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3 \ - --hash=sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8 \ - --hash=sha256:b4e42a40a5e164cbfdb7b386c966a588b1047558a990981ace551ed7e12ca9c2 \ - --hash=sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22 \ - --hash=sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72 \ - --hash=sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061 \ - --hash=sha256:ba0f0eb61ef00ea10e00eb53a9129501f52385c44853dbd6c4ad3f403603083f \ - --hash=sha256:bb87745b2e6dc56361bfde481d5a378dc314b252a98d7dd19a651a3fa58f24a9 \ - --hash=sha256:bb90fb8bda722a1b9d48ac1e6c38f923ea757b3baf8ebd0c82e09c5c1a0e7a04 \ - --hash=sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98 \ - --hash=sha256:c86563182421896d73858e08e1db93afdd2b947a70064b813d515d66549e15f9 \ - --hash=sha256:c958bcfd59bacc2d0249dcfe575e71da54f9dcf4a8bdf89c4cb9a68a1170d73f \ - --hash=sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b \ - --hash=sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925 \ - --hash=sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6 \ - --hash=sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0 \ - --hash=sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9 \ - --hash=sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c \ - --hash=sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991 \ - --hash=sha256:ecc840861360ba9d176d413a5489b9a0aff6d6303d7e733e2c4623cfa26904a6 \ - --hash=sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000 \ - --hash=sha256:f393cda562f79828f38a819f4788641ac7c4085f30f1ce1a68672baa686482bb \ - --hash=sha256:f917c1180fdb8623c2b75a99192f4025e412597c50b2ac870f156de8fb101119 \ - --hash=sha256:fc78a84e2dfbc27afe4b2bd7c80c8db9bca75cc5b85df52bfe634596a1da846b \ - --hash=sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58 - # via - # deprecated - # opentelemetry-instrumentation +wrapt==1.17.3 \ + --hash=sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56 \ + --hash=sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828 \ + --hash=sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f \ + --hash=sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396 \ + --hash=sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77 \ + --hash=sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d \ + --hash=sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139 \ + --hash=sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7 \ + --hash=sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb \ + --hash=sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f \ + --hash=sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f \ + --hash=sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067 \ + --hash=sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f \ + --hash=sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7 \ + --hash=sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b \ + --hash=sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc \ + --hash=sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05 \ + --hash=sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd \ + --hash=sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7 \ + --hash=sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9 \ + --hash=sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81 \ + --hash=sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977 \ + --hash=sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa \ + --hash=sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b \ + --hash=sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe \ + --hash=sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58 \ + --hash=sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8 \ + --hash=sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77 \ + --hash=sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85 \ + --hash=sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c \ + --hash=sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df \ + --hash=sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454 \ + --hash=sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a \ + --hash=sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e \ + --hash=sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c \ + --hash=sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6 \ + --hash=sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5 \ + --hash=sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9 \ + --hash=sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd \ + --hash=sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277 \ + --hash=sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225 \ + --hash=sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22 \ + --hash=sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116 \ + --hash=sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16 \ + --hash=sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc \ + --hash=sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00 \ + --hash=sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2 \ + --hash=sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a \ + --hash=sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804 \ + --hash=sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04 \ + --hash=sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1 \ + --hash=sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba \ + --hash=sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390 \ + --hash=sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0 \ + --hash=sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d \ + --hash=sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22 \ + --hash=sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0 \ + --hash=sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2 \ + --hash=sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18 \ + --hash=sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6 \ + --hash=sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311 \ + --hash=sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89 \ + --hash=sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f \ + --hash=sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39 \ + --hash=sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4 \ + --hash=sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5 \ + --hash=sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa \ + --hash=sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a \ + --hash=sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050 \ + --hash=sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6 \ + --hash=sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235 \ + --hash=sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056 \ + --hash=sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2 \ + --hash=sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418 \ + --hash=sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c \ + --hash=sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a \ + --hash=sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6 \ + --hash=sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0 \ + --hash=sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775 \ + --hash=sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10 \ + --hash=sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c + # via opentelemetry-instrumentation zipp==3.23.0 \ --hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \ --hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166 diff --git a/samples/informational_fk_sample.py b/samples/informational_fk_sample.py new file mode 100644 index 00000000..4e330dae --- /dev/null +++ b/samples/informational_fk_sample.py @@ -0,0 +1,92 @@ +# Copyright 2025 Google LLC All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import datetime +import uuid + +from sqlalchemy import create_engine +from sqlalchemy.orm import Session + +from sample_helper import run_sample +from model import Singer, Concert, Venue, TicketSale + + +# Shows how to create a non-enforced foreign key. +# +# The TicketSale model contains two foreign keys that are not enforced by Spanner. +# This allows the related records to be deleted without the need to delete the +# corresponding TicketSale record. +# +# __table_args__ = ( +# ForeignKeyConstraint( +# ["venue_code", "start_time", "singer_id"], +# ["concerts.venue_code", "concerts.start_time", "concerts.singer_id"], +# spanner_not_enforced=True, +# ), +# ) +# singer_id: Mapped[str] = mapped_column(String(36), ForeignKey("singers.id", spanner_not_enforced=True)) +# +# See https://cloud.google.com/spanner/docs/foreign-keys/overview#informational-foreign-keys +# for more information on informational foreign key constrains. +def informational_fk_sample(): + engine = create_engine( + "spanner:///projects/sample-project/" + "instances/sample-instance/" + "databases/sample-database", + echo=True, + ) + # First create a singer, venue, concert and ticket_sale. + singer_id = str(uuid.uuid4()) + ticket_sale_id = None + with Session(engine) as session: + singer = Singer(id=singer_id, first_name="John", last_name="Doe") + venue = Venue(code="CH", name="Concert Hall", active=True) + concert = Concert( + venue=venue, + start_time=datetime.datetime(2024, 11, 7, 19, 30, 0), + singer=singer, + title="John Doe - Live in Concert Hall", + ) + ticket_sale = TicketSale( + concert=concert, customer_name="Alice Doe", seats=["A010", "A011", "A012"] + ) + session.add_all([singer, venue, concert, ticket_sale]) + session.commit() + ticket_sale_id = ticket_sale.id + + # Now delete both the singer and concert that are referenced by the ticket_sale record. + # This is possible as the foreign key constraints between ticket_sales and singers/concerts + # are not enforced. + with Session(engine) as session: + session.delete(concert) + session.delete(singer) + session.commit() + + # Verify that the ticket_sale record still exists, while the concert and singer have been + # deleted. + with Session(engine) as session: + ticket_sale = session.get(TicketSale, ticket_sale_id) + singer = session.get(Singer, singer_id) + concert = session.get( + Concert, ("CH", datetime.datetime(2024, 11, 7, 19, 30, 0), singer_id) + ) + print( + "Ticket sale found: {}\nSinger found: {}\nConcert found: {}\n".format( + ticket_sale is not None, singer is not None, concert is not None + ) + ) + + +if __name__ == "__main__": + run_sample(informational_fk_sample) diff --git a/samples/insertmany_sample.py b/samples/insertmany_sample.py new file mode 100644 index 00000000..859bc158 --- /dev/null +++ b/samples/insertmany_sample.py @@ -0,0 +1,84 @@ +# Copyright 2025 Google LLC All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from datetime import datetime +import uuid +from sqlalchemy import text, String, create_engine +from sqlalchemy.orm import DeclarativeBase, Session +from sqlalchemy.orm import Mapped +from sqlalchemy.orm import mapped_column +from sample_helper import run_sample + + +class Base(DeclarativeBase): + pass + + +# To use SQLAlchemy 2.0's insertmany feature, models must have a +# unique column marked as an "insert_sentinal" with client-side +# generated values passed into it. This allows SQLAlchemy to perform a +# single bulk insert, even if the table has columns with server-side +# defaults which must be retrieved from a THEN RETURN clause, for +# operations like: +# +# with Session.begin() as session: +# session.add(Singer(name="a")) +# session.add(Singer(name="b")) +# +# Read more in the SQLAlchemy documentation of this feature: +# https://docs.sqlalchemy.org/en/20/core/connections.html#configuring-sentinel-columns + + +class Singer(Base): + __tablename__ = "singers_with_sentinel" + id: Mapped[str] = mapped_column( + String(36), + primary_key=True, + # Supply a unique UUID client-side + default=lambda: str(uuid.uuid4()), + # The column is unique and can be used as an insert_sentinel + insert_sentinel=True, + # Set a server-side default for write outside SQLAlchemy + server_default=text("GENERATE_UUID()"), + ) + name: Mapped[str] + inserted_at: Mapped[datetime] = mapped_column( + server_default=text("CURRENT_TIMESTAMP()") + ) + + +# Shows how to insert data using SQLAlchemy, including relationships that are +# defined both as foreign keys and as interleaved tables. +def insertmany(): + engine = create_engine( + "spanner:///projects/sample-project/" + "instances/sample-instance/" + "databases/sample-database", + echo=True, + ) + # Create the sample table. + Base.metadata.create_all(engine) + + # Insert two singers in one session. These two singers will be inserted using + # a single INSERT statement with a THEN RETURN clause to return the generated + # creation timestamp. + with Session(engine) as session: + session.add(Singer(name="John Smith")) + session.add(Singer(name="Jane Smith")) + session.commit() + + +if __name__ == "__main__": + run_sample(insertmany) diff --git a/samples/model.py b/samples/model.py index c7c68301..faa8a535 100644 --- a/samples/model.py +++ b/samples/model.py @@ -154,7 +154,9 @@ class Concert(Base): title: Mapped[str] = mapped_column(String(200), nullable=False) singer: Mapped["Singer"] = relationship(back_populates="concerts") venue: Mapped["Venue"] = relationship(back_populates="concerts") - ticket_sales: Mapped[List["TicketSale"]] = relationship(back_populates="concert") + ticket_sales: Mapped[List["TicketSale"]] = relationship( + back_populates="concert", passive_deletes=True + ) class TicketSale(Base): @@ -163,6 +165,7 @@ class TicketSale(Base): ForeignKeyConstraint( ["venue_code", "start_time", "singer_id"], ["concerts.venue_code", "concerts.start_time", "concerts.singer_id"], + spanner_not_enforced=True, ), ) id: Mapped[int] = mapped_column( @@ -178,7 +181,12 @@ class TicketSale(Base): start_time: Mapped[Optional[datetime.datetime]] = mapped_column( DateTime, nullable=False ) - singer_id: Mapped[str] = mapped_column(String(36), ForeignKey("singers.id")) + # Create an informational foreign key that is not enforced by Spanner. + # See https://cloud.google.com/spanner/docs/foreign-keys/overview#informational-foreign-keys + # for more information. + singer_id: Mapped[str] = mapped_column( + String(36), ForeignKey("singers.id", spanner_not_enforced=True) + ) # Create a commit timestamp column and set a client-side default of # PENDING_COMMIT_TIMESTAMP() An event handler below is responsible for # setting PENDING_COMMIT_TIMESTAMP() on updates. If using SQLAlchemy diff --git a/samples/noxfile.py b/samples/noxfile.py index 8c95f052..cd28a3f0 100644 --- a/samples/noxfile.py +++ b/samples/noxfile.py @@ -87,6 +87,16 @@ def database_role(session): _sample(session) +@nox.session() +def informational_fk(session): + _sample(session) + + +@nox.session() +def insertmany(session): + _sample(session) + + @nox.session() def _all_samples(session): _sample(session) diff --git a/samples/sample_helper.py b/samples/sample_helper.py index 862d535d..f10268b2 100644 --- a/samples/sample_helper.py +++ b/samples/sample_helper.py @@ -16,8 +16,10 @@ from typing import Callable from google.api_core.client_options import ClientOptions +from google.api_core.exceptions import AlreadyExists from google.auth.credentials import AnonymousCredentials from google.cloud.spanner_v1 import Client +from google.cloud.spanner_v1.database import Database from sqlalchemy import create_engine from sqlalchemy.dialects import registry from testcontainers.core.container import DockerContainer @@ -32,9 +34,15 @@ def run_sample(sample_method: Callable): "google.cloud.sqlalchemy_spanner.sqlalchemy_spanner", "SpannerDialect", ) - os.environ["SPANNER_EMULATOR_HOST"] = "" - emulator, port = start_emulator() - os.environ["SPANNER_EMULATOR_HOST"] = "localhost:" + str(port) + emulator = None + if os.getenv("USE_EXISTING_EMULATOR") == "true": + if os.getenv("SPANNER_EMULATOR_HOST") is None: + os.environ["SPANNER_EMULATOR_HOST"] = "localhost:9010" + _create_instance_and_database("9010") + else: + os.environ["SPANNER_EMULATOR_HOST"] = "" + emulator, port = start_emulator() + os.environ["SPANNER_EMULATOR_HOST"] = "localhost:" + str(port) try: _create_tables() sample_method() @@ -49,7 +57,7 @@ def start_emulator() -> (DockerContainer, str): ).with_exposed_ports(9010) emulator.start() wait_for_logs(emulator, "gRPC server listening at 0.0.0.0:9010") - port = emulator.get_exposed_port(9010) + port = str(emulator.get_exposed_port(9010)) _create_instance_and_database(port) return emulator, port @@ -68,10 +76,16 @@ def _create_instance_and_database(port: str): database_id = "sample-database" instance = client.instance(instance_id, instance_config) - created_op = instance.create() - created_op.result(1800) # block until completion + try: + created_op = instance.create() + created_op.result(1800) # block until completion + except AlreadyExists: + # Ignore + print("Using existing instance") - database = instance.database(database_id) + database: Database = instance.database(database_id) + if database.exists(): + database.drop() created_op = database.create() created_op.result(1800) diff --git a/setup.py b/setup.py index 2c039fe7..67cd184b 100644 --- a/setup.py +++ b/setup.py @@ -58,7 +58,11 @@ setuptools.setup( author="Google LLC", author_email="googleapis-packages@google.com", - classifiers=["Intended Audience :: Developers"], + license="Apache 2.0", + classifiers=[ + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + ], description=description, long_description=readme, entry_points={ diff --git a/test/mockserver_tests/default_model.py b/test/mockserver_tests/default_model.py new file mode 100644 index 00000000..6a363c57 --- /dev/null +++ b/test/mockserver_tests/default_model.py @@ -0,0 +1,30 @@ +# Copyright 2025 Google LLC All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from sqlalchemy import func +from sqlalchemy.orm import DeclarativeBase +from sqlalchemy.orm import Mapped +from sqlalchemy.orm import mapped_column + + +class Base(DeclarativeBase): + pass + + +class Singer(Base): + __tablename__ = "singers" + id: Mapped[str] = mapped_column( + server_default=func.GENERATE_UUID(), primary_key=True + ) + name: Mapped[str] diff --git a/test/mockserver_tests/insertmany_model.py b/test/mockserver_tests/insertmany_model.py new file mode 100644 index 00000000..a196e142 --- /dev/null +++ b/test/mockserver_tests/insertmany_model.py @@ -0,0 +1,48 @@ +# Copyright 2025 Google LLC All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import datetime +import uuid +from sqlalchemy import text, String +from sqlalchemy.orm import DeclarativeBase +from sqlalchemy.orm import Mapped +from sqlalchemy.orm import mapped_column + + +class Base(DeclarativeBase): + pass + + +class SingerUUID(Base): + __tablename__ = "singers_uuid" + id: Mapped[str] = mapped_column( + String(36), + primary_key=True, + server_default=text("GENERATE_UUID()"), + default=lambda: str(uuid.uuid4()), + insert_sentinel=True, + ) + name: Mapped[str] + inserted_at: Mapped[datetime] = mapped_column( + server_default=text("CURRENT_TIMESTAMP()") + ) + + +class SingerIntID(Base): + __tablename__ = "singers_int_id" + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String) + inserted_at: Mapped[datetime] = mapped_column( + server_default=text("CURRENT_TIMESTAMP()") + ) diff --git a/test/mockserver_tests/mock_server_test_base.py b/test/mockserver_tests/mock_server_test_base.py index a8fea819..6cdf9733 100644 --- a/test/mockserver_tests/mock_server_test_base.py +++ b/test/mockserver_tests/mock_server_test_base.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import logging from google.cloud.spanner_dbapi.parsed_statement import AutocommitDmlMode from sqlalchemy import Engine, create_engine @@ -22,7 +23,6 @@ from google.cloud.spanner_v1 import ( Client, ResultSet, - PingingPool, TypeCode, ) from google.cloud.spanner_v1.database import Database @@ -131,9 +131,12 @@ class MockServerTestBase(fixtures.TestBase): spanner_service: SpannerServicer = None database_admin_service: DatabaseAdminServicer = None port: int = None + logger: logging.Logger = None @classmethod def setup_class(cls): + MockServerTestBase.logger = logging.getLogger("level warning") + MockServerTestBase.logger.setLevel(logging.WARN) ( MockServerTestBase.server, MockServerTestBase.spanner_service, @@ -151,6 +154,7 @@ def setup_method(self): self._client = None self._instance = None self._database = None + _ = self.database def teardown_method(self): MockServerTestBase.spanner_service.clear_requests() @@ -159,7 +163,7 @@ def teardown_method(self): def create_engine(self) -> Engine: return create_engine( "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": PingingPool(size=10)}, + connect_args={"client": self.client, "logger": MockServerTestBase.logger}, ) @property @@ -177,13 +181,13 @@ def client(self) -> Client: @property def instance(self) -> Instance: if self._instance is None: - self._instance = self.client.instance("test-instance") + self._instance = self.client.instance("i") return self._instance @property def database(self) -> Database: + logger = logging.getLogger("level warning") + logger.setLevel(logging.WARN) if self._database is None: - self._database = self.instance.database( - "test-database", pool=PingingPool(size=10) - ) + self._database = self.instance.database("d", logger=logger) return self._database diff --git a/test/mockserver_tests/not_enforced_fk_model.py b/test/mockserver_tests/not_enforced_fk_model.py new file mode 100644 index 00000000..36965f01 --- /dev/null +++ b/test/mockserver_tests/not_enforced_fk_model.py @@ -0,0 +1,37 @@ +# Copyright 2025 Google LLC All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from sqlalchemy import ForeignKey +from sqlalchemy.orm import DeclarativeBase +from sqlalchemy.orm import Mapped +from sqlalchemy.orm import mapped_column + + +class Base(DeclarativeBase): + pass + + +class Singer(Base): + __tablename__ = "singers" + id: Mapped[str] = mapped_column(primary_key=True) + name: Mapped[str] + + +class Album(Base): + __tablename__ = "albums" + id: Mapped[str] = mapped_column(primary_key=True) + name: Mapped[str] + singer_id: Mapped[str] = mapped_column( + ForeignKey("singers.id", spanner_not_enforced=True) + ) diff --git a/test/mockserver_tests/test_auto_increment.py b/test/mockserver_tests/test_auto_increment.py index 7fa245e8..9e8051db 100644 --- a/test/mockserver_tests/test_auto_increment.py +++ b/test/mockserver_tests/test_auto_increment.py @@ -12,13 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from sqlalchemy import create_engine from sqlalchemy.orm import Session from sqlalchemy.testing import eq_, is_instance_of from google.cloud.spanner_v1 import ( - FixedSizePool, ResultSet, - BatchCreateSessionsRequest, + CreateSessionRequest, ExecuteSqlRequest, CommitRequest, BeginTransactionRequest, @@ -45,10 +43,7 @@ def test_create_table(self): """, ResultSet(), ) - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() Base.metadata.create_all(engine) requests = self.database_admin_service.requests eq_(1, len(requests)) @@ -74,10 +69,7 @@ def test_create_auto_increment_table(self): """, ResultSet(), ) - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() engine.dialect.use_auto_increment = True Base.metadata.create_all(engine) requests = self.database_admin_service.requests @@ -103,10 +95,7 @@ def test_create_table_with_specific_sequence_kind(self): """, ResultSet(), ) - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() engine.dialect.default_sequence_kind = "non_existing_kind" Base.metadata.create_all(engine) requests = self.database_admin_service.requests @@ -126,10 +115,7 @@ def test_insert_row(self): from test.mockserver_tests.auto_increment_model import Singer self.add_insert_result("INSERT INTO singers (name) VALUES (@a0) THEN RETURN id") - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() with Session(engine) as session: singer = Singer(name="Test") @@ -141,7 +127,7 @@ def test_insert_row(self): # Verify the requests that we got. requests = self.spanner_service.requests eq_(4, len(requests)) - is_instance_of(requests[0], BatchCreateSessionsRequest) + is_instance_of(requests[0], CreateSessionRequest) is_instance_of(requests[1], BeginTransactionRequest) is_instance_of(requests[2], ExecuteSqlRequest) is_instance_of(requests[3], CommitRequest) @@ -152,10 +138,7 @@ def test_insert_row_with_pk_value(self): # SQLAlchemy should not use a THEN RETURN clause when a value for the # primary key has been set on the model. add_update_count("INSERT INTO singers (id, name) VALUES (@a0, @a1)", 1) - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() with Session(engine) as session: # Manually specify a value for the primary key. diff --git a/test/mockserver_tests/test_basics.py b/test/mockserver_tests/test_basics.py index 7d40e874..3e6c2fe0 100644 --- a/test/mockserver_tests/test_basics.py +++ b/test/mockserver_tests/test_basics.py @@ -13,6 +13,7 @@ # limitations under the License. import datetime + from google.cloud.spanner_admin_database_v1 import UpdateDatabaseDdlRequest from google.cloud.spanner_dbapi.parsed_statement import AutocommitDmlMode from sqlalchemy import ( @@ -32,11 +33,9 @@ from sqlalchemy.orm import Session, DeclarativeBase, Mapped, mapped_column from sqlalchemy.testing import eq_, is_instance_of from google.cloud.spanner_v1 import ( - FixedSizePool, - BatchCreateSessionsRequest, + CreateSessionRequest, ExecuteSqlRequest, ResultSet, - PingingPool, TypeCode, ) from test.mockserver_tests.mock_server_test_base import ( @@ -58,7 +57,7 @@ def verify_select1(self, results): eq_(1, len(result_list)) requests = self.spanner_service.requests eq_(2, len(requests)) - is_instance_of(requests[0], BatchCreateSessionsRequest) + is_instance_of(requests[0], CreateSessionRequest) is_instance_of(requests[1], ExecuteSqlRequest) def test_select1(self): @@ -69,10 +68,7 @@ def test_select1(self): def test_sqlalchemy_select1(self): add_select1_result() - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": PingingPool(size=10)}, - ) + engine = self.create_engine() with engine.connect().execution_options( isolation_level="AUTOCOMMIT" ) as connection: @@ -92,10 +88,7 @@ def test_sqlalchemy_select_now(self): TypeCode.TIMESTAMP, [(iso_now,)], ) - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": PingingPool(size=10)}, - ) + engine = self.create_engine() with engine.connect().execution_options( isolation_level="AUTOCOMMIT" ) as connection: @@ -111,10 +104,7 @@ def test_create_table(self): """, ResultSet(), ) - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() metadata = MetaData() Table( "users", @@ -148,10 +138,7 @@ def test_create_table_in_schema(self): """, ResultSet(), ) - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() metadata = MetaData() Table( "users", @@ -190,10 +177,7 @@ def test_create_multiple_tables(self): """, ResultSet(), ) - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() metadata = MetaData() for i in range(2): Table( @@ -224,7 +208,7 @@ def test_partitioned_dml(self): "spanner:///projects/p/instances/i/databases/d", connect_args={ "client": self.client, - "pool": PingingPool(size=10), + "logger": MockServerTestBase.logger, "ignore_transaction_warnings": True, }, ) @@ -258,10 +242,7 @@ class Singer(Base): update = "UPDATE singers SET name=@a0 WHERE singers.id = @a1" add_update_count(update, 1) - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() with Session(engine) as session: singer = ( @@ -277,7 +258,7 @@ def test_database_role(self): "spanner:///projects/p/instances/i/databases/d", connect_args={ "client": self.client, - "pool": FixedSizePool(size=10), + "logger": MockServerTestBase.logger, "database_role": "my_role", }, ) @@ -285,10 +266,10 @@ def test_database_role(self): session.execute(select(1)) requests = self.spanner_service.requests eq_(2, len(requests)) - is_instance_of(requests[0], BatchCreateSessionsRequest) + is_instance_of(requests[0], CreateSessionRequest) is_instance_of(requests[1], ExecuteSqlRequest) - request: BatchCreateSessionsRequest = requests[0] - eq_("my_role", request.session_template.creator_role) + request: CreateSessionRequest = requests[0] + eq_("my_role", request.session.creator_role) def test_select_table_in_named_schema(self): class Base(DeclarativeBase): @@ -309,10 +290,7 @@ class Singer(Base): " LIMIT @a1" ) add_singer_query_result(query) - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() insert = "INSERT INTO my_schema.singers (name) VALUES (@a0) THEN RETURN id" add_single_result(insert, "id", TypeCode.INT64, [("1",)]) diff --git a/test/mockserver_tests/test_bit_reversed_sequence.py b/test/mockserver_tests/test_bit_reversed_sequence.py index 9e7a81a8..b54bb367 100644 --- a/test/mockserver_tests/test_bit_reversed_sequence.py +++ b/test/mockserver_tests/test_bit_reversed_sequence.py @@ -12,13 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from sqlalchemy import create_engine from sqlalchemy.orm import Session from sqlalchemy.testing import eq_, is_instance_of from google.cloud.spanner_v1 import ( - FixedSizePool, ResultSet, - BatchCreateSessionsRequest, + CreateSessionRequest, ExecuteSqlRequest, CommitRequest, BeginTransactionRequest, @@ -52,10 +50,7 @@ def test_create_table(self): LIMIT 1""", ResultSet(), ) - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() Base.metadata.create_all(engine) requests = self.database_admin_service.requests eq_(1, len(requests)) @@ -113,10 +108,7 @@ def test_insert_row(self): "THEN RETURN id", result, ) - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() with Session(engine) as session: singer = Singer(name="Test") @@ -128,7 +120,7 @@ def test_insert_row(self): # Verify the requests that we got. requests = self.spanner_service.requests eq_(4, len(requests)) - is_instance_of(requests[0], BatchCreateSessionsRequest) + is_instance_of(requests[0], CreateSessionRequest) is_instance_of(requests[1], BeginTransactionRequest) is_instance_of(requests[2], ExecuteSqlRequest) is_instance_of(requests[3], CommitRequest) diff --git a/test/mockserver_tests/test_commit_timestamp.py b/test/mockserver_tests/test_commit_timestamp.py index 3872bde1..f70fcec6 100644 --- a/test/mockserver_tests/test_commit_timestamp.py +++ b/test/mockserver_tests/test_commit_timestamp.py @@ -12,12 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from sqlalchemy import create_engine from sqlalchemy.testing import eq_, is_instance_of -from google.cloud.spanner_v1 import ( - FixedSizePool, - ResultSet, -) +from google.cloud.spanner_v1 import ResultSet from test.mockserver_tests.mock_server_test_base import ( MockServerTestBase, add_result, @@ -45,10 +41,7 @@ def test_create_table(self): LIMIT 1""", ResultSet(), ) - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() Base.metadata.create_all(engine) requests = self.database_admin_service.requests eq_(1, len(requests)) diff --git a/test/mockserver_tests/test_default.py b/test/mockserver_tests/test_default.py new file mode 100644 index 00000000..9b46ede0 --- /dev/null +++ b/test/mockserver_tests/test_default.py @@ -0,0 +1,49 @@ +# Copyright 2025 Google LLC All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from sqlalchemy import create_engine +from sqlalchemy.testing import eq_, is_instance_of +from google.cloud.spanner_v1 import FixedSizePool, ResultSet +from test.mockserver_tests.mock_server_test_base import MockServerTestBase, add_result +from google.cloud.spanner_admin_database_v1 import UpdateDatabaseDdlRequest + + +class TestCreateTableDefault(MockServerTestBase): + def test_create_table_with_default(self): + from test.mockserver_tests.default_model import Base + + add_result( + """SELECT true +FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA="" AND TABLE_NAME="singers" +LIMIT 1 +""", + ResultSet(), + ) + engine = create_engine( + "spanner:///projects/p/instances/i/databases/d", + connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, + ) + Base.metadata.create_all(engine) + requests = self.database_admin_service.requests + eq_(1, len(requests)) + is_instance_of(requests[0], UpdateDatabaseDdlRequest) + eq_(1, len(requests[0].statements)) + eq_( + "CREATE TABLE singers (\n" + "\tid STRING(MAX) NOT NULL DEFAULT (GENERATE_UUID()), \n" + "\tname STRING(MAX) NOT NULL\n" + ") PRIMARY KEY (id)", + requests[0].statements[0], + ) diff --git a/test/mockserver_tests/test_float32.py b/test/mockserver_tests/test_float32.py index 801a57c2..50cd6a86 100644 --- a/test/mockserver_tests/test_float32.py +++ b/test/mockserver_tests/test_float32.py @@ -19,7 +19,7 @@ is_false, ) from google.cloud.spanner_v1 import ( - BatchCreateSessionsRequest, + CreateSessionRequest, ExecuteSqlRequest, ResultSet, ResultSetStats, @@ -59,7 +59,7 @@ def test_insert_data(self): requests = self.spanner_service.requests eq_(4, len(requests)) - is_instance_of(requests[0], BatchCreateSessionsRequest) + is_instance_of(requests[0], CreateSessionRequest) is_instance_of(requests[1], BeginTransactionRequest) is_instance_of(requests[2], ExecuteSqlRequest) is_instance_of(requests[3], CommitRequest) diff --git a/test/mockserver_tests/test_insertmany.py b/test/mockserver_tests/test_insertmany.py new file mode 100644 index 00000000..f5b9f882 --- /dev/null +++ b/test/mockserver_tests/test_insertmany.py @@ -0,0 +1,191 @@ +# Copyright 2025 Google LLC All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import uuid +from unittest import mock + +import sqlalchemy +from sqlalchemy.orm import Session +from sqlalchemy.testing import eq_, is_instance_of +from google.cloud.spanner_v1 import ( + ExecuteSqlRequest, + CommitRequest, + RollbackRequest, + BeginTransactionRequest, + CreateSessionRequest, +) +from test.mockserver_tests.mock_server_test_base import ( + MockServerTestBase, + add_result, +) +import google.cloud.spanner_v1.types.type as spanner_type +import google.cloud.spanner_v1.types.result_set as result_set + + +class TestInsertmany(MockServerTestBase): + @mock.patch.object(uuid, "uuid4", mock.MagicMock(side_effect=["a", "b"])) + def test_insertmany_with_uuid_sentinels(self): + """Ensures one bulk insert for ORM objects distinguished by uuid.""" + from test.mockserver_tests.insertmany_model import SingerUUID + + self.add_uuid_insert_result( + "INSERT INTO singers_uuid (id, name) " + "VALUES (@a0, @a1), (@a2, @a3) " + "THEN RETURN inserted_at, id" + ) + engine = self.create_engine() + + with Session(engine) as session: + session.add(SingerUUID(name="a")) + session.add(SingerUUID(name="b")) + session.commit() + + # Verify the requests that we got. + requests = self.spanner_service.requests + eq_(4, len(requests)) + is_instance_of(requests[0], CreateSessionRequest) + is_instance_of(requests[1], BeginTransactionRequest) + is_instance_of(requests[2], ExecuteSqlRequest) + is_instance_of(requests[3], CommitRequest) + + def test_no_insertmany_with_bit_reversed_id(self): + """Ensures we don't try to bulk insert rows with bit-reversed PKs. + + SQLAlchemy's insertmany support requires either incrementing + PKs or client-side supplied sentinel values such as UUIDs. + Spanner's bit-reversed integer PKs don't meet the ordering + requirement, so we need to make sure we don't try to bulk + insert with them. + """ + from test.mockserver_tests.insertmany_model import SingerIntID + + self.add_int_id_insert_result( + "INSERT INTO singers_int_id (name) " + "VALUES (@a0) " + "THEN RETURN id, inserted_at" + ) + engine = self.create_engine() + + with Session(engine) as session: + session.add(SingerIntID(name="a")) + session.add(SingerIntID(name="b")) + try: + session.commit() + except sqlalchemy.exc.SAWarning: + # This will fail because we're returning the same PK + # for two rows. The mock server doesn't currently + # support associating the same query with two + # different results. For our purposes that's okay -- + # we just want to ensure we generate two INSERTs, not + # one. + pass + + # Verify the requests that we got. + requests = self.spanner_service.requests + eq_(5, len(requests)) + is_instance_of(requests[0], CreateSessionRequest) + is_instance_of(requests[1], BeginTransactionRequest) + is_instance_of(requests[2], ExecuteSqlRequest) + is_instance_of(requests[3], ExecuteSqlRequest) + is_instance_of(requests[4], RollbackRequest) + + def add_uuid_insert_result(self, sql): + result = result_set.ResultSet( + dict( + metadata=result_set.ResultSetMetadata( + dict( + row_type=spanner_type.StructType( + dict( + fields=[ + spanner_type.StructType.Field( + dict( + name="inserted_at", + type=spanner_type.Type( + dict( + code=spanner_type.TypeCode.TIMESTAMP + ) + ), + ) + ), + spanner_type.StructType.Field( + dict( + name="id", + type=spanner_type.Type( + dict(code=spanner_type.TypeCode.STRING) + ), + ) + ), + ] + ) + ) + ) + ), + ) + ) + result.rows.extend( + [ + ( + "2020-06-02T23:58:40Z", + "a", + ), + ( + "2020-06-02T23:58:41Z", + "b", + ), + ] + ) + add_result(sql, result) + + def add_int_id_insert_result(self, sql): + result = result_set.ResultSet( + dict( + metadata=result_set.ResultSetMetadata( + dict( + row_type=spanner_type.StructType( + dict( + fields=[ + spanner_type.StructType.Field( + dict( + name="id", + type=spanner_type.Type( + dict(code=spanner_type.TypeCode.INT64) + ), + ) + ), + spanner_type.StructType.Field( + dict( + name="inserted_at", + type=spanner_type.Type( + dict( + code=spanner_type.TypeCode.TIMESTAMP + ) + ), + ) + ), + ] + ) + ) + ) + ), + ) + ) + result.rows.extend( + [ + ( + "1", + "2020-06-02T23:58:40Z", + ), + ] + ) + add_result(sql, result) diff --git a/test/mockserver_tests/test_isolation_level.py b/test/mockserver_tests/test_isolation_level.py index f6545298..21dca305 100644 --- a/test/mockserver_tests/test_isolation_level.py +++ b/test/mockserver_tests/test_isolation_level.py @@ -16,8 +16,7 @@ from sqlalchemy.orm import Session from sqlalchemy.testing import eq_, is_instance_of from google.cloud.spanner_v1 import ( - FixedSizePool, - BatchCreateSessionsRequest, + CreateSessionRequest, ExecuteSqlRequest, CommitRequest, BeginTransactionRequest, @@ -41,10 +40,7 @@ def test_default_isolation_level(self): from test.mockserver_tests.isolation_level_model import Singer self.add_insert_result("INSERT INTO singers (name) VALUES (@a0) THEN RETURN id") - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() with Session(engine) as session: singer = Singer(name="Test") @@ -60,7 +56,7 @@ def test_engine_isolation_level(self): self.add_insert_result("INSERT INTO singers (name) VALUES (@a0) THEN RETURN id") engine = create_engine( "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, + connect_args={"client": self.client, "logger": MockServerTestBase.logger}, isolation_level="REPEATABLE READ", ) @@ -74,10 +70,7 @@ def test_execution_options_isolation_level(self): from test.mockserver_tests.isolation_level_model import Singer self.add_insert_result("INSERT INTO singers (name) VALUES (@a0) THEN RETURN id") - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() with Session( engine.execution_options(isolation_level="repeatable read") @@ -93,7 +86,7 @@ def test_override_engine_isolation_level(self): self.add_insert_result("INSERT INTO singers (name) VALUES (@a0) THEN RETURN id") engine = create_engine( "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, + connect_args={"client": self.client, "logger": MockServerTestBase.logger}, isolation_level="REPEATABLE READ", ) @@ -113,7 +106,7 @@ def test_auto_commit(self): "spanner:///projects/p/instances/i/databases/d", connect_args={ "client": self.client, - "pool": FixedSizePool(size=10), + "logger": MockServerTestBase.logger, "ignore_transaction_warnings": True, }, ) @@ -130,7 +123,7 @@ def test_auto_commit(self): # Verify the requests that we got. requests = self.spanner_service.requests eq_(3, len(requests)) - is_instance_of(requests[0], BatchCreateSessionsRequest) + is_instance_of(requests[0], CreateSessionRequest) is_instance_of(requests[1], ExecuteSqlRequest) is_instance_of(requests[2], CommitRequest) execute_request: ExecuteSqlRequest = requests[1] @@ -147,10 +140,7 @@ def test_auto_commit(self): def test_invalid_isolation_level(self): from test.mockserver_tests.isolation_level_model import Singer - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() with pytest.raises(ValueError): with Session(engine.execution_options(isolation_level="foo")) as session: singer = Singer(name="Test") @@ -161,7 +151,7 @@ def verify_isolation_level(self, level): # Verify the requests that we got. requests = self.spanner_service.requests eq_(4, len(requests)) - is_instance_of(requests[0], BatchCreateSessionsRequest) + is_instance_of(requests[0], CreateSessionRequest) is_instance_of(requests[1], BeginTransactionRequest) is_instance_of(requests[2], ExecuteSqlRequest) is_instance_of(requests[3], CommitRequest) diff --git a/test/mockserver_tests/test_json.py b/test/mockserver_tests/test_json.py index 6395fe3a..2d37a335 100644 --- a/test/mockserver_tests/test_json.py +++ b/test/mockserver_tests/test_json.py @@ -12,13 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from sqlalchemy import create_engine, select +from sqlalchemy import select from sqlalchemy.orm import Session from sqlalchemy.testing import eq_, is_instance_of from google.cloud.spanner_v1 import ( - FixedSizePool, ResultSet, - BatchCreateSessionsRequest, + CreateSessionRequest, ExecuteSqlRequest, CommitRequest, BeginTransactionRequest, @@ -47,10 +46,7 @@ def test_create_table(self): """, ResultSet(), ) - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() Base.metadata.create_all(engine) requests = self.database_admin_service.requests eq_(1, len(requests)) @@ -83,10 +79,7 @@ def _test_insert_json(self, description, expected): add_update_count( "INSERT INTO venues (id, name, description) VALUES (@a0, @a1, @a2)", 1 ) - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() with Session(engine) as session: venue = Venue(id=1, name="Test", description=description) @@ -96,7 +89,7 @@ def _test_insert_json(self, description, expected): # Verify the requests that we got. requests = self.spanner_service.requests eq_(4, len(requests)) - is_instance_of(requests[0], BatchCreateSessionsRequest) + is_instance_of(requests[0], CreateSessionRequest) is_instance_of(requests[1], BeginTransactionRequest) is_instance_of(requests[2], ExecuteSqlRequest) is_instance_of(requests[3], CommitRequest) @@ -126,10 +119,7 @@ def _test_select_json(self, description, expected): sql = "SELECT venues.id, venues.name, venues.description \n" "FROM venues" add_venue_query_result(sql, description) - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() with Session(engine.execution_options(read_only=True)) as session: venue = session.execute(select(Venue)).first()[0] diff --git a/test/mockserver_tests/test_not_enforced_fk.py b/test/mockserver_tests/test_not_enforced_fk.py new file mode 100644 index 00000000..b2253d1b --- /dev/null +++ b/test/mockserver_tests/test_not_enforced_fk.py @@ -0,0 +1,74 @@ +# Copyright 2025 Google LLC All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from sqlalchemy import create_engine +from sqlalchemy.testing import eq_, is_instance_of +from google.cloud.spanner_v1 import ( + FixedSizePool, + ResultSet, +) +from test.mockserver_tests.mock_server_test_base import ( + MockServerTestBase, + add_result, +) +from google.cloud.spanner_admin_database_v1 import UpdateDatabaseDdlRequest + + +class TestNotEnforcedFK(MockServerTestBase): + """Ensure we emit correct DDL for not enforced foreign keys.""" + + def test_create_table(self): + from test.mockserver_tests.not_enforced_fk_model import Base + + add_result( + """SELECT true +FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA="" AND TABLE_NAME="singers" +LIMIT 1 +""", + ResultSet(), + ) + add_result( + """SELECT true +FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA="" AND TABLE_NAME="albums" +LIMIT 1 +""", + ResultSet(), + ) + engine = create_engine( + "spanner:///projects/p/instances/i/databases/d", + connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, + ) + Base.metadata.create_all(engine) + requests = self.database_admin_service.requests + eq_(1, len(requests)) + is_instance_of(requests[0], UpdateDatabaseDdlRequest) + eq_(2, len(requests[0].statements)) + eq_( + "CREATE TABLE singers (\n" + "\tid STRING(MAX) NOT NULL, \n" + "\tname STRING(MAX) NOT NULL\n" + ") PRIMARY KEY (id)", + requests[0].statements[0], + ) + eq_( + "CREATE TABLE albums (\n" + "\tid STRING(MAX) NOT NULL, \n" + "\tname STRING(MAX) NOT NULL, \n" + "\tsinger_id STRING(MAX) NOT NULL, \n" + "\tFOREIGN KEY(singer_id) REFERENCES singers (id) NOT ENFORCED\n" + ") PRIMARY KEY (id)", + requests[0].statements[1], + ) diff --git a/test/mockserver_tests/test_pickle_type.py b/test/mockserver_tests/test_pickle_type.py index b4c2e76c..b2f1a2ab 100644 --- a/test/mockserver_tests/test_pickle_type.py +++ b/test/mockserver_tests/test_pickle_type.py @@ -12,13 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from sqlalchemy import create_engine from sqlalchemy.orm import Session from sqlalchemy.testing import eq_, is_instance_of from google.cloud.spanner_v1 import ( - FixedSizePool, ResultSet, - BatchCreateSessionsRequest, + CreateSessionRequest, ExecuteSqlRequest, CommitRequest, BeginTransactionRequest, @@ -46,10 +44,7 @@ def test_create_table(self): """, ResultSet(), ) - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() Base.metadata.create_all(engine) requests = self.database_admin_service.requests eq_(1, len(requests)) @@ -74,10 +69,7 @@ def test_insert_and_query(self): "VALUES (@a0, @a1, @a2, @a3)", 1, ) - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() preferences = {"setting": "true"} preferences_base64 = "gAWVFQAAAAAAAAB9lIwHc2V0dGluZ5SMBHRydWWUcy4=" with Session(engine) as session: @@ -94,7 +86,7 @@ def test_insert_and_query(self): # Verify the requests that we got. requests = self.spanner_service.requests eq_(4, len(requests)) - is_instance_of(requests[0], BatchCreateSessionsRequest) + is_instance_of(requests[0], CreateSessionRequest) is_instance_of(requests[1], BeginTransactionRequest) is_instance_of(requests[2], ExecuteSqlRequest) is_instance_of(requests[3], CommitRequest) diff --git a/test/mockserver_tests/test_quickstart.py b/test/mockserver_tests/test_quickstart.py index c7db636e..d62f0359 100644 --- a/test/mockserver_tests/test_quickstart.py +++ b/test/mockserver_tests/test_quickstart.py @@ -16,7 +16,7 @@ from google.cloud.spanner_v1 import ( ResultSet, ResultSetStats, - BatchCreateSessionsRequest, + CreateSessionRequest, ExecuteBatchDmlRequest, CommitRequest, BeginTransactionRequest, @@ -116,7 +116,7 @@ def test_insert_data(self): requests = self.spanner_service.requests eq_(5, len(requests)) - is_instance_of(requests[0], BatchCreateSessionsRequest) + is_instance_of(requests[0], CreateSessionRequest) is_instance_of(requests[1], BeginTransactionRequest) is_instance_of(requests[2], ExecuteBatchDmlRequest) is_instance_of(requests[3], ExecuteBatchDmlRequest) diff --git a/test/mockserver_tests/test_read_only_transaction.py b/test/mockserver_tests/test_read_only_transaction.py index 013c0401..0dffbb88 100644 --- a/test/mockserver_tests/test_read_only_transaction.py +++ b/test/mockserver_tests/test_read_only_transaction.py @@ -12,12 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from sqlalchemy import create_engine, select +from sqlalchemy import select from sqlalchemy.orm import Session from sqlalchemy.testing import eq_, is_instance_of from google.cloud.spanner_v1 import ( - FixedSizePool, - BatchCreateSessionsRequest, + CreateSessionRequest, ExecuteSqlRequest, BeginTransactionRequest, TransactionOptions, @@ -33,11 +32,7 @@ def test_read_only_transaction(self): from test.mockserver_tests.read_only_model import Singer add_singer_query_result("SELECT singers.id, singers.name \n" + "FROM singers") - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - echo=True, - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() for i in range(2): with Session(engine.execution_options(read_only=True)) as session: @@ -48,7 +43,7 @@ def test_read_only_transaction(self): # Verify the requests that we got. requests = self.spanner_service.requests eq_(7, len(requests)) - is_instance_of(requests[0], BatchCreateSessionsRequest) + is_instance_of(requests[0], CreateSessionRequest) is_instance_of(requests[1], BeginTransactionRequest) is_instance_of(requests[2], ExecuteSqlRequest) is_instance_of(requests[3], ExecuteSqlRequest) diff --git a/test/mockserver_tests/test_stale_reads.py b/test/mockserver_tests/test_stale_reads.py index d3ac91e8..0dcf8b38 100644 --- a/test/mockserver_tests/test_stale_reads.py +++ b/test/mockserver_tests/test_stale_reads.py @@ -13,12 +13,11 @@ # limitations under the License. import datetime -from sqlalchemy import create_engine, select +from sqlalchemy import select from sqlalchemy.orm import Session from sqlalchemy.testing import eq_, is_instance_of from google.cloud.spanner_v1 import ( - FixedSizePool, - BatchCreateSessionsRequest, + CreateSessionRequest, ExecuteSqlRequest, BeginTransactionRequest, TransactionOptions, @@ -34,11 +33,7 @@ def test_stale_read_multi_use(self): from test.mockserver_tests.stale_read_model import Singer add_singer_query_result("SELECT singers.id, singers.name \nFROM singers") - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - echo=True, - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() timestamp = datetime.datetime.fromtimestamp(1733328910) for i in range(2): @@ -55,7 +50,7 @@ def test_stale_read_multi_use(self): # Verify the requests that we got. requests = self.spanner_service.requests eq_(7, len(requests)) - is_instance_of(requests[0], BatchCreateSessionsRequest) + is_instance_of(requests[0], CreateSessionRequest) is_instance_of(requests[1], BeginTransactionRequest) is_instance_of(requests[2], ExecuteSqlRequest) is_instance_of(requests[3], ExecuteSqlRequest) @@ -83,11 +78,7 @@ def test_stale_read_single_use(self): from test.mockserver_tests.stale_read_model import Singer add_singer_query_result("SELECT singers.id, singers.name \nFROM singers") - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - echo=True, - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() with Session( engine.execution_options( @@ -102,7 +93,7 @@ def test_stale_read_single_use(self): # Verify the requests that we got. requests = self.spanner_service.requests eq_(3, len(requests)) - is_instance_of(requests[0], BatchCreateSessionsRequest) + is_instance_of(requests[0], CreateSessionRequest) is_instance_of(requests[1], ExecuteSqlRequest) is_instance_of(requests[2], ExecuteSqlRequest) # Verify that the requests use a stale read. diff --git a/test/mockserver_tests/test_tags.py b/test/mockserver_tests/test_tags.py index 8c157154..0a4d6337 100644 --- a/test/mockserver_tests/test_tags.py +++ b/test/mockserver_tests/test_tags.py @@ -12,12 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from sqlalchemy import create_engine, select +from sqlalchemy import select from sqlalchemy.orm import Session from sqlalchemy.testing import eq_, is_instance_of from google.cloud.spanner_v1 import ( - FixedSizePool, - BatchCreateSessionsRequest, + CreateSessionRequest, ExecuteSqlRequest, BeginTransactionRequest, CommitRequest, @@ -36,10 +35,7 @@ def test_request_tag(self): from test.mockserver_tests.tags_model import Singer add_singer_query_result("SELECT singers.id, singers.name \n" + "FROM singers") - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() with Session(engine.execution_options(read_only=True)) as session: # Execute two queries in a read-only transaction. @@ -53,7 +49,7 @@ def test_request_tag(self): # Verify the requests that we got. requests = self.spanner_service.requests eq_(4, len(requests)) - is_instance_of(requests[0], BatchCreateSessionsRequest) + is_instance_of(requests[0], CreateSessionRequest) is_instance_of(requests[1], BeginTransactionRequest) is_instance_of(requests[2], ExecuteSqlRequest) is_instance_of(requests[3], ExecuteSqlRequest) @@ -71,10 +67,7 @@ def test_transaction_tag(self): "WHERE singers.id = @a0" ) add_update_count("INSERT INTO singers (id, name) VALUES (@a0, @a1)", 1) - engine = create_engine( - "spanner:///projects/p/instances/i/databases/d", - connect_args={"client": self.client, "pool": FixedSizePool(size=10)}, - ) + engine = self.create_engine() with Session( engine.execution_options(transaction_tag="my-transaction-tag") @@ -91,7 +84,7 @@ def test_transaction_tag(self): # Verify the requests that we got. requests = self.spanner_service.requests eq_(6, len(requests)) - is_instance_of(requests[0], BatchCreateSessionsRequest) + is_instance_of(requests[0], CreateSessionRequest) is_instance_of(requests[1], BeginTransactionRequest) is_instance_of(requests[2], ExecuteSqlRequest) is_instance_of(requests[3], ExecuteSqlRequest) diff --git a/test/system/test_basics.py b/test/system/test_basics.py index 7ea6fa2b..75d9682f 100644 --- a/test/system/test_basics.py +++ b/test/system/test_basics.py @@ -34,7 +34,7 @@ ) from sqlalchemy.orm import Session, DeclarativeBase, Mapped, mapped_column from sqlalchemy.types import REAL -from sqlalchemy.testing import eq_, is_true, is_not_none +from sqlalchemy.testing import eq_, is_true, is_not_none, is_none from sqlalchemy.testing.plugin.plugin_base import fixtures @@ -47,7 +47,7 @@ def define_tables(cls, metadata): Column("number", Integer), Column("name", String(20)), Column("alternative_name", String(20)), - Column("prime", Boolean), + Column("prime", Boolean, server_default=text("FALSE")), Column("ln", REAL), PrimaryKeyConstraint("number"), ) @@ -120,12 +120,15 @@ def test_reflect(self, connection): eq_(5, len(table.columns)) eq_("number", table.columns[0].name) eq_(BIGINT, type(table.columns[0].type)) + is_none(table.columns[0].server_default) eq_("name", table.columns[1].name) eq_(String, type(table.columns[1].type)) eq_("alternative_name", table.columns[2].name) eq_(String, type(table.columns[2].type)) eq_("prime", table.columns[3].name) eq_(Boolean, type(table.columns[3].type)) + is_not_none(table.columns[3].server_default) + eq_("FALSE", table.columns[3].server_default.arg.text) eq_("ln", table.columns[4].name) eq_(REAL, type(table.columns[4].type)) eq_(1, len(table.indexes)) @@ -316,6 +319,8 @@ class TimestampUser(Base): updated_at: Mapped[datetime.datetime] = mapped_column( spanner_allow_commit_timestamp=True, default=text("PENDING_COMMIT_TIMESTAMP()"), + # Make sure that this column is never part of a THEN RETURN clause. + spanner_exclude_from_returning=True, ) @event.listens_for(TimestampUser, "before_update") diff --git a/test/test_suite_20.py b/test/test_suite_20.py index 3cb252d2..27143de9 100644 --- a/test/test_suite_20.py +++ b/test/test_suite_20.py @@ -256,6 +256,11 @@ def test_binary_reflection(self, connection, metadata): assert isinstance(typ, LargeBinary) eq_(typ.length, 20) + @testing.requires.table_reflection + def test_string_length_reflection(self, connection, metadata): + typ = self._type_round_trip(connection, metadata, types.String(52))[0] + assert isinstance(typ, types.String) + class ComputedReflectionFixtureTest(_ComputedReflectionFixtureTest): @classmethod @@ -2576,6 +2581,12 @@ class BizarroCharacterFKResolutionTest(fixtures.TestBase): pass +class BizarroCharacterTest(fixtures.TestBase): + @pytest.mark.skip("Bizarre characters in foreign key names are not supported") + def test_fk_ref(self, testing_engine): + pass + + class IsolationLevelTest(fixtures.TestBase): @pytest.mark.skip("Cloud Spanner does not support different isolation levels") def test_dialect_user_setting_is_restored(self, testing_engine): diff --git a/test/unit/__init__.py b/test/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/unit/test_alembic.py b/test/unit/test_alembic.py new file mode 100644 index 00000000..75e39561 --- /dev/null +++ b/test/unit/test_alembic.py @@ -0,0 +1,97 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from alembic.ddl import base as ddl_base +from google.cloud.sqlalchemy_spanner import sqlalchemy_spanner +from sqlalchemy import String, TextClause +from sqlalchemy.testing import eq_ +from sqlalchemy.testing.plugin.plugin_base import fixtures + + +class TestAlembicTest(fixtures.TestBase): + def test_visit_column_nullable_with_not_null_column(self): + ddl = sqlalchemy_spanner.visit_column_nullable( + ddl_base.ColumnNullable( + name="tbl", column_name="col", nullable=False, existing_type=String(256) + ), + sqlalchemy_spanner.SpannerDDLCompiler( + sqlalchemy_spanner.SpannerDialect(), None + ), + ) + eq_(ddl, "ALTER TABLE tbl ALTER COLUMN col STRING(256) NOT NULL") + + def test_visit_column_nullable_with_nullable_column(self): + ddl = sqlalchemy_spanner.visit_column_nullable( + ddl_base.ColumnNullable( + name="tbl", column_name="col", nullable=True, existing_type=String(256) + ), + sqlalchemy_spanner.SpannerDDLCompiler( + sqlalchemy_spanner.SpannerDialect(), None + ), + ) + eq_(ddl, "ALTER TABLE tbl ALTER COLUMN col STRING(256)") + + def test_visit_column_nullable_with_default(self): + ddl = sqlalchemy_spanner.visit_column_nullable( + ddl_base.ColumnNullable( + name="tbl", + column_name="col", + nullable=False, + existing_type=String(256), + existing_server_default=TextClause("GENERATE_UUID()"), + ), + sqlalchemy_spanner.SpannerDDLCompiler( + sqlalchemy_spanner.SpannerDialect(), None + ), + ) + eq_( + ddl, + "ALTER TABLE tbl " + "ALTER COLUMN col " + "STRING(256) NOT NULL DEFAULT (GENERATE_UUID())", + ) + + def test_visit_column_type(self): + ddl = sqlalchemy_spanner.visit_column_type( + ddl_base.ColumnType( + name="tbl", + column_name="col", + type_=String(256), + existing_nullable=True, + ), + sqlalchemy_spanner.SpannerDDLCompiler( + sqlalchemy_spanner.SpannerDialect(), None + ), + ) + eq_(ddl, "ALTER TABLE tbl ALTER COLUMN col STRING(256)") + + def test_visit_column_type_with_default(self): + ddl = sqlalchemy_spanner.visit_column_type( + ddl_base.ColumnType( + name="tbl", + column_name="col", + type_=String(256), + existing_nullable=False, + existing_server_default=TextClause("GENERATE_UUID()"), + ), + sqlalchemy_spanner.SpannerDDLCompiler( + sqlalchemy_spanner.SpannerDialect(), None + ), + ) + eq_( + ddl, + "ALTER TABLE tbl " + "ALTER COLUMN col " + "STRING(256) NOT NULL DEFAULT (GENERATE_UUID())", + )