dbx-tools-postgres 0.6.222__tar.gz → 0.6.224__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbx-tools-postgres
3
- Version: 0.6.222
3
+ Version: 0.6.224
4
4
  Summary: WorkspaceClient-backed Lakebase Postgres resolution, SQLAlchemy engines, advisory locks, and LISTEN/NOTIFY topic bus
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
7
7
  Requires-Dist: asyncpg>=0.30
8
- Requires-Dist: databricks-sdk>=0.63.0
8
+ Requires-Dist: databricks-sdk>=0.123.0
9
9
  Requires-Dist: greenlet>=3.2
10
10
  Requires-Dist: psycopg[binary]>=3.2.9
11
11
  Requires-Dist: sqlalchemy>=2.0.41
12
- Requires-Dist: dbx-tools-core==0.6.222
13
- Requires-Dist: dbx-tools-core-rs==0.6.222
12
+ Requires-Dist: dbx-tools-core==0.6.224
13
+ Requires-Dist: dbx-tools-core-rs==0.6.224
14
14
  Requires-Python: >=3.11, <4
15
15
  Project-URL: Source, https://github.com/reggie-db/dbx-tools/tree/main/packages/py/postgres
16
16
  Description-Content-Type: text/markdown
@@ -39,12 +39,13 @@ Key features:
39
39
  - accepts the same Postgres URI, Lakebase resource path, hostname, and project-id
40
40
  address shapes as `@dbx-tools/appkit` through the native Rust parser;
41
41
  - resolves missing autoscaling endpoint fields through
42
- `WorkspaceClient.api_client`;
42
+ `WorkspaceClient.postgres`, including SDK-owned pagination and workspace-id
43
+ headers;
43
44
  - resolves provisioned Lakebase instance DNS through
44
45
  `WorkspaceClient.database`;
45
46
  - injects a cached database credential on SQLAlchemy's `do_connect` event rather
46
47
  than storing an expiring password in the engine URL, using the SDK's
47
- provisioned-instance API or the Autoscaling `/postgres/credentials` endpoint;
48
+ provisioned-instance or typed Autoscaling credential API;
48
49
  - refreshes built-in credential providers ahead of expiry with a process-local
49
50
  check-lock-check load, so concurrent pool connections share one mint;
50
51
  - supports sync psycopg and asyncpg SQLAlchemy engines;
@@ -143,7 +144,6 @@ driver mints the Lakebase token once and the UDF closes over it, so the executor
143
144
  builds a plain SQLAlchemy engine and installs the token as its provider:
144
145
 
145
146
  ```python
146
- from sqlalchemy import URL
147
147
  from sqlalchemy.ext.asyncio import create_async_engine
148
148
  from dbx_tools.postgres import (
149
149
  PostgresTopicBus,
@@ -153,26 +153,13 @@ from dbx_tools.postgres import (
153
153
 
154
154
  # driver: resolve once, capture in the closure
155
155
  resolved = resolve_postgres_connection(workspace_client, config)
156
- token = workspace_client.api_client.do(
157
- "POST",
158
- "/api/2.0/postgres/credentials",
159
- body={"endpoint": resolved.endpoint},
160
- )["token"]
156
+ token = workspace_client.postgres.generate_database_credential(resolved.endpoint).token
161
157
 
162
158
 
163
159
  @udf(returnType=StringType())
164
160
  def publish(key: str) -> str:
165
161
  async def run() -> str:
166
- engine = create_async_engine(
167
- URL.create(
168
- "postgresql+asyncpg",
169
- username=resolved.user,
170
- host=resolved.host,
171
- port=resolved.port,
172
- database=resolved.database,
173
- query={"ssl": resolved.ssl_mode},
174
- )
175
- )
162
+ engine = create_async_engine(resolved.url("postgresql+asyncpg"))
176
163
  install_credential_injection(engine.sync_engine, lambda: token)
177
164
  bus = PostgresTopicBus(engine, channel="app-events")
178
165
  try:
@@ -22,12 +22,13 @@ Key features:
22
22
  - accepts the same Postgres URI, Lakebase resource path, hostname, and project-id
23
23
  address shapes as `@dbx-tools/appkit` through the native Rust parser;
24
24
  - resolves missing autoscaling endpoint fields through
25
- `WorkspaceClient.api_client`;
25
+ `WorkspaceClient.postgres`, including SDK-owned pagination and workspace-id
26
+ headers;
26
27
  - resolves provisioned Lakebase instance DNS through
27
28
  `WorkspaceClient.database`;
28
29
  - injects a cached database credential on SQLAlchemy's `do_connect` event rather
29
30
  than storing an expiring password in the engine URL, using the SDK's
30
- provisioned-instance API or the Autoscaling `/postgres/credentials` endpoint;
31
+ provisioned-instance or typed Autoscaling credential API;
31
32
  - refreshes built-in credential providers ahead of expiry with a process-local
32
33
  check-lock-check load, so concurrent pool connections share one mint;
33
34
  - supports sync psycopg and asyncpg SQLAlchemy engines;
@@ -126,7 +127,6 @@ driver mints the Lakebase token once and the UDF closes over it, so the executor
126
127
  builds a plain SQLAlchemy engine and installs the token as its provider:
127
128
 
128
129
  ```python
129
- from sqlalchemy import URL
130
130
  from sqlalchemy.ext.asyncio import create_async_engine
131
131
  from dbx_tools.postgres import (
132
132
  PostgresTopicBus,
@@ -136,26 +136,13 @@ from dbx_tools.postgres import (
136
136
 
137
137
  # driver: resolve once, capture in the closure
138
138
  resolved = resolve_postgres_connection(workspace_client, config)
139
- token = workspace_client.api_client.do(
140
- "POST",
141
- "/api/2.0/postgres/credentials",
142
- body={"endpoint": resolved.endpoint},
143
- )["token"]
139
+ token = workspace_client.postgres.generate_database_credential(resolved.endpoint).token
144
140
 
145
141
 
146
142
  @udf(returnType=StringType())
147
143
  def publish(key: str) -> str:
148
144
  async def run() -> str:
149
- engine = create_async_engine(
150
- URL.create(
151
- "postgresql+asyncpg",
152
- username=resolved.user,
153
- host=resolved.host,
154
- port=resolved.port,
155
- database=resolved.database,
156
- query={"ssl": resolved.ssl_mode},
157
- )
158
- )
145
+ engine = create_async_engine(resolved.url("postgresql+asyncpg"))
159
146
  install_credential_injection(engine.sync_engine, lambda: token)
160
147
  bus = PostgresTopicBus(engine, channel="app-events")
161
148
  try:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dbx-tools-postgres"
3
- version = "0.6.222"
3
+ version = "0.6.224"
4
4
  description = "WorkspaceClient-backed Lakebase Postgres resolution, SQLAlchemy engines, advisory locks, and LISTEN/NOTIFY topic bus"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11,<4"
@@ -8,12 +8,12 @@ license = "Apache-2.0"
8
8
  license-files = ["LICENSE"]
9
9
  dependencies = [
10
10
  "asyncpg>=0.30",
11
- "databricks-sdk>=0.63.0",
11
+ "databricks-sdk>=0.123.0",
12
12
  "greenlet>=3.2",
13
13
  "psycopg[binary]>=3.2.9",
14
14
  "sqlalchemy>=2.0.41",
15
- "dbx-tools-core==0.6.222",
16
- "dbx-tools-core-rs==0.6.222",
15
+ "dbx-tools-core==0.6.224",
16
+ "dbx-tools-core-rs==0.6.224",
17
17
  ]
18
18
 
19
19
  [project.urls]
@@ -2,19 +2,19 @@
2
2
 
3
3
  [project]
4
4
  name = "dbx-tools-postgres"
5
- version = "0.6.222"
5
+ version = "0.6.224"
6
6
  description = "WorkspaceClient-backed Lakebase Postgres resolution, SQLAlchemy engines, advisory locks, and LISTEN/NOTIFY topic bus"
7
7
  readme = "README.md"
8
8
  requires-python = ">=3.11,<4"
9
9
  license = "Apache-2.0"
10
- license-files = ["LICENSE"]
11
- dependencies = ["asyncpg>=0.30", "databricks-sdk>=0.63.0", "greenlet>=3.2", "psycopg[binary]>=3.2.9", "sqlalchemy>=2.0.41", "dbx-tools-core==0.6.222", "dbx-tools-core-rs==0.6.222"]
10
+ license-files = [ "LICENSE" ]
11
+ dependencies = [ "asyncpg>=0.30", "databricks-sdk>=0.123.0", "greenlet>=3.2", "psycopg[binary]>=3.2.9", "sqlalchemy>=2.0.41", "dbx-tools-core==0.6.224", "dbx-tools-core-rs==0.6.224" ]
12
12
 
13
13
  [project.urls]
14
14
  Source = "https://github.com/reggie-db/dbx-tools/tree/main/packages/py/postgres"
15
15
 
16
16
  [build-system]
17
- requires = ["uv_build>=0.11.28,<0.12.0"]
17
+ requires = [ "uv_build>=0.11.28,<0.12.0" ]
18
18
  build-backend = "uv_build"
19
19
 
20
20
  [tool.uv.build-backend]
@@ -1,6 +1,6 @@
1
1
  """Lakebase address types and parsers backed by the Databricks Rust bindings."""
2
2
 
3
- from typing import Literal, TypeAlias
3
+ from typing import TypeAlias
4
4
 
5
5
  from dbx_tools.core_rs.bindings import (
6
6
  ParsedAddress,
@@ -11,11 +11,11 @@ from dbx_tools.core_rs.bindings import (
11
11
  SslMode as NativeSslMode,
12
12
  )
13
13
 
14
- SslMode = Literal["require", "disable", "prefer"]
15
- """Accepted PostgreSQL SSL modes."""
14
+ SslMode: TypeAlias = NativeSslMode
15
+ """Generated PostgreSQL SSL mode enum."""
16
16
 
17
- SSL_MODES: tuple[SslMode, ...] = ("require", "disable", "prefer")
18
- """All accepted values for :class:`SslMode`."""
17
+ SSL_MODES: tuple[str, ...] = tuple(mode.name.lower() for mode in NativeSslMode)
18
+ """Accepted configuration spellings derived from :class:`SslMode`."""
19
19
 
20
20
  LakebaseConnectionInputs: TypeAlias = ParsedAddress
21
21
  """Resolved address inputs returned by the native parser."""
@@ -10,6 +10,11 @@ from collections.abc import Callable, Mapping
10
10
  from dataclasses import dataclass
11
11
  from typing import Any, Protocol
12
12
 
13
+ from databricks.sdk.service.postgres import (
14
+ Endpoint,
15
+ EndpointType,
16
+ PostgresAPI,
17
+ )
13
18
  from sqlalchemy import URL, Engine, event
14
19
  from sqlalchemy import create_engine as sqlalchemy_create_engine
15
20
  from sqlalchemy.ext.asyncio import AsyncEngine
@@ -30,27 +35,21 @@ CredentialProvider = Callable[[], str]
30
35
  CredentialLoader = Callable[[], tuple[str, dt.datetime | None]]
31
36
  """Load a database credential and its optional absolute expiration time."""
32
37
 
33
- _API_BASE = "/api/2.0/postgres"
34
38
  _CREDENTIAL_REFRESH_LEAD = dt.timedelta(minutes=5)
35
39
  _DEFAULT_CREDENTIAL_LIFETIME = dt.timedelta(minutes=50)
36
40
  _MINIMUM_CREDENTIAL_LIFETIME = dt.timedelta(minutes=1)
37
41
  _DEFAULT_DATABASE = "databricks_postgres"
38
42
  _DEFAULT_PORT = 5432
39
- _DEFAULT_SSL_MODE: SslMode = "require"
40
- _READ_WRITE_ENDPOINT_TYPE = "ENDPOINT_TYPE_READ_WRITE"
41
-
42
-
43
- class WorkspaceApiClient(Protocol):
44
- def do(self, method: str, path: str, **kwargs: Any) -> Any: ...
43
+ _DEFAULT_SSL_MODE: SslMode = NativeSslMode.REQUIRE
45
44
 
46
45
 
47
46
  class WorkspaceClientLike(Protocol):
48
47
  """WorkspaceClient surface required for Lakebase discovery and credentials."""
49
48
 
50
- api_client: WorkspaceApiClient
51
49
  config: Any
52
50
  current_user: Any
53
51
  database: Any
52
+ postgres: PostgresAPI
54
53
 
55
54
 
56
55
  @dataclass(frozen=True, slots=True)
@@ -71,7 +70,7 @@ class PostgresEngineConfig:
71
70
  database: str | None = None
72
71
  host: str | None = None
73
72
  port: int | None = None
74
- ssl_mode: SslMode | None = None
73
+ ssl_mode: SslMode | str | None = None
75
74
  user: str | None = None
76
75
 
77
76
 
@@ -104,7 +103,7 @@ class ResolvedPostgresConnection:
104
103
  host=self.host,
105
104
  port=self.port,
106
105
  database=self.database,
107
- query={ssl_parameter: self.ssl_mode},
106
+ query={ssl_parameter: self.ssl_mode.name.lower()},
108
107
  )
109
108
 
110
109
 
@@ -163,24 +162,19 @@ def autoscaling_credential_provider(
163
162
  ) -> CredentialProvider:
164
163
  """Create a cached Autoscaling-Lakebase endpoint credential provider.
165
164
 
166
- Tokens come from ``POST /api/2.0/postgres/credentials`` and use the same
167
- process-local early-refresh and refresh-lock policy as provisioned tokens.
165
+ Tokens come from the typed Postgres SDK and use the same process-local
166
+ early-refresh and refresh-lock policy as provisioned tokens.
168
167
  """
169
168
 
170
169
  if not endpoint.strip():
171
170
  raise ValueError("endpoint must not be empty")
172
171
 
173
172
  def load() -> tuple[str, dt.datetime | None]:
174
- credential = workspace_client.api_client.do(
175
- "POST",
176
- f"{_API_BASE}/credentials",
177
- headers={"Accept": "application/json", "Content-Type": "application/json"},
178
- body={"endpoint": endpoint},
179
- )
180
- token = credential.get("token") if isinstance(credential, dict) else None
173
+ credential = workspace_client.postgres.generate_database_credential(endpoint)
174
+ token = credential.token
181
175
  if not isinstance(token, str) or not token:
182
176
  raise RuntimeError("WorkspaceClient returned no Lakebase database credential token")
183
- return token, _mapping_credential_expiration(credential)
177
+ return token, _credential_expiration(credential.expire_time)
184
178
 
185
179
  return _CachedCredentialProvider(load)
186
180
 
@@ -295,15 +289,14 @@ def resolve_postgres_connection(
295
289
  if endpoint:
296
290
  endpoint_parts = parse_address(endpoint)
297
291
  if endpoint_parts.project and endpoint_parts.branch and endpoint_parts.endpoint_id:
298
- response = _get(
299
- workspace_client,
300
- f"{_API_BASE}/projects/{endpoint_parts.project}/branches/"
301
- f"{endpoint_parts.branch}/endpoints/{endpoint_parts.endpoint_id}",
292
+ response = workspace_client.postgres.get_endpoint(
293
+ f"projects/{endpoint_parts.project}/branches/"
294
+ f"{endpoint_parts.branch}/endpoints/{endpoint_parts.endpoint_id}"
302
295
  )
303
296
  host = _endpoint_host(response)
304
297
  if not host:
305
298
  endpoint_record = _pick_endpoint(workspace_client, project, branch)
306
- endpoint = _string(endpoint_record.get("name"))
299
+ endpoint = _string(endpoint_record.name)
307
300
  host = _endpoint_host(endpoint_record)
308
301
 
309
302
  if not database and project and branch:
@@ -343,6 +336,9 @@ def _credential_renewal(expiration: dt.datetime | None) -> dt.datetime:
343
336
  def _credential_expiration(value: object) -> dt.datetime | None:
344
337
  if isinstance(value, dt.datetime):
345
338
  return value
339
+ to_datetime = getattr(value, "ToDatetime", None)
340
+ if callable(to_datetime):
341
+ return to_datetime(tzinfo=dt.UTC)
346
342
  if isinstance(value, (int, float)):
347
343
  try:
348
344
  return dt.datetime.fromtimestamp(value, tz=dt.UTC)
@@ -357,14 +353,6 @@ def _credential_expiration(value: object) -> dt.datetime | None:
357
353
  return None
358
354
 
359
355
 
360
- def _mapping_credential_expiration(credential: Mapping[str, object]) -> dt.datetime | None:
361
- for key in ("expiration_time", "expirationTime", "expires_at", "expiresAt"):
362
- expiration = _credential_expiration(credential.get(key))
363
- if expiration is not None:
364
- return expiration
365
- return None
366
-
367
-
368
356
  def _utcnow() -> dt.datetime:
369
357
  return dt.datetime.now(tz=dt.UTC)
370
358
 
@@ -391,23 +379,9 @@ def _workspace_user(workspace_client: WorkspaceClientLike) -> str | None:
391
379
  )
392
380
 
393
381
 
394
- def _get(workspace_client: WorkspaceClientLike, path: str) -> dict[str, Any]:
395
- value = workspace_client.api_client.do("GET", path, headers={"Accept": "application/json"})
396
- if not isinstance(value, dict):
397
- raise TypeError(f"WorkspaceClient returned a non-object response for {path}")
398
- return value
399
-
400
-
401
- def _list(workspace_client: WorkspaceClientLike, path: str, key: str) -> list[dict[str, Any]]:
402
- values = _get(workspace_client, path).get(key, [])
403
- if not isinstance(values, list):
404
- raise TypeError(f"WorkspaceClient returned a non-list {key} response for {path}")
405
- return [value for value in values if isinstance(value, dict)]
406
-
407
-
408
382
  def _pick_project(workspace_client: WorkspaceClientLike) -> str:
409
- projects = _list(workspace_client, f"{_API_BASE}/projects", "projects")
410
- candidates = [_resource_part(project.get("name"), "project") for project in projects]
383
+ projects = workspace_client.postgres.list_projects()
384
+ candidates = [_resource_part(project.name, "project") for project in projects]
411
385
  candidates = [candidate for candidate in candidates if candidate]
412
386
  if len(candidates) != 1:
413
387
  raise ValueError(f"Expected one Lakebase project, found: {', '.join(candidates) or 'none'}")
@@ -415,20 +389,16 @@ def _pick_project(workspace_client: WorkspaceClientLike) -> str:
415
389
 
416
390
 
417
391
  def _pick_branch(workspace_client: WorkspaceClientLike, project: str) -> str:
418
- branches = _list(
419
- workspace_client,
420
- f"{_API_BASE}/projects/{project}/branches",
421
- "branches",
422
- )
392
+ branches = list(workspace_client.postgres.list_branches(f"projects/{project}"))
423
393
  selected = next(
424
394
  (
425
395
  branch
426
396
  for branch in branches
427
- if isinstance(branch.get("status"), dict) and branch["status"].get("default") is True
397
+ if branch.status is not None and branch.status.default is True
428
398
  ),
429
399
  branches[0] if len(branches) == 1 else None,
430
400
  )
431
- branch = _resource_part(selected.get("name") if selected else None, "branch")
401
+ branch = _resource_part(selected.name if selected else None, "branch")
432
402
  if not branch:
433
403
  raise ValueError(f"Could not choose a Lakebase branch for project {project}")
434
404
  return branch
@@ -438,18 +408,16 @@ def _pick_endpoint(
438
408
  workspace_client: WorkspaceClientLike,
439
409
  project: str,
440
410
  branch: str,
441
- ) -> dict[str, Any]:
442
- endpoints = _list(
443
- workspace_client,
444
- f"{_API_BASE}/projects/{project}/branches/{branch}/endpoints",
445
- "endpoints",
411
+ ) -> Endpoint:
412
+ endpoints = list(
413
+ workspace_client.postgres.list_endpoints(f"projects/{project}/branches/{branch}")
446
414
  )
447
415
  selected = next(
448
416
  (
449
417
  endpoint
450
418
  for endpoint in endpoints
451
- if isinstance(endpoint.get("status"), dict)
452
- and endpoint["status"].get("endpoint_type") == _READ_WRITE_ENDPOINT_TYPE
419
+ if endpoint.status is not None
420
+ and endpoint.status.endpoint_type is EndpointType.ENDPOINT_TYPE_READ_WRITE
453
421
  ),
454
422
  endpoints[0] if len(endpoints) == 1 else None,
455
423
  )
@@ -464,21 +432,19 @@ def _pick_database(
464
432
  branch: str,
465
433
  resource_id: str | None,
466
434
  ) -> str:
467
- databases = _list(
468
- workspace_client,
469
- f"{_API_BASE}/projects/{project}/branches/{branch}/databases",
470
- "databases",
435
+ databases = list(
436
+ workspace_client.postgres.list_databases(f"projects/{project}/branches/{branch}")
471
437
  )
472
438
  if resource_id:
473
439
  databases = [
474
440
  database
475
441
  for database in databases
476
- if _resource_part(database.get("name"), "database") == resource_id
442
+ if _resource_part(database.name, "database") == resource_id
477
443
  ]
478
444
  names = [
479
- _string(database.get("status", {}).get("postgres_database"))
445
+ _string(database.status.postgres_database)
480
446
  for database in databases
481
- if isinstance(database.get("status"), dict)
447
+ if database.status is not None
482
448
  ]
483
449
  names = [name for name in names if name]
484
450
  if _DEFAULT_DATABASE in names:
@@ -492,32 +458,25 @@ def _find_endpoint_by_host(
492
458
  workspace_client: WorkspaceClientLike,
493
459
  host: str,
494
460
  ) -> tuple[str, str, str] | None:
495
- for project_record in _list(workspace_client, f"{_API_BASE}/projects", "projects"):
496
- project = _resource_part(project_record.get("name"), "project")
461
+ for project_record in workspace_client.postgres.list_projects():
462
+ project = _resource_part(project_record.name, "project")
497
463
  if not project:
498
464
  continue
499
- for branch_record in _list(
500
- workspace_client,
501
- f"{_API_BASE}/projects/{project}/branches",
502
- "branches",
503
- ):
504
- branch = _resource_part(branch_record.get("name"), "branch")
465
+ for branch_record in workspace_client.postgres.list_branches(f"projects/{project}"):
466
+ branch = _resource_part(branch_record.name, "branch")
505
467
  if not branch:
506
468
  continue
507
- for endpoint in _list(
508
- workspace_client,
509
- f"{_API_BASE}/projects/{project}/branches/{branch}/endpoints",
510
- "endpoints",
469
+ for endpoint in workspace_client.postgres.list_endpoints(
470
+ f"projects/{project}/branches/{branch}"
511
471
  ):
512
- if _endpoint_host(endpoint) == host and (name := _string(endpoint.get("name"))):
472
+ if _endpoint_host(endpoint) == host and (name := _string(endpoint.name)):
513
473
  return project, branch, name
514
474
  return None
515
475
 
516
476
 
517
- def _endpoint_host(endpoint: Mapping[str, Any]) -> str | None:
518
- status = endpoint.get("status")
519
- hosts = status.get("hosts") if isinstance(status, dict) else None
520
- return _string(hosts.get("host")) if isinstance(hosts, dict) else None
477
+ def _endpoint_host(endpoint: Endpoint) -> str | None:
478
+ hosts = endpoint.status.hosts if endpoint.status is not None else None
479
+ return _string(hosts.host) if hosts is not None else None
521
480
 
522
481
 
523
482
  def _resource_part(value: object, kind: str) -> str | None:
@@ -545,10 +504,14 @@ def _parse_port(value: object) -> int:
545
504
  def _parse_ssl_mode(value: object) -> SslMode:
546
505
  if value is None or value == "":
547
506
  return _DEFAULT_SSL_MODE
548
- mode = value.name.lower() if isinstance(value, NativeSslMode) else str(value).strip().lower()
549
- if mode not in SSL_MODES:
550
- raise ValueError(f"PGSSLMODE must be one of {', '.join(SSL_MODES)}, got {value!r}")
551
- return mode # type: ignore[return-value]
507
+ if isinstance(value, NativeSslMode):
508
+ return value
509
+ normalized = str(value).strip().upper()
510
+ try:
511
+ return NativeSslMode[normalized]
512
+ except KeyError:
513
+ accepted = ", ".join(SSL_MODES)
514
+ raise ValueError(f"PGSSLMODE must be one of {accepted}, got {value!r}") from None
552
515
 
553
516
 
554
517
  def _first(*values: Any) -> Any: