3tears-datasources 0.14.0__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 (59) hide show
  1. 3tears_datasources-0.14.0/.gitignore +216 -0
  2. 3tears_datasources-0.14.0/CHANGELOG.md +392 -0
  3. 3tears_datasources-0.14.0/IMPLEMENTING_DRIVERS.md +397 -0
  4. 3tears_datasources-0.14.0/LICENSE +21 -0
  5. 3tears_datasources-0.14.0/PKG-INFO +105 -0
  6. 3tears_datasources-0.14.0/README.md +70 -0
  7. 3tears_datasources-0.14.0/pyproject.toml +86 -0
  8. 3tears_datasources-0.14.0/src/threetears/datasources/__init__.py +119 -0
  9. 3tears_datasources-0.14.0/src/threetears/datasources/collections.py +1176 -0
  10. 3tears_datasources-0.14.0/src/threetears/datasources/config.py +662 -0
  11. 3tears_datasources-0.14.0/src/threetears/datasources/drivers/__init__.py +38 -0
  12. 3tears_datasources-0.14.0/src/threetears/datasources/drivers/_sync_bridge.py +176 -0
  13. 3tears_datasources-0.14.0/src/threetears/datasources/drivers/_util.py +232 -0
  14. 3tears_datasources-0.14.0/src/threetears/datasources/drivers/asyncpg_driver.py +738 -0
  15. 3tears_datasources-0.14.0/src/threetears/datasources/drivers/base.py +721 -0
  16. 3tears_datasources-0.14.0/src/threetears/datasources/drivers/bigquery_driver.py +278 -0
  17. 3tears_datasources-0.14.0/src/threetears/datasources/drivers/factory.py +128 -0
  18. 3tears_datasources-0.14.0/src/threetears/datasources/drivers/redshift_driver.py +1605 -0
  19. 3tears_datasources-0.14.0/src/threetears/datasources/drivers/snowflake_driver.py +241 -0
  20. 3tears_datasources-0.14.0/src/threetears/datasources/entities.py +275 -0
  21. 3tears_datasources-0.14.0/src/threetears/datasources/introspection.py +220 -0
  22. 3tears_datasources-0.14.0/src/threetears/datasources/namespace.py +55 -0
  23. 3tears_datasources-0.14.0/src/threetears/datasources/py.typed +0 -0
  24. 3tears_datasources-0.14.0/src/threetears/datasources/schema_priming.py +165 -0
  25. 3tears_datasources-0.14.0/src/threetears/datasources/secrets.py +30 -0
  26. 3tears_datasources-0.14.0/tests/__init__.py +0 -0
  27. 3tears_datasources-0.14.0/tests/enforcement/__init__.py +0 -0
  28. 3tears_datasources-0.14.0/tests/enforcement/test_no_hardcoded_pool_params.py +130 -0
  29. 3tears_datasources-0.14.0/tests/enforcement/test_secrets_typed.py +204 -0
  30. 3tears_datasources-0.14.0/tests/integration/__init__.py +0 -0
  31. 3tears_datasources-0.14.0/tests/integration/conftest.py +11 -0
  32. 3tears_datasources-0.14.0/tests/integration/test_asyncpg_driver_live.py +589 -0
  33. 3tears_datasources-0.14.0/tests/integration/test_redshift_driver_live.py +538 -0
  34. 3tears_datasources-0.14.0/tests/unit/__init__.py +0 -0
  35. 3tears_datasources-0.14.0/tests/unit/_helpers/__init__.py +0 -0
  36. 3tears_datasources-0.14.0/tests/unit/_helpers/cancellation_contract.py +98 -0
  37. 3tears_datasources-0.14.0/tests/unit/_helpers/fake_driver.py +141 -0
  38. 3tears_datasources-0.14.0/tests/unit/test_asyncpg_driver.py +632 -0
  39. 3tears_datasources-0.14.0/tests/unit/test_asyncpg_driver_secret_leak.py +98 -0
  40. 3tears_datasources-0.14.0/tests/unit/test_bigquery_driver_stub.py +116 -0
  41. 3tears_datasources-0.14.0/tests/unit/test_collections.py +628 -0
  42. 3tears_datasources-0.14.0/tests/unit/test_config.py +627 -0
  43. 3tears_datasources-0.14.0/tests/unit/test_driver_abc.py +235 -0
  44. 3tears_datasources-0.14.0/tests/unit/test_driver_coverage.py +144 -0
  45. 3tears_datasources-0.14.0/tests/unit/test_driver_coverage_by_dimension.py +170 -0
  46. 3tears_datasources-0.14.0/tests/unit/test_entities.py +143 -0
  47. 3tears_datasources-0.14.0/tests/unit/test_factory.py +323 -0
  48. 3tears_datasources-0.14.0/tests/unit/test_introspection.py +305 -0
  49. 3tears_datasources-0.14.0/tests/unit/test_lazy_imports.py +54 -0
  50. 3tears_datasources-0.14.0/tests/unit/test_namespace.py +51 -0
  51. 3tears_datasources-0.14.0/tests/unit/test_redshift_driver.py +1177 -0
  52. 3tears_datasources-0.14.0/tests/unit/test_redshift_driver_concurrency.py +227 -0
  53. 3tears_datasources-0.14.0/tests/unit/test_redshift_driver_secret_leak.py +103 -0
  54. 3tears_datasources-0.14.0/tests/unit/test_schema_digest_l1_roundtrip.py +155 -0
  55. 3tears_datasources-0.14.0/tests/unit/test_schema_priming_integration.py +143 -0
  56. 3tears_datasources-0.14.0/tests/unit/test_secrets.py +182 -0
  57. 3tears_datasources-0.14.0/tests/unit/test_snowflake_driver_stub.py +139 -0
  58. 3tears_datasources-0.14.0/tests/unit/test_sync_bridge.py +231 -0
  59. 3tears_datasources-0.14.0/tests/unit/test_translate_placeholders.py +139 -0
@@ -0,0 +1,216 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # Ruff stuff:
192
+ .ruff_cache/
193
+
194
+ # PyPI configuration file
195
+ .pypirc
196
+
197
+ # Cursor
198
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
+ # refer to https://docs.cursor.com/context/ignore-files
201
+ .cursorignore
202
+ .cursorindexingignore
203
+
204
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
208
+
209
+ # Claude Code local state
210
+ .claude/
211
+
212
+ # prawduct session evidence (local governance artifacts, never shipped)
213
+ .prawduct/
214
+
215
+ # macOS folder metadata
216
+ .DS_Store
@@ -0,0 +1,392 @@
1
+ # Changelog
2
+
3
+ All notable changes to `3tears-datasources` are documented here.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and the package version moves in **lockstep** with the rest of the
7
+ 3tears monorepo (every package tracks the framework git tag; see
8
+ `README.md` "Versioning policy").
9
+
10
+ ## [0.13.9]
11
+
12
+ ### Added
13
+
14
+ - `RedshiftConnectionConfig.sslmode` (`verify-ca` | `verify-full`, default `verify-ca`)
15
+ — the TLS verification mode passed to `redshift_connector.connect`, now configurable
16
+ per datasource instead of hardcoded to the library default. A Redshift cluster fronted
17
+ by a TLS-terminating proxy (e.g. Satori) can require `verify-full` to complete the
18
+ handshake — `verify-ca` fails there mid-handshake with a broken pipe. The default
19
+ preserves the prior behavior, so existing callers are unaffected.
20
+
21
+ ## [0.13.8]
22
+
23
+ ### Fixed
24
+
25
+ - `RedshiftDriver` now terminates the **server-side** query on cancel. It captures
26
+ each connection's `pg_backend_pid()` at open and, on cancel, issues
27
+ `pg_terminate_backend(<pid>)` from a fresh short-lived connection before
28
+ closing/evicting the connection. Closing the client socket alone did not stop
29
+ the running Redshift query — a real abandoned query ran for 7.4h, leaking a
30
+ pool slot. Best-effort and non-fatal: the pid read and the terminate never raise
31
+ and never block the existing close + evict path.
32
+
33
+ ## [0.13.3]
34
+
35
+ ### Added
36
+
37
+ - `Driver.column_value_coverage_by_dimension(schema, table, dimension_column, columns)` — the
38
+ grouped sibling of `column_value_coverage`: a single `GROUP BY` pass reporting non-null/non-zero
39
+ value coverage per numeric column per dimension value, surfacing the partial-coverage case (a
40
+ column loaded for some dimension values, all-zero for others) the whole-table probe can't see.
41
+ Concrete on the `Driver` ABC (portable SQL routed through `fetch`).
42
+
43
+ ## [0.13.2]
44
+
45
+ ### Changed
46
+
47
+ - Cap simultaneously-open Redshift connections with an `asyncio.Semaphore` sized to
48
+ `connection_cache_size`, acquired before opening — so a burst of concurrent `fetch()` can no
49
+ longer open connections past the warehouse user's `CONNECTION LIMIT` (the 0.13.1 cache bound
50
+ alone didn't gate concurrent opens). The executor still bounds concurrent work.
51
+
52
+ ## [0.13.1]
53
+
54
+ ### Fixed
55
+
56
+ - Redshift warm-connection cache sized as a bounded pool. `executor_max_workers`
57
+ default lowered 10 -> 5, and `connection_cache_size` defaults to
58
+ `executor_max_workers` (cache == workers) via a model validator, so concurrent
59
+ queries reuse warm connections instead of opening past the cache every time —
60
+ which overshot a tight per-user Redshift CONNECTION LIMIT ("too many
61
+ connections"). A query past the bound queues on the executor. Set both per
62
+ datasource to the user's connection limit.
63
+
64
+ ## [0.13.0]
65
+
66
+ ### Changed
67
+
68
+ - Collection store-tier override methods renamed to the storage-agnostic
69
+ L3-store-seam names (`fetch_from_postgres` -> `fetch_from_store`,
70
+ `save_to_postgres` -> `save_to_store`, `delete_from_postgres` ->
71
+ `delete_from_store`), tracking the core `collections-task-06` rename. The
72
+ schema-digest and column collections implement the new names; behavior is
73
+ unchanged.
74
+
75
+ ## [0.12.3]
76
+
77
+ ### Added
78
+
79
+ - Per-column value-coverage probe: classifies a column as unloaded when every
80
+ value is zero across the table (the `UNLOADED_COLUMN` source the hub mirrors
81
+ into datasource read results), with driver-coverage tests.
82
+
83
+ ### Fixed
84
+
85
+ - Redshift: re-apply `search_path` on every connection acquisition, so a pooled
86
+ connection never serves a stale path left by a prior caller.
87
+
88
+ ### Changed
89
+
90
+ - Centralize JSONB handling through native binding under the codec
91
+ (`collections-task-04`, Option B), simplifying the collection write path; an
92
+ enforcement drift guard prevents a new column bypassing it.
93
+
94
+ ## [0.12.2]
95
+
96
+ ### Added
97
+
98
+ - `DataSourceSchemaDigest` entity + `DataSourceSchemaDigestCollection` — a
99
+ three-tier collection for the materialized documented-schema digest, one row
100
+ per datasource, addressed by primary key `datasource_id` for a by-pk hot-L1
101
+ read with L2/L3 fallback and cross-pod invalidation. The `tables` projection
102
+ is stored as JSONB.
103
+
104
+ ### Fixed
105
+
106
+ - JSONB write double-encode in the digest tables: a pre-`json.dumps`'d string
107
+ bound as `::jsonb` was re-encoded to a scalar by the text-format jsonb codec;
108
+ writes now text-cast (`::text::jsonb`). Covered by a real-L1 round-trip test.
109
+
110
+ ## [0.11.0]
111
+
112
+ ### Added
113
+
114
+ - Platform-sharing primitives: a flat datasource primary key, a
115
+ `visibility` field, and `origin_datasource_id` lineage so a customer
116
+ datasource can inherit a platform-shared datasource's schema docs and
117
+ governed knowledge (concepts / entries) rather than re-documenting them.
118
+
119
+ ## [0.10.2]
120
+
121
+ ### Added
122
+
123
+ - New `allowed_schemas: list[str]` field on
124
+ `RedshiftConnectionConfig`, `PostgresConnectionConfig`, and
125
+ `YugabyteConnectionConfig`. Drivers thread the value onto the
126
+ connection's `search_path` at open time so agents can write
127
+ unqualified table names (`SELECT … FROM report_geofacts_joined_data`)
128
+ instead of having to fully qualify every reference. Empty default
129
+ preserves the backend's default `search_path`.
130
+ - `build_search_path_value` and `build_set_search_path_sql`
131
+ helpers in `threetears.datasources.drivers._util` with
132
+ identifier-quoting suitable for adversarial schema names.
133
+ - Redshift driver issues `SET search_path TO "<schemas>"` via
134
+ `cursor.execute` after the existing `SET statement_timeout`
135
+ block on every connection open. Cached connection lifecycle
136
+ applies the SET once per backend session.
137
+ - asyncpg driver passes `server_settings={"search_path": "..."}`
138
+ through `create_pool`. The value rides the pgwire STARTUP
139
+ packet and is preserved across `DISCARD ALL` (the default
140
+ asyncpg pool-release reset), where an `init=` callback would
141
+ have been wiped. The live testcontainer pass caught the reset
142
+ issue before release.
143
+ - 8 new unit tests (4 Redshift, 4 asyncpg) pin the SQL / kwargs
144
+ both drivers ship to the client library, including the
145
+ identifier-quoting paths. 4 new live integration tests (2
146
+ Redshift against `central-reporting`, 2 asyncpg against the
147
+ testcontainer) prove unqualified-table-name resolution
148
+ end-to-end and confirm empty `allowed_schemas` leaves the
149
+ default `search_path` intact.
150
+
151
+ ## [0.10.1]
152
+
153
+ ### Fixed
154
+
155
+ - `RedshiftDriver` now runs `ROLLBACK` on a query error before
156
+ returning the connection to its cache. `redshift_connector` uses
157
+ the DB-API default of `autocommit=False`, so a failed statement
158
+ leaves the connection's implicit transaction in `aborted` state;
159
+ without the rollback the next caller to acquire that cached
160
+ connection trips `25P02: current transaction is aborted, commands
161
+ ignored until end of transaction block` on every subsequent
162
+ statement until eviction. The fix lives in `_acquire_and_run`, the
163
+ central wrapper every query method routes through (so `fetch`,
164
+ `execute`, and `fetch_iter` all benefit). If the rollback itself
165
+ raises, the connection is evicted instead of released and a
166
+ WARNING is logged; the ORIGINAL query exception is what propagates
167
+ to the caller. Covered by `TestRollbackOnError` unit tests
168
+ (mocked-cursor positive / failure / two-fetch end-to-end shapes)
169
+ and one live integration test against `central-reporting`.
170
+
171
+ ## [0.9.1]
172
+
173
+ ### Changed
174
+
175
+ - Credentials are now referenced by a `scheme://locator` string
176
+ (`password_ref` / `credentials_json_ref`) instead of an env-var
177
+ name (`password_env` / `credentials_json_env`). The value is
178
+ resolved at use time by a pluggable backend in
179
+ `threetears.datasources.secrets`. Shipped backends: `env://NAME`
180
+ (process env) and `k8s://rel/path` (projected-Secret file under
181
+ `AIBOTS_DATASOURCE_SECRETS_DIR`). `vault://`, `aws-secretsmanager://`
182
+ and `gcp-sm://` are registered but raise until implemented.
183
+ - Package version realigned to the monorepo lockstep (`0.9.1`); the
184
+ earlier independent-SemVer experiment (`0.1.x`) is retired.
185
+
186
+ ## Roadmap
187
+
188
+ Future enhancements after the initial driver migration ships:
189
+
190
+ - Snowflake + BigQuery concrete driver implementations (stubs today
191
+ per shard 12 — the ABC supports both shapes; implementations land
192
+ when a consumer needs them).
193
+ - `Collection.save_entities_batched` to restore the per-batch write
194
+ performance that shard-13's per-row `save_entity` flow gave up
195
+ (preserving the L2-invalidation contract was non-negotiable; the
196
+ batched variant ships as a 3tears-core enhancement when profiling
197
+ data justifies it).
198
+ - `Driver.execute` return-value extension (currently `None`;
199
+ callers that need a row count use `RETURNING *` + the read tool).
200
+ - Lift the Hub-side `introspect_if_changed` orchestrator helpers
201
+ (probe + diff coordination) into `threetears.datasources.introspection`
202
+ when a second 3tears consumer wires up a change-driven introspector.
203
+
204
+ ## [0.1.0] — initial release
205
+
206
+ initial release shipping the full datasource driver-abstraction
207
+ migration (shards 07–15 in the `14-eng-ai-bot/docs/datasource-task-*.md`
208
+ series). drops in as the canonical home for datasource primitives
209
+ across every 3tears consumer.
210
+
211
+ ### Driver layer (shards 09–12)
212
+
213
+ - `threetears.datasources.drivers.Driver` ABC: the contract every
214
+ concrete driver implements. async-only surface (`fetch`,
215
+ `execute`, `fetch_iter`, `list_tables`, `list_columns`,
216
+ `table_hashes`, `test_connection`, `close`). `fetch_iter` carries
217
+ a working default impl (yields from `fetch`); native streaming
218
+ is the concrete-driver responsibility.
219
+ - `threetears.datasources.drivers.create_driver(config, *, hub_l3_pool=None, datasource_name="unknown")`:
220
+ the factory. dispatches on `config.datasource_type` to the right
221
+ concrete driver class. lazy backend imports — importing the
222
+ `threetears.datasources` package roots does NOT pull
223
+ `redshift_connector` / `snowflake-connector-python` /
224
+ `google-cloud-bigquery`. one runtime audit verifies the contract
225
+ on every test run.
226
+ - Shared helpers (`drivers/base.py`, `_util.py`, `_sync_bridge.py`):
227
+ - `Driver._with_cancellation(coro_fn, cancel_callback=...)` — the
228
+ canonical pattern every concrete driver routes through for
229
+ `CancelledError` propagation to the backend cancel hook.
230
+ - `_translate_placeholders(sql, target_style)` — `$1` ->
231
+ `%s`/`:1`/`@p1`. asyncpg style is a no-op. handles `$10` vs
232
+ `$1`, escaped `$$`, string-literal `'$1'`.
233
+ - `AsyncSyncBridge(max_workers, name)` — bounded
234
+ `ThreadPoolExecutor` + cancel-aware `to_thread_with_cancel`.
235
+ Redshift / Snowflake / BigQuery drivers share one
236
+ implementation rather than three drifting copies.
237
+ - `@_observed(driver_type=...)` decorator — auto-emits OTel
238
+ `datasource.driver.query.duration` (histogram) +
239
+ `datasource.driver.error` (counter) on every wrapped method.
240
+ - `AsyncpgDriver` (shard 10) — covers POSTGRES / YUGABYTE /
241
+ AGENT_INTERNAL. server-side cursor streaming via
242
+ `conn.cursor()` inside `conn.transaction()`. AGENT_INTERNAL
243
+ borrows Hub's L3 pool via `external_pool=`. cancellation uses
244
+ `Connection.cancel()` (NOT `terminate()`) so the connection
245
+ returns to the pool clean.
246
+ - `RedshiftDriver` (shard 11) — covers REDSHIFT via
247
+ `redshift-connector` (AWS's official driver; `asyncpg` against
248
+ Redshift's pg-protocol quirks never completed in production).
249
+ bounded connection cache (`connection_cache_size`) amortises the
250
+ ~1-3s TLS+auth handshake. `weakref.finalize` registers GC-time
251
+ cache drain for pod-crash mitigation. cancellation uses
252
+ `Connection.close()` wrapped in `asyncio.wait_for(_, 5.0)` (the
253
+ lib has no `cancel()` API; closing the connection is the only
254
+ in-flight-abort primitive). live integration test against the
255
+ `central-reporting` warehouse is CI-required.
256
+ - `SnowflakeDriver` + `BigQueryDriver` (shard 12) — stub
257
+ implementations. every abstract method raises
258
+ `NotImplementedError` with a roadmap-pointing message; module
259
+ docstrings codify the future implementation shape so the next
260
+ contributor reuses `AsyncSyncBridge` / `_with_cancellation` /
261
+ `_translate_placeholders` instead of reinventing.
262
+
263
+ ### Configuration layer (shards 07–08)
264
+
265
+ - `threetears.datasources.config.DatasourceConfig`: agent.yaml-facing
266
+ config. carries `name`, `access_mode`, `schemas`, and a nested
267
+ `connection_config: ConnectionConfig` field.
268
+ - `threetears.datasources.config.ConnectionConfig`: discriminated
269
+ union (Pydantic `Annotated[Union[...], Field(discriminator=
270
+ "datasource_type")]`) routing to per-backend members:
271
+ `PostgresConnectionConfig`, `YugabyteConnectionConfig`,
272
+ `RedshiftConnectionConfig`, `SnowflakeConnectionConfig`,
273
+ `BigQueryConnectionConfig`, `AgentInternalConnectionConfig`.
274
+ - Each member declares pool / executor / timeout knobs as Pydantic
275
+ fields with documented defaults. The enforcement test
276
+ `tests/enforcement/test_no_hardcoded_pool_params.py` fails the
277
+ build on any concrete driver that inlines a banned-kwarg literal
278
+ (`min_size`, `max_size`, `command_timeout`, `connection_cache_size`,
279
+ `executor_max_workers`, ...).
280
+ - Passwords pass as env-var NAMES (`password_env: str`), NEVER
281
+ resolved values. `resolve_password() -> SecretStr` is the only
282
+ way to read the secret; drivers unwrap `.get_secret_value()`
283
+ at the last moment when handing to the backend lib. Exception
284
+ sanitization (`raise X from None`) breaks the cause chain so
285
+ backend errors don't surface the password.
286
+
287
+ ### Entity + collection + namespace layer (shard 07)
288
+
289
+ - `threetears.datasources.entities` — `DataSourceEntity`,
290
+ `DataSourceTableEntity`, `DataSourceColumnEntity`,
291
+ `DataSourceRelationEntity`, `TableTemplateEntity` + the
292
+ `DataSourceType`, `DataSourceAccessMode`, `DataSourceStatus`
293
+ enums. All entities subclass `threetears.core.entities.base.BaseEntity`
294
+ and preserve the composite-PK / single-PK shape from their
295
+ Hub origins byte-for-byte.
296
+ - `threetears.datasources.collections` — `DataSourceCollection`
297
+ (`SchemaBackedCollection` subclass), `DataSourceTableCollection`,
298
+ `DataSourceColumnCollection`, `DataSourceRelationCollection`,
299
+ `TableTemplateCollection`. `get_by_natural_key(...)` on the
300
+ table + column collections supports the introspector's "insert
301
+ vs update" decision. `DataSourceTableCollection` carries
302
+ `column_hash` natively (the Tier-2 probe digest).
303
+ - `threetears.datasources.namespace` — `DATASOURCE_NAMESPACE_TYPE`,
304
+ `datasource_namespace_id`, `datasource_namespace_name` helpers.
305
+
306
+ ### Introspection helpers (shard 02 + 03, lifted from Hub)
307
+
308
+ - `threetears.datasources.introspection.compute_column_hash(cols) -> str`:
309
+ Python-side cross-language Tier-2 hash. byte-identical to the
310
+ driver-side SQL `MD5(STRING_AGG(column_name || ':' || data_type
311
+ || ':' || COALESCE(is_nullable, ''), ',' ORDER BY ordinal_position))`.
312
+ - `IntrospectionDiff` (frozen dataclass) — carries work lists
313
+ (`tables_to_introspect`, `tables_to_delete`) + summary counts
314
+ (`tables_checked`, `tables_unchanged`, `tables_changed`,
315
+ `tables_added`, `tables_removed`, `columns_*`, `elapsed_ms`).
316
+ `has_changes` property short-circuits the orchestrator when
317
+ there's no work to do.
318
+ - `compute_introspection_diff(warehouse_hashes, stored_hashes)`:
319
+ pure function. classifies every key into the right work list +
320
+ counter bucket. null stored hashes are forced re-introspect
321
+ (matches the migration-backfill sentinel semantics).
322
+
323
+ ### Companion 3tears-core promotion (shard 07)
324
+
325
+ - `threetears.core.utils.pg_pool_kwargs` — promoted from Hub. the
326
+ shared `asyncpg.create_pool` kwargs helper, DSN redactor, and
327
+ startup-timeout wrapper. previously lived in Hub's
328
+ `aibots/hub/common/pg_pool.py`; shard 15 deletes the Hub copy
329
+ entirely. consumers import directly from the core helper.
330
+
331
+ ### Out of scope (intentionally, for 0.1.0)
332
+
333
+ - Hub admin API DTOs (`DataSourceCreateRequest`, `DataSourceResponse`,
334
+ etc.) — those are Hub-API contracts, not framework primitives;
335
+ they stay in Hub as `aibots/hub/datasources/hub_api.py`.
336
+ - Per-table ACL via the `datasource_table` namespace — already
337
+ lives in Hub's template routes; separate concern.
338
+ - Snowflake + BigQuery concrete driver implementations (stubs only
339
+ today per shard 12 — see Unreleased section).
340
+
341
+ ### Migration notes
342
+
343
+ `3tears-datasources` is a brand-new package; no prior version exists.
344
+ Hub + agent-SDK consumers flip imports from the old Hub paths to
345
+ `threetears.datasources.*` in the same migration PR (shards 07 + 08).
346
+ No backward-compat shims are provided.
347
+
348
+ ### Added
349
+
350
+ - `threetears.datasources` package home for the datasource entity +
351
+ collection + namespace + config primitives previously inlined in
352
+ Hub (`aibots/hub/datasources/{entities,collections,schema_collections,namespace,schemas}.py`)
353
+ and the agent SDK (`aibots_agents/devx/schema/agent_config.py:DatasourceConfig`).
354
+ - `threetears.datasources.entities` — `DataSourceEntity`,
355
+ `DataSourceTableEntity`, `DataSourceColumnEntity`,
356
+ `DataSourceRelationEntity`, `TableTemplateEntity` + the
357
+ `DataSourceType`, `DataSourceAccessMode`, `DataSourceStatus` enums.
358
+ All entities subclass `threetears.core.entities.base.BaseEntity` and
359
+ preserve the composite-PK / single-PK shape from their Hub origins
360
+ byte-for-byte.
361
+ - `threetears.datasources.collections` — `DataSourceCollection`
362
+ (`SchemaBackedCollection` subclass), `DataSourceTableCollection`,
363
+ `DataSourceColumnCollection`, `DataSourceRelationCollection`,
364
+ `TableTemplateCollection` (all `BaseCollection` subclasses with the
365
+ same `serialize / deserialize / fetch_from_postgres /
366
+ save_to_postgres / delete_from_postgres` shape).
367
+ - `threetears.datasources.namespace` — `DATASOURCE_NAMESPACE_TYPE`,
368
+ `datasource_namespace_id`, `datasource_namespace_name` helpers.
369
+ - `threetears.datasources.config` — `DatasourceConfig` (agent-yaml
370
+ shape). The discriminated-union `ConnectionConfig` lands in
371
+ `datasource-task-08`.
372
+ - Companion promotion: `threetears.core.utils.pg_pool_kwargs` — the
373
+ shared `asyncpg.create_pool` kwargs helper, DSN redactor, and
374
+ startup-timeout wrapper used by both Hub L3 and the future
375
+ `AsyncpgDriver`. Promoted from Hub's `aibots/hub/common/pg_pool.py`
376
+ so neither owns a divergent copy.
377
+
378
+ ### Out of scope (intentionally, for 0.1.0)
379
+
380
+ - Concrete driver implementations (shards 09-12).
381
+ - Hub admin API DTOs (`DataSourceCreateRequest`, `DataSourceResponse`,
382
+ etc.) — those are Hub-API contracts, not framework primitives, and
383
+ stay in Hub as `aibots/hub/datasources/hub_api.py`.
384
+ - Per-table ACL via the `datasource_table` namespace — already lives
385
+ in Hub's template routes; separate concern.
386
+
387
+ ### Migration notes
388
+
389
+ `3tears-datasources` is a brand-new package; no prior version exists.
390
+ Hub + agent-SDK consumers flip imports from the old Hub paths to
391
+ `threetears.datasources.*` in the same migration PR (shards 07 + 08).
392
+ No backward-compat shims are provided.