dbctl 0.7.1__tar.gz → 0.7.2__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.
Files changed (52) hide show
  1. {dbctl-0.7.1 → dbctl-0.7.2}/CHANGELOG.md +19 -0
  2. {dbctl-0.7.1 → dbctl-0.7.2}/PKG-INFO +7 -1
  3. {dbctl-0.7.1 → dbctl-0.7.2}/README.md +6 -0
  4. {dbctl-0.7.1 → dbctl-0.7.2}/dbctl/db.py +8 -0
  5. dbctl-0.7.2/dbctl/refs.py +173 -0
  6. {dbctl-0.7.1 → dbctl-0.7.2}/dbctl/tunnels/base.py +7 -0
  7. {dbctl-0.7.1 → dbctl-0.7.2}/docs/connections.md +55 -0
  8. {dbctl-0.7.1 → dbctl-0.7.2}/docs/operations.md +13 -13
  9. {dbctl-0.7.1 → dbctl-0.7.2}/pyproject.toml +1 -1
  10. {dbctl-0.7.1 → dbctl-0.7.2}/tests/test_copy_features.py +2 -2
  11. dbctl-0.7.2/tests/test_refs.py +292 -0
  12. {dbctl-0.7.1 → dbctl-0.7.2}/uv.lock +214 -1
  13. {dbctl-0.7.1 → dbctl-0.7.2}/.dbctl/connections.yaml +0 -0
  14. {dbctl-0.7.1 → dbctl-0.7.2}/.dbctl/operations.yaml +0 -0
  15. {dbctl-0.7.1 → dbctl-0.7.2}/.github/workflows/ci.yml +0 -0
  16. {dbctl-0.7.1 → dbctl-0.7.2}/.github-local/ci.yml +0 -0
  17. {dbctl-0.7.1 → dbctl-0.7.2}/.gitignore +0 -0
  18. {dbctl-0.7.1 → dbctl-0.7.2}/Makefile +0 -0
  19. {dbctl-0.7.1 → dbctl-0.7.2}/dbctl/__init__.py +0 -0
  20. {dbctl-0.7.1 → dbctl-0.7.2}/dbctl/__main__.py +0 -0
  21. {dbctl-0.7.1 → dbctl-0.7.2}/dbctl/audit.py +0 -0
  22. {dbctl-0.7.1 → dbctl-0.7.2}/dbctl/cli.py +0 -0
  23. {dbctl-0.7.1 → dbctl-0.7.2}/dbctl/config.py +0 -0
  24. {dbctl-0.7.1 → dbctl-0.7.2}/dbctl/connections.py +0 -0
  25. {dbctl-0.7.1 → dbctl-0.7.2}/dbctl/execute.py +0 -0
  26. {dbctl-0.7.1 → dbctl-0.7.2}/dbctl/init.py +0 -0
  27. {dbctl-0.7.1 → dbctl-0.7.2}/dbctl/multi.py +0 -0
  28. {dbctl-0.7.1 → dbctl-0.7.2}/dbctl/operations.py +0 -0
  29. {dbctl-0.7.1 → dbctl-0.7.2}/dbctl/reports.py +0 -0
  30. {dbctl-0.7.1 → dbctl-0.7.2}/dbctl/runtime.py +0 -0
  31. {dbctl-0.7.1 → dbctl-0.7.2}/dbctl/tunnels/__init__.py +0 -0
  32. {dbctl-0.7.1 → dbctl-0.7.2}/dbctl/tunnels/direct.py +0 -0
  33. {dbctl-0.7.1 → dbctl-0.7.2}/dbctl/tunnels/k8s.py +0 -0
  34. {dbctl-0.7.1 → dbctl-0.7.2}/dbctl/tunnels/ssh.py +0 -0
  35. {dbctl-0.7.1 → dbctl-0.7.2}/dbctl/tunnels/ssm.py +0 -0
  36. {dbctl-0.7.1 → dbctl-0.7.2}/docker-compose.yml +0 -0
  37. {dbctl-0.7.1 → dbctl-0.7.2}/docs/ACTION_OUTPUT.md +0 -0
  38. {dbctl-0.7.1 → dbctl-0.7.2}/docs/DESIGN.md +0 -0
  39. {dbctl-0.7.1 → dbctl-0.7.2}/docs/SESSION_STATE.md +0 -0
  40. {dbctl-0.7.1 → dbctl-0.7.2}/docs/logo.png +0 -0
  41. {dbctl-0.7.1 → dbctl-0.7.2}/docs/logo_small.png +0 -0
  42. {dbctl-0.7.1 → dbctl-0.7.2}/docs/tutorial.md +0 -0
  43. {dbctl-0.7.1 → dbctl-0.7.2}/seed/mssql.sql +0 -0
  44. {dbctl-0.7.1 → dbctl-0.7.2}/seed/mysql.sql +0 -0
  45. {dbctl-0.7.1 → dbctl-0.7.2}/seed/postgres.sql +0 -0
  46. {dbctl-0.7.1 → dbctl-0.7.2}/tests/test_bastion_tags.py +0 -0
  47. {dbctl-0.7.1 → dbctl-0.7.2}/tests/test_connections_loader.py +0 -0
  48. {dbctl-0.7.1 → dbctl-0.7.2}/tests/test_issue_1.py +0 -0
  49. {dbctl-0.7.1 → dbctl-0.7.2}/tests/test_k8s_tunnel.py +0 -0
  50. {dbctl-0.7.1 → dbctl-0.7.2}/tests/test_regressions.py +0 -0
  51. {dbctl-0.7.1 → dbctl-0.7.2}/tests/test_smoke.py +0 -0
  52. {dbctl-0.7.1 → dbctl-0.7.2}/tests/test_sso_cache.py +0 -0
@@ -5,6 +5,25 @@ All notable changes to this project will be documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.7.2] — 2026-08-04
9
+
10
+ ### Added
11
+
12
+ - **`{{ssm:...}}` connection references** — any string field on a
13
+ connection (`password`, `username`, `database`, `driver`, `url`, or a
14
+ nested tunnel field like `ssm.remote_host` / `direct.host`) can now be
15
+ a placeholder — `{{ssm:<parameter-name>[;property:<json-key>][;profile:<aws-profile>]}}`
16
+ — resolved against AWS SSM Parameter Store instead of a literal value.
17
+ `SecureString` parameters are fetched with decryption; `property`
18
+ extracts one key from a JSON-object parameter value (so several
19
+ credentials can share one parameter). Resolution is lazy — it runs via
20
+ `dbctl.refs.resolve_connection` right before a connection is actually
21
+ used to open a tunnel or build an engine, never at `connections.yaml`
22
+ load time and never from `dbctl connections list` / `show` — so
23
+ browsing the registry never makes an AWS call or risks echoing a
24
+ decrypted secret, and one unreachable profile can't break unrelated
25
+ connections. See `docs/connections.md` for the full reference.
26
+
8
27
  ## [0.7.1] — 2026-08-03
9
28
 
10
29
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbctl
3
- Version: 0.7.1
3
+ Version: 0.7.2
4
4
  Summary: Generic CLI to monitor, control, and administer multiple databases via SSM, SSH, or direct connection.
5
5
  Author: dbctl contributors
6
6
  License: MIT
@@ -339,6 +339,12 @@ DB password sources are mutually exclusive — pick **one** per connection:
339
339
  | `windows_sso: true` | mssql+pyodbc only: Windows Integrated Security (`Trusted_Connection=yes`). No username/password needed. |
340
340
  | `url: "…"` | full SQLAlchemy URL (overrides all the above). Use for Azure AD, ODBC-specific kwargs, or any non-standard connection string. |
341
341
 
342
+ Any of these — plus `username`, `database`, `driver`, or a tunnel field like
343
+ `direct.host` — can instead be a `{{ssm:/param/path;property:key;profile:name}}`
344
+ placeholder resolved against AWS SSM Parameter Store at connection time. See
345
+ [`docs/connections.md`](docs/connections.md#ssm-references) for the full
346
+ reference.
347
+
342
348
  Secret-typed **operation** parameters (`type: secret`) are redacted in the
343
349
  audit log regardless of which DB password source the connection uses.
344
350
 
@@ -305,6 +305,12 @@ DB password sources are mutually exclusive — pick **one** per connection:
305
305
  | `windows_sso: true` | mssql+pyodbc only: Windows Integrated Security (`Trusted_Connection=yes`). No username/password needed. |
306
306
  | `url: "…"` | full SQLAlchemy URL (overrides all the above). Use for Azure AD, ODBC-specific kwargs, or any non-standard connection string. |
307
307
 
308
+ Any of these — plus `username`, `database`, `driver`, or a tunnel field like
309
+ `direct.host` — can instead be a `{{ssm:/param/path;property:key;profile:name}}`
310
+ placeholder resolved against AWS SSM Parameter Store at connection time. See
311
+ [`docs/connections.md`](docs/connections.md#ssm-references) for the full
312
+ reference.
313
+
308
314
  Secret-typed **operation** parameters (`type: secret`) are redacted in the
309
315
  audit log regardless of which DB password source the connection uses.
310
316
 
@@ -102,7 +102,15 @@ def build_engine(conn: Connection, tunnel: Tunnel, *, echo: bool = False) -> Eng
102
102
  password injected — the URL is just ``driver:///path/to/file``. The
103
103
  tunnel's local bind is irrelevant for file-based DBs (the file is
104
104
  local), and injecting `host:port` makes SQLAlchemy reject the URL.
105
+
106
+ Resolves any ``{{ssm:...}}`` placeholders on ``conn`` first (see
107
+ ``dbctl.refs``) — this is the point a connection is actually used, so
108
+ it's the right place for that lazy resolution to happen.
105
109
  """
110
+ from dbctl.refs import resolve_connection
111
+
112
+ conn = resolve_connection(conn)
113
+
106
114
  driver = _driver_name(conn)
107
115
  _check_driver_available(driver)
108
116
 
@@ -0,0 +1,173 @@
1
+ """Resolve ``{{ssm:...}}`` placeholders in connection values against AWS SSM
2
+ Parameter Store, so secrets never need to live in plaintext in
3
+ ``connections.yaml``.
4
+
5
+ Reference format::
6
+
7
+ {{ssm:<parameter-name>[;property:<json-key>][;profile:<aws-profile>]}}
8
+
9
+ Resolution shells out to the ``aws`` CLI — the same convention already used
10
+ by :mod:`dbctl.tunnels.ssm` for bastion/session lookups — so no extra AWS SDK
11
+ dependency is required.
12
+
13
+ Resolution is deliberately **lazy**: nothing in this module runs at
14
+ ``connections.yaml`` load time. It only runs when :func:`resolve_connection`
15
+ is called, which happens right before a connection is actually used (see
16
+ ``build_tunnel`` / ``build_engine``). A connection nobody is using — or one
17
+ just being displayed via ``dbctl connections show`` — never triggers an AWS
18
+ call, so one unreachable profile can't break unrelated connections.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import re
25
+ import subprocess
26
+ from typing import TYPE_CHECKING, Any
27
+
28
+ if TYPE_CHECKING:
29
+ from dbctl.config import Connection
30
+
31
+ _REF_RE = re.compile(r"\{\{ssm:([^}]+)\}\}")
32
+
33
+
34
+ class RefResolutionError(RuntimeError):
35
+ """Raised when a ``{{ssm:...}}`` reference cannot be resolved.
36
+
37
+ Messages here must only ever name the *reference* (parameter name,
38
+ property, profile) — never the resolved value — so this exception is
39
+ always safe to print to logs/stderr/console.
40
+ """
41
+
42
+
43
+ def _parse_ref(spec: str) -> tuple[str, str | None, str | None]:
44
+ """Split ``<name>[;property:<key>][;profile:<profile>]`` into its parts."""
45
+ parts = spec.split(";")
46
+ name = parts[0].strip()
47
+ if not name:
48
+ raise RefResolutionError(f"ssm reference {{{{ssm:{spec}}}}} is missing a parameter name")
49
+
50
+ property_: str | None = None
51
+ profile: str | None = None
52
+ for raw_part in parts[1:]:
53
+ part = raw_part.strip()
54
+ if not part:
55
+ continue
56
+ key, sep, value = part.partition(":")
57
+ if not sep:
58
+ raise RefResolutionError(
59
+ f"ssm reference {{{{ssm:{spec}}}}}: malformed option {part!r} (expected key:value)"
60
+ )
61
+ key = key.strip()
62
+ value = value.strip()
63
+ if key == "property":
64
+ property_ = value
65
+ elif key == "profile":
66
+ profile = value
67
+ else:
68
+ raise RefResolutionError(f"ssm reference {{{{ssm:{spec}}}}}: unknown option {key!r}")
69
+ return name, property_, profile
70
+
71
+
72
+ def _fetch_parameter(name: str, profile: str | None) -> str:
73
+ """Fetch a parameter's raw value via ``aws ssm get-parameter``.
74
+
75
+ Always passes ``--with-decryption`` — a no-op for plain ``String``
76
+ parameters, required for ``SecureString`` ones.
77
+ """
78
+ cmd = ["aws", "ssm", "get-parameter", "--name", name, "--with-decryption", "--output", "json"]
79
+ if profile:
80
+ cmd[1:1] = ["--profile", profile]
81
+
82
+ try:
83
+ result = subprocess.run(cmd, check=True, capture_output=True, text=True, timeout=15)
84
+ except FileNotFoundError as e:
85
+ raise RefResolutionError(
86
+ "the `aws` CLI was not found on PATH — install it before resolving ssm: references"
87
+ ) from e
88
+ except subprocess.CalledProcessError as e:
89
+ stderr = (e.stderr or "").strip()
90
+ raise RefResolutionError(
91
+ f"failed to resolve ssm parameter {name!r}: {stderr[:300] or 'unknown error'}"
92
+ ) from e
93
+ except subprocess.TimeoutExpired as e:
94
+ raise RefResolutionError(f"aws ssm get-parameter timed out resolving {name!r}") from e
95
+
96
+ try:
97
+ payload: Any = json.loads(result.stdout)
98
+ value = payload["Parameter"]["Value"]
99
+ except (json.JSONDecodeError, KeyError, TypeError) as e:
100
+ raise RefResolutionError(f"unexpected response resolving ssm parameter {name!r}") from e
101
+ if not isinstance(value, str):
102
+ raise RefResolutionError(f"unexpected response resolving ssm parameter {name!r}")
103
+ return value
104
+
105
+
106
+ def _extract_property(raw_value: str, property_name: str, param_name: str) -> str:
107
+ try:
108
+ parsed = json.loads(raw_value)
109
+ except json.JSONDecodeError as e:
110
+ raise RefResolutionError(
111
+ f"ssm parameter {param_name!r} is not valid JSON; cannot extract property {property_name!r}"
112
+ ) from e
113
+ if not isinstance(parsed, dict) or property_name not in parsed:
114
+ raise RefResolutionError(f"ssm parameter {param_name!r} has no property {property_name!r}")
115
+ return str(parsed[property_name])
116
+
117
+
118
+ def resolve_ssm_ref(spec: str, *, cache: dict[str, str]) -> str:
119
+ """Resolve one ``<name>[;property:...][;profile:...]`` reference body
120
+ (the part inside ``{{ssm: }}``), consulting/populating ``cache`` so the
121
+ same reference is never fetched twice within one resolution pass."""
122
+ if spec in cache:
123
+ return cache[spec]
124
+ name, property_, profile = _parse_ref(spec)
125
+ raw = _fetch_parameter(name, profile)
126
+ value = _extract_property(raw, property_, name) if property_ else raw
127
+ cache[spec] = value
128
+ return value
129
+
130
+
131
+ def resolve_string(value: str, *, cache: dict[str, str]) -> str:
132
+ """Replace every ``{{ssm:...}}`` occurrence in ``value`` with its
133
+ resolved value. Strings with no reference are returned unchanged
134
+ (and never trigger an AWS call)."""
135
+ if "{{ssm:" not in value:
136
+ return value
137
+ return _REF_RE.sub(lambda m: resolve_ssm_ref(m.group(1), cache=cache), value)
138
+
139
+
140
+ def resolve_value(value: Any, *, cache: dict[str, str]) -> Any:
141
+ """Recursively resolve ``{{ssm:...}}`` references nested in dicts/lists."""
142
+ if isinstance(value, str):
143
+ return resolve_string(value, cache=cache)
144
+ if isinstance(value, dict):
145
+ return {k: resolve_value(v, cache=cache) for k, v in value.items()}
146
+ if isinstance(value, list):
147
+ return [resolve_value(v, cache=cache) for v in value]
148
+ return value
149
+
150
+
151
+ def resolve_connection(conn: Connection) -> Connection:
152
+ """Return a copy of ``conn`` with every ``{{ssm:...}}`` placeholder in
153
+ its string fields (including nested tunnel blocks) resolved.
154
+
155
+ Call this right before a connection is actually used (opening a tunnel,
156
+ building an engine) — never at ``connections.yaml`` load time or from a
157
+ listing/display command, so browsing the registry never makes an AWS
158
+ call or risks echoing a decrypted secret to the screen.
159
+ """
160
+ from dbctl.config import Connection
161
+
162
+ raw = conn.model_dump()
163
+ resolved = resolve_value(raw, cache={})
164
+ return Connection.model_validate(resolved)
165
+
166
+
167
+ __all__ = [
168
+ "RefResolutionError",
169
+ "resolve_connection",
170
+ "resolve_ssm_ref",
171
+ "resolve_string",
172
+ "resolve_value",
173
+ ]
@@ -56,12 +56,19 @@ def build_tunnel(conn: Connection, *, override_port: int | None = None) -> Tunne
56
56
  the local bind port even when the config says ``local_port: 0`` (auto).
57
57
  For ``direct`` tunnels the override replaces the upstream port (useful
58
58
  for pointing at a different port than the config declares).
59
+
60
+ Resolves any ``{{ssm:...}}`` placeholders on ``conn`` first (see
61
+ ``dbctl.refs``) — this is the point a connection is actually used, so
62
+ it's the right place for that lazy resolution to happen.
59
63
  """
64
+ from dbctl.refs import resolve_connection
60
65
  from dbctl.tunnels.direct import DirectTunnel as _Direct
61
66
  from dbctl.tunnels.k8s import K8sTunnel as _K8k
62
67
  from dbctl.tunnels.ssh import SshTunnel as _Ssh
63
68
  from dbctl.tunnels.ssm import SsmTunnel as _Ssm
64
69
 
70
+ conn = resolve_connection(conn)
71
+
65
72
  match conn.type.value:
66
73
  case "ssm":
67
74
  assert conn.ssm
@@ -97,6 +97,61 @@ connections.yaml: 1 invalid connection skipped:
97
97
  The offending connection is simply absent from `dbctl connections list` until
98
98
  you fix it — the CLI never refuses to start because of one bad entry.
99
99
 
100
+ ## `{{ssm:...}}` references
101
+
102
+ Any string field on a connection — `password`, `username`, `database`,
103
+ `driver`, `url`, or a nested tunnel field like `ssm.remote_host` or
104
+ `direct.host` — can be a placeholder that resolves against AWS SSM
105
+ Parameter Store instead of a literal value:
106
+
107
+ ```
108
+ {{ssm:<parameter-name>[;property:<json-key>][;profile:<aws-profile>]}}
109
+ ```
110
+
111
+ | part | required | meaning |
112
+ |------|----------|---------|
113
+ | `<parameter-name>` | yes | full SSM parameter name/path, e.g. `/prod/db/password`. |
114
+ | `property` | no | if the parameter value is a JSON object, extract only this key. Omit to use the whole raw value. |
115
+ | `profile` | no | AWS CLI profile for the lookup. Omit to use the default credential chain (env vars, default profile, instance role). |
116
+
117
+ ```yaml
118
+ connections:
119
+ prod-pg:
120
+ type: direct
121
+ driver: postgresql+psycopg
122
+ database: app
123
+ username: app_admin
124
+ password: "{{ssm:/prod/db/credentials;property:password;profile:prod-admin}}"
125
+ direct:
126
+ host: "{{ssm:/prod/db/credentials;property:host;profile:prod-admin}}"
127
+ ```
128
+
129
+ Given an SSM parameter `/prod/db/credentials` holding
130
+ `{"username": "app_admin", "password": "s3cret", "host": "prod.rds.internal"}`,
131
+ `{{ssm:/prod/db/credentials;property:password}}` resolves to `s3cret`.
132
+
133
+ **When resolution happens.** References are resolved lazily, right before a
134
+ connection is actually used to open a tunnel or build an engine (`dbctl
135
+ <conn> health`, `dbctl tunnel open`, an operation, …) — never at load time.
136
+ `dbctl connections list` / `dbctl connections show` never touch AWS and
137
+ never print a resolved secret; they show the raw `{{ssm:...}}` placeholder
138
+ as written in the YAML. Because resolution is per-connection and on-demand,
139
+ an unreachable profile on one connection never breaks loading or using any
140
+ other connection.
141
+
142
+ **Resolution details:**
143
+ - Parameters are always fetched `--with-decryption`, so `SecureString`
144
+ parameters work transparently; plain `String` parameters are unaffected.
145
+ - If `property` is set but the parameter's value isn't valid JSON, or the
146
+ key is missing, resolution fails with an error naming the parameter and
147
+ property — never the (partial) value.
148
+ - If the parameter doesn't exist or access is denied, the error names the
149
+ parameter but never attempts to print a value.
150
+ - Resolution shells out to the `aws` CLI (same mechanism `ssm` tunnels
151
+ already use for bastion lookups), so no extra AWS SDK dependency is
152
+ required — just a working `aws` CLI on `PATH` and credentials for the
153
+ chosen profile.
154
+
100
155
  ## `ssm` block
101
156
 
102
157
  AWS SSM port-forwarding to a remote host (typically RDS) through an EC2
@@ -282,7 +282,7 @@ cross-engine migration needs, without writing a custom script:
282
282
 
283
283
  `rstrip` exists specifically for the SQL Server → PostgreSQL case: SQL
284
284
  Server's default collation treats `'abc'` and `'abc '` as equal, so a
285
- column like `model_clean` can accumulate trailing padding that never
285
+ column like `notes` can accumulate trailing padding that never
286
286
  breaks a lookup on the source — until it's copied byte-exact into a
287
287
  target whose collation *is* whitespace-sensitive, at which point every
288
288
  padded row silently stops matching.
@@ -290,17 +290,17 @@ cross-engine migration needs, without writing a custom script:
290
290
  ```yaml
291
291
  operations:
292
292
  copy-lookup-tables:
293
- description: "Migrate DE-test lookup tables (SQL Server) → India Postgres"
293
+ description: "Migrate legacy lookup tables (SQL Server) -> Postgres"
294
294
  scope: multi
295
295
  mode: copy
296
296
  roles: [src, trg]
297
297
  copy_spec:
298
298
  batch_size: 10000
299
- tables: [ce_repair_cost_lookup_data, replacement_lookup_data]
299
+ tables: [orders, order_items]
300
300
  exclude_columns: [Id] # source IDENTITY column; trg generates its own
301
301
  transforms:
302
- model_clean: rstrip # trailing-space padding from src collation
303
- Manufacturer: rstrip
302
+ notes: rstrip # trailing-space padding from src collation
303
+ category: rstrip
304
304
  ```
305
305
 
306
306
  ```bash
@@ -329,12 +329,12 @@ dbctl copy-lookup-tables mssql pg --validate-data
329
329
 
330
330
  ```text
331
331
  pre-flight: scanning src rows against trg's column constraints; nothing will be written yet
332
- ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
333
- ┃ table ┃ column ┃ kind ┃ row ┃ detail ┃
334
- ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
335
- │ replacement_lookup_data │ Search_Manu │ not_null │ 482 │ NULL value for a NOT NULL │
336
- │ │ │ │ │ column │
337
- └────────────────────────────────┴─────────────┴──────────┴─────┴─────────────────────────────┘
332
+ ┏━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
333
+ ┃ table ┃ column ┃ kind ┃ row ┃ detail ┃
334
+ ┡━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
335
+ │ order_items │ region │ not_null │ 482 │ NULL value for a NOT NULL │
336
+ │ │ │ │ │ column │
337
+ └─────────────┴────────┴──────────┴─────┴─────────────────────────────┘
338
338
  1 violation(s) found — copy aborted, nothing was written
339
339
  ```
340
340
 
@@ -354,8 +354,8 @@ row range that reproduces the failure, and reports the driver's
354
354
  root-cause error instead of the full wrapped SQLAlchemy exception:
355
355
 
356
356
  ```text
357
- Error: table 'ce_repair_cost_lookup_data': insert failed at row 2847 of this batch:
358
- duplicate key value violates unique constraint "ce_repair_cost_lookup_data_pkey"
357
+ Error: table 'orders': insert failed at row 2847 of this batch:
358
+ duplicate key value violates unique constraint "orders_pkey"
359
359
  ```
360
360
 
361
361
  Disable it with `--no-diagnose-failures` (or `diagnose_failures: false` in
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dbctl"
7
- version = "0.7.1"
7
+ version = "0.7.2"
8
8
  description = "Generic CLI to monitor, control, and administer multiple databases via SSM, SSH, or direct connection."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -237,12 +237,12 @@ def test_copy_spec_accepts_exclude_columns_and_transforms():
237
237
  "copy_spec": {
238
238
  "tables": ["t"],
239
239
  "exclude_columns": ["Id"],
240
- "transforms": {"model_clean": "rstrip"},
240
+ "transforms": {"notes": "rstrip"},
241
241
  },
242
242
  }
243
243
  )
244
244
  assert op.copy_spec.exclude_columns == ["Id"]
245
- assert op.copy_spec.transforms == {"model_clean": ColumnTransform.rstrip_}
245
+ assert op.copy_spec.transforms == {"notes": ColumnTransform.rstrip_}
246
246
  assert op.copy_spec.diagnose_failures is True # default on
247
247
 
248
248
 
@@ -0,0 +1,292 @@
1
+ """Tests for `dbctl.refs` — `{{ssm:...}}` placeholder resolution against AWS
2
+ SSM Parameter Store.
3
+
4
+ `aws` CLI calls are mocked throughout (via `_fetch_parameter` or
5
+ `subprocess.run`) — no AWS credentials are needed to run this file. The
6
+ `resolve_connection` integration tests confirm the whole pipeline (raw YAML
7
+ -> validated `Connection` with placeholders intact -> resolved `Connection`
8
+ with real values) end to end, using a fake SSM backend.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import subprocess
15
+ from unittest.mock import patch
16
+
17
+ import pytest
18
+
19
+ from dbctl.refs import (
20
+ RefResolutionError,
21
+ _extract_property,
22
+ _fetch_parameter,
23
+ _parse_ref,
24
+ resolve_connection,
25
+ resolve_ssm_ref,
26
+ resolve_string,
27
+ resolve_value,
28
+ )
29
+
30
+
31
+ # --------------------------------------------------------------------------- #
32
+ # _parse_ref
33
+ # --------------------------------------------------------------------------- #
34
+ def test_parse_ref_name_only():
35
+ assert _parse_ref("/prod/db/password") == ("/prod/db/password", None, None)
36
+
37
+
38
+ def test_parse_ref_with_property():
39
+ assert _parse_ref("/prod/db/creds;property:password") == ("/prod/db/creds", "password", None)
40
+
41
+
42
+ def test_parse_ref_with_property_and_profile():
43
+ assert _parse_ref("/prod/db/creds;property:password;profile:prod-admin") == (
44
+ "/prod/db/creds",
45
+ "password",
46
+ "prod-admin",
47
+ )
48
+
49
+
50
+ def test_parse_ref_profile_only():
51
+ assert _parse_ref("/dev/db/password;profile:developer") == ("/dev/db/password", None, "developer")
52
+
53
+
54
+ def test_parse_ref_ignores_blank_segments():
55
+ # Trailing `;` (or accidental double `;;`) shouldn't blow up parsing.
56
+ assert _parse_ref("/dev/db/password;profile:developer;") == ("/dev/db/password", None, "developer")
57
+
58
+
59
+ def test_parse_ref_empty_name_rejected():
60
+ with pytest.raises(RefResolutionError, match="missing a parameter name"):
61
+ _parse_ref(";property:password")
62
+
63
+
64
+ def test_parse_ref_malformed_option_rejected():
65
+ with pytest.raises(RefResolutionError, match="malformed option"):
66
+ _parse_ref("/prod/db/password;profileprod-admin")
67
+
68
+
69
+ def test_parse_ref_unknown_option_rejected():
70
+ with pytest.raises(RefResolutionError, match="unknown option"):
71
+ _parse_ref("/prod/db/password;region:eu-west-1")
72
+
73
+
74
+ # --------------------------------------------------------------------------- #
75
+ # _extract_property
76
+ # --------------------------------------------------------------------------- #
77
+ def test_extract_property_happy_path():
78
+ raw = json.dumps({"username": "app_user", "password": "s3cret"})
79
+ assert _extract_property(raw, "password", "/prod/db/creds") == "s3cret"
80
+
81
+
82
+ def test_extract_property_not_json_raises_without_leaking_value():
83
+ with pytest.raises(RefResolutionError) as exc_info:
84
+ _extract_property("not-json-at-all", "password", "/prod/db/creds")
85
+ msg = str(exc_info.value)
86
+ assert "/prod/db/creds" in msg
87
+ assert "not-json-at-all" not in msg
88
+
89
+
90
+ def test_extract_property_missing_key_raises_without_leaking_value():
91
+ raw = json.dumps({"username": "app_user", "password": "s3cret"})
92
+ with pytest.raises(RefResolutionError) as exc_info:
93
+ _extract_property(raw, "host", "/prod/db/creds")
94
+ msg = str(exc_info.value)
95
+ assert "host" in msg
96
+ assert "s3cret" not in msg
97
+
98
+
99
+ # --------------------------------------------------------------------------- #
100
+ # _fetch_parameter (aws CLI mocked)
101
+ # --------------------------------------------------------------------------- #
102
+ def _fake_run(value: str, *, secure: bool = True):
103
+ def _run(cmd, **kwargs):
104
+ assert cmd[0] == "aws"
105
+ assert "ssm" in cmd and "get-parameter" in cmd
106
+ assert "--with-decryption" in cmd
107
+ stdout = json.dumps({"Parameter": {"Value": value, "Type": "SecureString" if secure else "String"}})
108
+ return subprocess.CompletedProcess(cmd, 0, stdout=stdout, stderr="")
109
+
110
+ return _run
111
+
112
+
113
+ def test_fetch_parameter_happy_path():
114
+ with patch("dbctl.refs.subprocess.run", side_effect=_fake_run("s3cret")):
115
+ assert _fetch_parameter("/prod/db/password", None) == "s3cret"
116
+
117
+
118
+ def test_fetch_parameter_uses_profile_flag():
119
+ captured_cmd = {}
120
+
121
+ def _run(cmd, **kwargs):
122
+ captured_cmd["cmd"] = cmd
123
+ return subprocess.CompletedProcess(
124
+ cmd, 0, stdout=json.dumps({"Parameter": {"Value": "s3cret"}}), stderr=""
125
+ )
126
+
127
+ with patch("dbctl.refs.subprocess.run", side_effect=_run):
128
+ _fetch_parameter("/prod/db/password", "prod-admin")
129
+ assert "--profile" in captured_cmd["cmd"]
130
+ assert "prod-admin" in captured_cmd["cmd"]
131
+
132
+
133
+ def test_fetch_parameter_aws_cli_missing():
134
+ with (
135
+ patch("dbctl.refs.subprocess.run", side_effect=FileNotFoundError),
136
+ pytest.raises(RefResolutionError, match="not found on PATH"),
137
+ ):
138
+ _fetch_parameter("/prod/db/password", None)
139
+
140
+
141
+ def test_fetch_parameter_denied_reports_param_name_not_value():
142
+ err = subprocess.CalledProcessError(
143
+ 254, ["aws"], stderr="An error occurred (AccessDeniedException): secret-looking-stderr-text"
144
+ )
145
+
146
+ with patch("dbctl.refs.subprocess.run", side_effect=err), pytest.raises(RefResolutionError) as exc_info:
147
+ _fetch_parameter("/prod/db/password", None)
148
+ msg = str(exc_info.value)
149
+ assert "/prod/db/password" in msg
150
+ assert "AccessDeniedException" in msg
151
+
152
+
153
+ def test_fetch_parameter_timeout():
154
+ with (
155
+ patch(
156
+ "dbctl.refs.subprocess.run",
157
+ side_effect=subprocess.TimeoutExpired(cmd=["aws"], timeout=15),
158
+ ),
159
+ pytest.raises(RefResolutionError, match="timed out"),
160
+ ):
161
+ _fetch_parameter("/prod/db/password", None)
162
+
163
+
164
+ def test_fetch_parameter_bad_json_response():
165
+ def _run(cmd, **kwargs):
166
+ return subprocess.CompletedProcess(cmd, 0, stdout="not json", stderr="")
167
+
168
+ with (
169
+ patch("dbctl.refs.subprocess.run", side_effect=_run),
170
+ pytest.raises(RefResolutionError, match="unexpected response"),
171
+ ):
172
+ _fetch_parameter("/prod/db/password", None)
173
+
174
+
175
+ # --------------------------------------------------------------------------- #
176
+ # resolve_ssm_ref / resolve_string / resolve_value (caching + composition)
177
+ # --------------------------------------------------------------------------- #
178
+ def test_resolve_ssm_ref_caches_same_spec():
179
+ calls = []
180
+
181
+ def _run(cmd, **kwargs):
182
+ calls.append(cmd)
183
+ stdout = json.dumps({"Parameter": {"Value": "s3cret"}})
184
+ return subprocess.CompletedProcess(cmd, 0, stdout=stdout, stderr="")
185
+
186
+ cache: dict[str, str] = {}
187
+ with patch("dbctl.refs.subprocess.run", side_effect=_run):
188
+ assert resolve_ssm_ref("/prod/db/password", cache=cache) == "s3cret"
189
+ assert resolve_ssm_ref("/prod/db/password", cache=cache) == "s3cret"
190
+ assert len(calls) == 1 # second call served from cache, no second `aws` invocation
191
+
192
+
193
+ def test_resolve_string_no_placeholder_is_noop_and_never_calls_aws():
194
+ with patch("dbctl.refs.subprocess.run") as mock_run:
195
+ assert resolve_string("plain-value", cache={}) == "plain-value"
196
+ mock_run.assert_not_called()
197
+
198
+
199
+ def test_resolve_string_single_placeholder():
200
+ with patch("dbctl.refs.subprocess.run", side_effect=_fake_run("s3cret")):
201
+ assert resolve_string("{{ssm:/prod/db/password}}", cache={}) == "s3cret"
202
+
203
+
204
+ def test_resolve_string_with_property_and_profile():
205
+ raw = json.dumps({"host": "prod.rds.internal", "password": "s3cret"})
206
+ with patch("dbctl.refs.subprocess.run", side_effect=_fake_run(raw)):
207
+ result = resolve_string("{{ssm:/prod/db/creds;property:host;profile:prod-admin}}", cache={})
208
+ assert result == "prod.rds.internal"
209
+
210
+
211
+ def test_resolve_string_embedded_in_larger_string():
212
+ with patch("dbctl.refs.subprocess.run", side_effect=_fake_run("prod-host")):
213
+ result = resolve_string("jdbc://{{ssm:/prod/db/host}}:5432/app", cache={})
214
+ assert result == "jdbc://prod-host:5432/app"
215
+
216
+
217
+ def test_resolve_value_recurses_dict_and_list():
218
+ with patch("dbctl.refs.subprocess.run", side_effect=_fake_run("resolved")):
219
+ result = resolve_value(
220
+ {"a": "{{ssm:/x}}", "b": ["plain", "{{ssm:/x}}"], "c": {"d": "{{ssm:/x}}"}, "e": 5, "f": None},
221
+ cache={},
222
+ )
223
+ assert result == {
224
+ "a": "resolved",
225
+ "b": ["plain", "resolved"],
226
+ "c": {"d": "resolved"},
227
+ "e": 5,
228
+ "f": None,
229
+ }
230
+
231
+
232
+ # --------------------------------------------------------------------------- #
233
+ # resolve_connection — full Connection round-trip
234
+ # --------------------------------------------------------------------------- #
235
+ def _connection(**overrides):
236
+ from dbctl.config import Connection
237
+
238
+ base = {
239
+ "type": "direct",
240
+ "driver": "postgresql+psycopg",
241
+ "database": "app",
242
+ "username": "app_admin",
243
+ "password": "plain-password",
244
+ "direct": {"host": "127.0.0.1", "port": 5432},
245
+ }
246
+ base.update(overrides)
247
+ return Connection.model_validate(base)
248
+
249
+
250
+ def test_resolve_connection_resolves_password_placeholder():
251
+ conn = _connection(password="{{ssm:/prod/db/password}}")
252
+ with patch("dbctl.refs.subprocess.run", side_effect=_fake_run("s3cret")):
253
+ resolved = resolve_connection(conn)
254
+ assert resolved.password == "s3cret"
255
+ # Original connection object (and its placeholder) is untouched.
256
+ assert conn.password == "{{ssm:/prod/db/password}}"
257
+
258
+
259
+ def test_resolve_connection_leaves_plain_values_untouched_and_skips_aws():
260
+ conn = _connection()
261
+ with patch("dbctl.refs.subprocess.run") as mock_run:
262
+ resolved = resolve_connection(conn)
263
+ mock_run.assert_not_called()
264
+ assert resolved.password == "plain-password"
265
+ assert resolved.username == "app_admin"
266
+
267
+
268
+ def test_resolve_connection_resolves_nested_tunnel_field():
269
+ conn = _connection(direct={"host": "{{ssm:/prod/db/host}}", "port": 5432})
270
+ with patch("dbctl.refs.subprocess.run", side_effect=_fake_run("prod.rds.internal")):
271
+ resolved = resolve_connection(conn)
272
+ assert resolved.direct.host == "prod.rds.internal"
273
+
274
+
275
+ def test_resolve_connection_property_extraction_from_shared_parameter():
276
+ raw = json.dumps({"username": "app_admin", "password": "s3cret", "host": "prod.rds.internal"})
277
+ conn = _connection(
278
+ password="{{ssm:/prod/db/creds;property:password}}",
279
+ direct={"host": "{{ssm:/prod/db/creds;property:host}}", "port": 5432},
280
+ )
281
+ with patch("dbctl.refs.subprocess.run", side_effect=_fake_run(raw)):
282
+ resolved = resolve_connection(conn)
283
+ assert resolved.password == "s3cret"
284
+ assert resolved.direct.host == "prod.rds.internal"
285
+
286
+
287
+ def test_resolve_connection_missing_parameter_error_names_param_not_value():
288
+ conn = _connection(password="{{ssm:/prod/db/password}}")
289
+ err = subprocess.CalledProcessError(254, ["aws"], stderr="ParameterNotFound: /prod/db/password")
290
+ with patch("dbctl.refs.subprocess.run", side_effect=err), pytest.raises(RefResolutionError) as exc_info:
291
+ resolve_connection(conn)
292
+ assert "/prod/db/password" in str(exc_info.value)
@@ -56,6 +56,91 @@ wheels = [
56
56
  { url = "https://files.pythonhosted.org/packages/66/40/c53deb2cd0c9b0fb636d24d9f40924cf2e65028e6b20b10cd5c1eeb2c730/ast_serialize-0.6.0-cp39-abi3-win_arm64.whl", hash = "sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e", size = 1072965, upload-time = "2026-06-30T20:02:54.097Z" },
57
57
  ]
58
58
 
59
+ [[package]]
60
+ name = "cffi"
61
+ version = "2.1.1"
62
+ source = { registry = "https://pypi.org/simple" }
63
+ dependencies = [
64
+ { name = "pycparser", marker = "implementation_name != 'PyPy'" },
65
+ ]
66
+ sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" }
67
+ wheels = [
68
+ { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" },
69
+ { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" },
70
+ { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" },
71
+ { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" },
72
+ { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" },
73
+ { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" },
74
+ { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" },
75
+ { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" },
76
+ { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" },
77
+ { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" },
78
+ { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" },
79
+ { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" },
80
+ { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" },
81
+ { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" },
82
+ { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" },
83
+ { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" },
84
+ { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" },
85
+ { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" },
86
+ { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" },
87
+ { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" },
88
+ { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" },
89
+ { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" },
90
+ { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" },
91
+ { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" },
92
+ { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" },
93
+ { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" },
94
+ { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" },
95
+ { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" },
96
+ { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" },
97
+ { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" },
98
+ { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" },
99
+ { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" },
100
+ { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" },
101
+ { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" },
102
+ { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" },
103
+ { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" },
104
+ { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" },
105
+ { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" },
106
+ { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" },
107
+ { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" },
108
+ { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" },
109
+ { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" },
110
+ { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" },
111
+ { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" },
112
+ { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" },
113
+ { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" },
114
+ { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" },
115
+ { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" },
116
+ { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" },
117
+ { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" },
118
+ { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" },
119
+ { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" },
120
+ { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" },
121
+ { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" },
122
+ { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" },
123
+ { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" },
124
+ { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" },
125
+ { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" },
126
+ { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" },
127
+ { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" },
128
+ { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" },
129
+ { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" },
130
+ { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" },
131
+ { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" },
132
+ { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" },
133
+ { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" },
134
+ { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" },
135
+ { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" },
136
+ { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" },
137
+ { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" },
138
+ { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" },
139
+ { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" },
140
+ { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" },
141
+ { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" },
142
+ ]
143
+
59
144
  [[package]]
60
145
  name = "click"
61
146
  version = "8.4.2"
@@ -146,12 +231,64 @@ wheels = [
146
231
  { url = "https://files.pythonhosted.org/packages/ec/82/32e3bd191d498e64f6f911ad55d14006a0861e54869d2d32452326399e65/coverage-7.15.2-py3-none-any.whl", hash = "sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c", size = 213375, upload-time = "2026-07-15T18:56:17.305Z" },
147
232
  ]
148
233
 
234
+ [[package]]
235
+ name = "cryptography"
236
+ version = "50.0.0"
237
+ source = { registry = "https://pypi.org/simple" }
238
+ dependencies = [
239
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
240
+ ]
241
+ sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" }
242
+ wheels = [
243
+ { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" },
244
+ { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" },
245
+ { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" },
246
+ { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" },
247
+ { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" },
248
+ { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" },
249
+ { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" },
250
+ { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" },
251
+ { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" },
252
+ { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" },
253
+ { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" },
254
+ { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" },
255
+ { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" },
256
+ { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" },
257
+ { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" },
258
+ { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" },
259
+ { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" },
260
+ { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" },
261
+ { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" },
262
+ { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" },
263
+ { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" },
264
+ { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" },
265
+ { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" },
266
+ { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" },
267
+ { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" },
268
+ { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" },
269
+ { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" },
270
+ { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" },
271
+ { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" },
272
+ { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" },
273
+ { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" },
274
+ { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" },
275
+ { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" },
276
+ { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" },
277
+ { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" },
278
+ { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" },
279
+ { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" },
280
+ { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" },
281
+ { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" },
282
+ ]
283
+
149
284
  [[package]]
150
285
  name = "dbctl"
151
- version = "0.6.0"
286
+ version = "0.7.2"
152
287
  source = { editable = "." }
153
288
  dependencies = [
154
289
  { name = "click" },
290
+ { name = "duckdb" },
291
+ { name = "oracledb" },
155
292
  { name = "psycopg", extra = ["binary"] },
156
293
  { name = "pydantic" },
157
294
  { name = "pymysql" },
@@ -172,7 +309,9 @@ dev = [
172
309
  [package.metadata]
173
310
  requires-dist = [
174
311
  { name = "click", specifier = ">=8.1" },
312
+ { name = "duckdb", specifier = ">=1" },
175
313
  { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.10" },
314
+ { name = "oracledb", specifier = ">=2" },
176
315
  { name = "psycopg", extras = ["binary"], specifier = ">=3.1" },
177
316
  { name = "pydantic", specifier = ">=2" },
178
317
  { name = "pymysql", specifier = ">=1.1" },
@@ -186,6 +325,35 @@ requires-dist = [
186
325
  ]
187
326
  provides-extras = ["dev"]
188
327
 
328
+ [[package]]
329
+ name = "duckdb"
330
+ version = "1.5.5"
331
+ source = { registry = "https://pypi.org/simple" }
332
+ sdist = { url = "https://files.pythonhosted.org/packages/7d/19/e57151753576373c6696a12022648546cca6038e8833fda2908ee2342d9b/duckdb-1.5.5.tar.gz", hash = "sha256:72f33ee57ca7595b23957671a2cc7f7fe2be0ecc2d68f63abedcfcaa3a5c1238", size = 18066741, upload-time = "2026-07-22T10:55:17.819Z" }
333
+ wheels = [
334
+ { url = "https://files.pythonhosted.org/packages/d6/40/2e05d324400fdaa5656c9f48d6298da421cb034d85e509fa0e6e325cf04b/duckdb-1.5.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d4dd65f8941a604b947e0b9b4b4f7165988e29a23ec0b69b4038520956d9933e", size = 32753858, upload-time = "2026-07-22T10:54:05.514Z" },
335
+ { url = "https://files.pythonhosted.org/packages/79/15/5ceb58ffb5bb8a62b3fd7abb39c41467cdf94850ece02e6d88664dfc75ce/duckdb-1.5.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33db46679b071f108d57139493dee2d37e1f5efcf5c5c039c2969eed11a6c8a7", size = 17368293, upload-time = "2026-07-22T10:54:09.139Z" },
336
+ { url = "https://files.pythonhosted.org/packages/bf/5c/bf02da0b354fe83cca4f95a4fbf762181af466f7d551ab2a093f7698882a/duckdb-1.5.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f0b88535a5d86fdd63dba6ea02ab68c003dfb9e4892b11256ef24c4da208baae", size = 15509131, upload-time = "2026-07-22T10:54:12.228Z" },
337
+ { url = "https://files.pythonhosted.org/packages/ea/a9/5f1f09da421d8e930e0b063d11c1b3f90363f40ede74438cd188afdd13a2/duckdb-1.5.5-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f316eae2323d9a851883fdf2dee91c1f9efe251ab33e14a2272f82a913422ed6", size = 19391959, upload-time = "2026-07-22T10:54:15.551Z" },
338
+ { url = "https://files.pythonhosted.org/packages/4f/98/6549769f158126fa64fd6c1ac2eb59a18282146c939867a3eb31b7c1db07/duckdb-1.5.5-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a6d2d11859d82a936ebdcb30ce3d8a1cbb3e990bff05c12abb9b54c44fa7bd1", size = 21510909, upload-time = "2026-07-22T10:54:19.681Z" },
339
+ { url = "https://files.pythonhosted.org/packages/af/b7/5753b41d3124838f868f9f523362812d9fc45409e9e4dd70dcbb0a25826e/duckdb-1.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:ddfbdb096c11d51ee22492397d342c90a82e62c5d09961477895934d0a25372f", size = 13168544, upload-time = "2026-07-22T10:54:22.789Z" },
340
+ { url = "https://files.pythonhosted.org/packages/5c/28/44b679c7d46245f8398feae7edac959d1b83d4eb143e25b3fce0630b78bd/duckdb-1.5.5-cp312-cp312-win_arm64.whl", hash = "sha256:2725d2b9ace3a4e75d72fc5a239f6a44b502c580edadb8fb2676db772c5f9282", size = 13988684, upload-time = "2026-07-22T10:54:26.003Z" },
341
+ { url = "https://files.pythonhosted.org/packages/47/37/4a38116e7700720fd152c666292214fd3abdf916496991296d8d1f66efbf/duckdb-1.5.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd98829b67788609017e65c761bd42a5dd0f9129441bed8bda4d6881ccf819f0", size = 32754294, upload-time = "2026-07-22T10:54:29.822Z" },
342
+ { url = "https://files.pythonhosted.org/packages/66/42/7d392f1ba1eee0eaf4ab4c8c7a604bfe3536cd63f979cf5c98798664f807/duckdb-1.5.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:feead93c56679b79592d437c62975d39cb67adedffa7592c763baf8160ac7366", size = 17368211, upload-time = "2026-07-22T10:54:33.359Z" },
343
+ { url = "https://files.pythonhosted.org/packages/9f/a5/0a6f4fa60562faa615e55e15bd1953a2f2b17a8edd8105e5cda215e43457/duckdb-1.5.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:49c963d9469373d7aba8d750d9ea565ab823e94166efed953f184dd9b169b98c", size = 15509136, upload-time = "2026-07-22T10:54:36.369Z" },
344
+ { url = "https://files.pythonhosted.org/packages/e4/cb/023c89f51978545b9fab318581bba0c457a58e7530d2d933e54ae7d8647c/duckdb-1.5.5-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a736217825461732b5442d05a220f3da2e23a0dae114efbf08c9bf171b53098a", size = 19392147, upload-time = "2026-07-22T10:54:39.551Z" },
345
+ { url = "https://files.pythonhosted.org/packages/3e/c5/41bef391fb8b23dbc133c9f2ba016e7a7a8124513d2cc1b430f1897d87e4/duckdb-1.5.5-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:078e6a60dd8eedde5832f45422ca5c4a6b8c837aeabd8a56ca0b7d933f588053", size = 21511060, upload-time = "2026-07-22T10:54:42.788Z" },
346
+ { url = "https://files.pythonhosted.org/packages/07/9f/c44dfc1f924ac29b3252dc1b91393c01d009dbfe9f8ed33f10b986151bd1/duckdb-1.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:6826504277dba513c0c5d71d828456c94d729c9d2482f94b2e289f90a9167e28", size = 13168028, upload-time = "2026-07-22T10:54:46.127Z" },
347
+ { url = "https://files.pythonhosted.org/packages/ca/88/591384b2cd59abddd6f5dc175e60374f9abae6064429f0c4402854c10f44/duckdb-1.5.5-cp313-cp313-win_arm64.whl", hash = "sha256:baa9c5702002fabb559ded2a39008f9f421fcbc7237d388b8213eff1e08858de", size = 13989955, upload-time = "2026-07-22T10:54:49.262Z" },
348
+ { url = "https://files.pythonhosted.org/packages/3e/56/12c65bfa2d2605b81981b264788891bcf11ec72227889554cead5d8d13b9/duckdb-1.5.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8e6413dd40facb7b8ab21bd844450cd8f549b29e138635be9cf090ef4d2049e2", size = 32761946, upload-time = "2026-07-22T10:54:53.412Z" },
349
+ { url = "https://files.pythonhosted.org/packages/b9/46/682ce155f17e0d2822d4f13ee3db9ca4b5b7c2da61b841b2629035e1f4bc/duckdb-1.5.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:64078acfd16541132ac6e191eb81b2845554444a0305cc1aa581ba107e514aa8", size = 17375069, upload-time = "2026-07-22T10:54:57.269Z" },
350
+ { url = "https://files.pythonhosted.org/packages/39/ce/a24bcbd3289c8f305a430759c5fc12242740b4af3e17f7593f3a34e333d2/duckdb-1.5.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8c11775cc99a447618d5f1840126db17f2652f3eae05529df4f81f40e2df7151", size = 15519791, upload-time = "2026-07-22T10:55:00.681Z" },
351
+ { url = "https://files.pythonhosted.org/packages/d9/76/3a01afbc615c1d418c0de58a6b68ac5ce2a8563232c0464bfbc2ce552398/duckdb-1.5.5-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77bbc1e6ba12e1e06f9020117bdf848627ecfdf36f907550e62e008e6109dece", size = 19398251, upload-time = "2026-07-22T10:55:04.168Z" },
352
+ { url = "https://files.pythonhosted.org/packages/a1/43/3a5e81d1728f4d234c79bfe385808ee7c04834f7c37a4b5c257459c25614/duckdb-1.5.5-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fbf0f2d48b43c6c304d00463b463c27ead6c4b01c3c1816b750f728decf71afe", size = 21513851, upload-time = "2026-07-22T10:55:07.864Z" },
353
+ { url = "https://files.pythonhosted.org/packages/91/41/fc7c829172c60ca22485251eab285f4f1a0d87b486a024c726f21471d86e/duckdb-1.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:9dc826c4b50e64f6c4e4d07a3a9cb075ef70ba3899dc43ec5493dc3d7b04b353", size = 13691858, upload-time = "2026-07-22T10:55:11.181Z" },
354
+ { url = "https://files.pythonhosted.org/packages/e1/2c/95d9216b79e9273689d7ebce125a54503ed0c9bd7da931f0265888e99779/duckdb-1.5.5-cp314-cp314-win_arm64.whl", hash = "sha256:63e48d4b74b15aeacd688976432a7225163df8c226eddeb8536bba2d4d4ff433", size = 14470180, upload-time = "2026-07-22T10:55:14.445Z" },
355
+ ]
356
+
189
357
  [[package]]
190
358
  name = "greenlet"
191
359
  version = "3.5.4"
@@ -387,6 +555,42 @@ wheels = [
387
555
  { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
388
556
  ]
389
557
 
558
+ [[package]]
559
+ name = "oracledb"
560
+ version = "4.0.2"
561
+ source = { registry = "https://pypi.org/simple" }
562
+ dependencies = [
563
+ { name = "cryptography" },
564
+ { name = "typing-extensions" },
565
+ ]
566
+ sdist = { url = "https://files.pythonhosted.org/packages/87/ae/4576e5df7b8eadec51bb7d981a2bcc0d8387d7d6998a51d146c0886a523e/oracledb-4.0.2.tar.gz", hash = "sha256:0a380ab72853487ea2764c5df772f35026b4219868fc3eba68e193c9aea230ac", size = 881658, upload-time = "2026-07-14T17:21:28.876Z" }
567
+ wheels = [
568
+ { url = "https://files.pythonhosted.org/packages/a5/89/2568c2d32afb3c0a66cef2e74dac7769f69e117a14402301b106970475dd/oracledb-4.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cbe3a75b463a334da9b3d76026062bcfb24ec563b7df291f700c9f7eefcbeefe", size = 4366002, upload-time = "2026-07-14T17:21:58.951Z" },
569
+ { url = "https://files.pythonhosted.org/packages/9c/c8/1825d240aa68b255eb78867c62964d2a69c6ae6197ab3543fe220539bf69/oracledb-4.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d11723018b6aeae035f4e1feae4150b7cca1161023c2d68d957d7310fe0dd56", size = 2286174, upload-time = "2026-07-14T17:22:00.649Z" },
570
+ { url = "https://files.pythonhosted.org/packages/10/31/5b9d6fa28942ff38e2d76dcef3184e7569d4b0006d0425f92a3d4a764dc4/oracledb-4.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:579f2c568433523a990cde5bea73c980d144754dc54d3ab2cd37efd670dc31d6", size = 2490198, upload-time = "2026-07-14T17:22:02.322Z" },
571
+ { url = "https://files.pythonhosted.org/packages/28/4e/77b21ec50c786270a7471abd6f48ec3c2e629ce304ec792eeeb6f03ba4d0/oracledb-4.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82d3d83bdc6246e53f8ad8b598d2508e9f5d983e352ce2e8a71a020200ba2d6", size = 2330058, upload-time = "2026-07-14T17:23:04.213Z" },
572
+ { url = "https://files.pythonhosted.org/packages/24/83/834e07805b8b3aaab7e87b818ef44ab3cb5394a73149a580ecf73cd92d4d/oracledb-4.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b4f2b51837248d4acfb323a64c48d89b62467b0f73d1211c99db9b80df0cdfbc", size = 2510815, upload-time = "2026-07-14T17:23:05.838Z" },
573
+ { url = "https://files.pythonhosted.org/packages/49/10/f05a3348a50ecab07b86317424751a4f63891e31c228a6d95f92b9538afd/oracledb-4.0.2-cp312-cp312-win32.whl", hash = "sha256:b5d203426f0d191842b4cfd87c223163ccc57f7e7875ec7ed073f163de2229af", size = 1488879, upload-time = "2026-07-14T17:23:07.53Z" },
574
+ { url = "https://files.pythonhosted.org/packages/85/e2/99fe3fa29466533df10fdfed90faee133aa1e7147b9edfc13b839e06f738/oracledb-4.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:5fe6e07ed29f84a6656e0580b4e662109d3bb90fc13d8f98ae3a56a67c75b80e", size = 1865771, upload-time = "2026-07-14T17:23:09.374Z" },
575
+ { url = "https://files.pythonhosted.org/packages/87/cb/009980df826442900419a7bc317e35dc85a031bc2d3806256d469de7d670/oracledb-4.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:a1f46b01a089e0e0dd44c4ac4c5c79903760976346c74a698955c1fb76d68bf5", size = 1519969, upload-time = "2026-07-14T17:23:11.531Z" },
576
+ { url = "https://files.pythonhosted.org/packages/2b/cb/e9ad24c2fa20ff977ca52b5a68a2010a054f724d420681be75e7e3d92b57/oracledb-4.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:af4121112f0b68e8d61ce46a086c6aec8256fa2ca32d2e6467195a6c575c207b", size = 4352202, upload-time = "2026-07-14T17:23:13.202Z" },
577
+ { url = "https://files.pythonhosted.org/packages/f6/ad/583d85906b5c4b344600be2bf30077f3a6c2bf04ee01b4fee9856d1da269/oracledb-4.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a814307ca8a6bf0649d8bf0a650a72f23030d9af052a2d4a481b94b6ab50d5a4", size = 2274847, upload-time = "2026-07-14T17:23:14.967Z" },
578
+ { url = "https://files.pythonhosted.org/packages/cf/53/badcc7e29ba9e9f2158f2e18214197d63c53d99c4720d436d8fc0b6b175f/oracledb-4.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b89c02670bafc9b1fcc0d15175f097bdc1968bc36ee4d66aa63aee09bfbbe30", size = 2474482, upload-time = "2026-07-14T17:23:16.605Z" },
579
+ { url = "https://files.pythonhosted.org/packages/b9/76/0bc64bdfc7ca794f4576c757da89617922be402f9fcf434bf5de7e44235a/oracledb-4.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:db0d76199b6dffd721bda48dad4eeea6603942ce2e246f3b9301d85af6bda0ee", size = 2327237, upload-time = "2026-07-14T17:23:18.602Z" },
580
+ { url = "https://files.pythonhosted.org/packages/15/f5/2fc4e30b24a60e40fa6419dbadd8f869878a3ac49d28b7d16bca59dd2519/oracledb-4.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f5ed684ab419603d53981e0f32b51da673e21a42d7dce1bf3f215a9301c7c108", size = 2504800, upload-time = "2026-07-14T17:23:20.025Z" },
581
+ { url = "https://files.pythonhosted.org/packages/2e/03/0a15a43a88addfb7b4f8d8343f0c47df68173510a27e2196b6b7347efb06/oracledb-4.0.2-cp313-cp313-win32.whl", hash = "sha256:a45a13e33509db5bd3622a05cb2e4a6cb41d56d1bfd2a32caeacd26f4ce8b988", size = 1493052, upload-time = "2026-07-14T17:23:21.603Z" },
582
+ { url = "https://files.pythonhosted.org/packages/bb/4a/9895cb5a1fffa68f2d9fb1cfb43de88628c049d313734de7ecef17099460/oracledb-4.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:6444be4991f33754cd98f624cecef3eb98db3e87f8ac14e9b65bd3591c9dd252", size = 1864126, upload-time = "2026-07-14T17:23:22.985Z" },
583
+ { url = "https://files.pythonhosted.org/packages/0b/14/5c7c44a8f441783d461b9657994e03f8798e28bebe67395951c53c86c270/oracledb-4.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:087fdf5bc36b03dc3c55f7abc944a163ba8edc9c802bc8baf91698a52041ee13", size = 1519906, upload-time = "2026-07-14T17:23:24.338Z" },
584
+ { url = "https://files.pythonhosted.org/packages/d8/96/30cdbc8399f8edd5cd8bc28bae89fba0eaa40c529168360e9aa84fa21e97/oracledb-4.0.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:58e71a3c1e294a99e39b086adf42074d48287548bf8edfa720c9507106186bbb", size = 4389290, upload-time = "2026-07-14T17:23:25.769Z" },
585
+ { url = "https://files.pythonhosted.org/packages/1f/35/eec834f01d84f6974ff9b397b83cc80b67064410476049aaf9f88269590d/oracledb-4.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78d4a1cc1482b5927a9962c7766aeeae616a85eedbe49af8b797148ac1c1ac32", size = 2315809, upload-time = "2026-07-14T17:23:27.265Z" },
586
+ { url = "https://files.pythonhosted.org/packages/fd/09/5185ea1270987fa3514ff6bf99540e23befefdb511759137f57848e1ab72/oracledb-4.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14124b5e8e4ba087f105ee389ee43e407840220a0ba2504de68eed903f51a711", size = 2490090, upload-time = "2026-07-14T17:23:28.995Z" },
587
+ { url = "https://files.pythonhosted.org/packages/c2/63/2547b13274f95d2eb073af2c936a37d7da3025f910937dc2e411b7160784/oracledb-4.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:36e72ffaaf589041dac72ddf589d694c0a9ac4576662a04628966d4ea089d6ea", size = 2365819, upload-time = "2026-07-14T17:23:30.691Z" },
588
+ { url = "https://files.pythonhosted.org/packages/23/bd/f52b69a5d86d62cf9cb3992a7cafd1d46c418ae64eaa8a256a3d1eee9e51/oracledb-4.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0101b88c15ae628af556506840ae876e12bdb75506ef342e4990ac1758bad1eb", size = 2518689, upload-time = "2026-07-14T17:23:32.566Z" },
589
+ { url = "https://files.pythonhosted.org/packages/2e/c5/6d15fedcbd67fabd6c9c345554e184ba6617e389bfea348358f8aacc0e5f/oracledb-4.0.2-cp314-cp314-win32.whl", hash = "sha256:b14d52c09b3c7a8273b3640ba06675acec5000dab0356ef288587c46c30bbbb4", size = 1514946, upload-time = "2026-07-14T17:23:33.981Z" },
590
+ { url = "https://files.pythonhosted.org/packages/2d/b9/d6e2c3ee3d9fb3585f04910ecf6ff91dbfd84e8dafc06084717f6a134bca/oracledb-4.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:5c1f13ba535d9f0fc61e1987720988612cf9424667d491fc344ba681f3336a17", size = 1915171, upload-time = "2026-07-14T17:23:35.574Z" },
591
+ { url = "https://files.pythonhosted.org/packages/0d/6b/47f5c76223798ebfb5b6d41e9c3a451d296d6d90123584d85159e4236ff5/oracledb-4.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:93b4d2cd17a93224711fadd28f77e956e5c6575cd923f24bfe0b1a2a581beb79", size = 1574652, upload-time = "2026-07-14T17:23:36.982Z" },
592
+ ]
593
+
390
594
  [[package]]
391
595
  name = "packaging"
392
596
  version = "26.2"
@@ -472,6 +676,15 @@ wheels = [
472
676
  { url = "https://files.pythonhosted.org/packages/eb/e6/5fff07a70d1f945ed90ae131c3bd76cab32beff7c58c6db15ad5820b6d1f/psycopg_binary-3.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:c37e024c07308cd06cf3ec51bfd0e7f6157585a4d84d1bce4a7f5f7913719bf8", size = 3666849, upload-time = "2026-05-01T23:31:51.165Z" },
473
677
  ]
474
678
 
679
+ [[package]]
680
+ name = "pycparser"
681
+ version = "3.0"
682
+ source = { registry = "https://pypi.org/simple" }
683
+ sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
684
+ wheels = [
685
+ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
686
+ ]
687
+
475
688
  [[package]]
476
689
  name = "pydantic"
477
690
  version = "2.13.4"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes