onestep-sql 0.1.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.
- onestep_sql-0.1.0/.gitignore +146 -0
- onestep_sql-0.1.0/PKG-INFO +85 -0
- onestep_sql-0.1.0/README.md +56 -0
- onestep_sql-0.1.0/pyproject.toml +51 -0
- onestep_sql-0.1.0/src/onestep_sql/__init__.py +21 -0
- onestep_sql-0.1.0/src/onestep_sql/_shared/__init__.py +26 -0
- onestep_sql-0.1.0/src/onestep_sql/_shared/resilience.py +147 -0
- onestep_sql-0.1.0/src/onestep_sql/_shared/state_keys.py +31 -0
- onestep_sql-0.1.0/src/onestep_sql/_shared/state_sqlalchemy.py +209 -0
- onestep_sql-0.1.0/src/onestep_sql/_shared/table_sink_policy.py +119 -0
- onestep_sql-0.1.0/src/onestep_sql/mysql/__init__.py +41 -0
- onestep_sql-0.1.0/src/onestep_sql/mysql/connector.py +1054 -0
- onestep_sql-0.1.0/src/onestep_sql/mysql/resilience.py +104 -0
- onestep_sql-0.1.0/src/onestep_sql/mysql/resources.py +352 -0
- onestep_sql-0.1.0/src/onestep_sql/mysql/state_sqlalchemy.py +44 -0
- onestep_sql-0.1.0/src/onestep_sql/postgres/__init__.py +50 -0
- onestep_sql-0.1.0/src/onestep_sql/postgres/connector.py +767 -0
- onestep_sql-0.1.0/src/onestep_sql/postgres/execution_backend.py +1151 -0
- onestep_sql-0.1.0/src/onestep_sql/postgres/execution_schema.py +207 -0
- onestep_sql-0.1.0/src/onestep_sql/postgres/execution_source.py +474 -0
- onestep_sql-0.1.0/src/onestep_sql/postgres/resilience.py +106 -0
- onestep_sql-0.1.0/src/onestep_sql/postgres/resources.py +402 -0
- onestep_sql-0.1.0/src/onestep_sql/postgres/state_sqlalchemy.py +44 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# created by virtualenv automatically
|
|
2
|
+
# Byte-compiled / optimized / DLL files
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.py[cod]
|
|
5
|
+
*$py.class
|
|
6
|
+
|
|
7
|
+
# C extensions
|
|
8
|
+
*.so
|
|
9
|
+
|
|
10
|
+
# Distribution / packaging
|
|
11
|
+
.Python
|
|
12
|
+
build/
|
|
13
|
+
develop-eggs/
|
|
14
|
+
dist/
|
|
15
|
+
downloads/
|
|
16
|
+
eggs/
|
|
17
|
+
.eggs/
|
|
18
|
+
lib/
|
|
19
|
+
lib64/
|
|
20
|
+
parts/
|
|
21
|
+
sdist/
|
|
22
|
+
var/
|
|
23
|
+
wheels/
|
|
24
|
+
pip-wheel-metadata/
|
|
25
|
+
share/python-wheels/
|
|
26
|
+
*.egg-info/
|
|
27
|
+
.installed.cfg
|
|
28
|
+
*.egg
|
|
29
|
+
MANIFEST
|
|
30
|
+
|
|
31
|
+
# PyInstaller
|
|
32
|
+
# Usually these files are written by a python script from a template
|
|
33
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
34
|
+
*.manifest
|
|
35
|
+
*.spec
|
|
36
|
+
|
|
37
|
+
# Installer logs
|
|
38
|
+
pip-log.txt
|
|
39
|
+
pip-delete-this-directory.txt
|
|
40
|
+
|
|
41
|
+
# Unit test / coverage reports
|
|
42
|
+
htmlcov/
|
|
43
|
+
.tox/
|
|
44
|
+
.nox/
|
|
45
|
+
.coverage
|
|
46
|
+
.coverage.*
|
|
47
|
+
.cache
|
|
48
|
+
nosetests.xml
|
|
49
|
+
coverage.xml
|
|
50
|
+
*.cover
|
|
51
|
+
*.py,cover
|
|
52
|
+
.hypothesis/
|
|
53
|
+
.pytest_cache/
|
|
54
|
+
|
|
55
|
+
# Translations
|
|
56
|
+
*.mo
|
|
57
|
+
*.pot
|
|
58
|
+
|
|
59
|
+
# Django stuff:
|
|
60
|
+
*.log
|
|
61
|
+
local_settings.py
|
|
62
|
+
db.sqlite3
|
|
63
|
+
db.sqlite3-journal
|
|
64
|
+
|
|
65
|
+
# Flask stuff:
|
|
66
|
+
instance/
|
|
67
|
+
.webassets-cache
|
|
68
|
+
|
|
69
|
+
# Scrapy stuff:
|
|
70
|
+
.scrapy
|
|
71
|
+
|
|
72
|
+
# Sphinx documentation
|
|
73
|
+
docs/_build/
|
|
74
|
+
|
|
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
|
+
.python-version
|
|
87
|
+
|
|
88
|
+
# pipenv
|
|
89
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
90
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
91
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
92
|
+
# install all needed dependencies.
|
|
93
|
+
#Pipfile.lock
|
|
94
|
+
|
|
95
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
96
|
+
__pypackages__/
|
|
97
|
+
|
|
98
|
+
# Celery stuff
|
|
99
|
+
celerybeat-schedule
|
|
100
|
+
celerybeat.pid
|
|
101
|
+
|
|
102
|
+
# SageMath parsed files
|
|
103
|
+
*.sage.py
|
|
104
|
+
|
|
105
|
+
# Environments
|
|
106
|
+
.env
|
|
107
|
+
.venv
|
|
108
|
+
env/
|
|
109
|
+
venv/
|
|
110
|
+
ENV/
|
|
111
|
+
env.bak/
|
|
112
|
+
venv.bak/
|
|
113
|
+
!deploy/env/
|
|
114
|
+
!deploy/env/onestep-app.env.example
|
|
115
|
+
|
|
116
|
+
# Spyder project settings
|
|
117
|
+
.spyderproject
|
|
118
|
+
.spyproject
|
|
119
|
+
|
|
120
|
+
# Rope project settings
|
|
121
|
+
.ropeproject
|
|
122
|
+
|
|
123
|
+
# mkdocs documentation
|
|
124
|
+
/site
|
|
125
|
+
|
|
126
|
+
# mypy
|
|
127
|
+
.mypy_cache/
|
|
128
|
+
.dmypy.json
|
|
129
|
+
dmypy.json
|
|
130
|
+
|
|
131
|
+
# Pyre type checker
|
|
132
|
+
.pyre/
|
|
133
|
+
|
|
134
|
+
# pycharm
|
|
135
|
+
.idea
|
|
136
|
+
poetry.lock
|
|
137
|
+
|
|
138
|
+
# Ignore dynaconf secret files
|
|
139
|
+
*.secrets.*
|
|
140
|
+
node_modules/
|
|
141
|
+
.DS_Store
|
|
142
|
+
|
|
143
|
+
.release-smoke-check/
|
|
144
|
+
.release-smoke-local/
|
|
145
|
+
.release-smoke-local2/
|
|
146
|
+
.uv-cache/
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: onestep-sql
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unified MySQL and PostgreSQL connector plugin for onestep.
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Requires-Dist: aiosqlite>=0.20.0
|
|
8
|
+
Requires-Dist: onestep>=1.9.0
|
|
9
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.0
|
|
10
|
+
Provides-Extra: all
|
|
11
|
+
Requires-Dist: aiosqlite>=0.20.0; extra == 'all'
|
|
12
|
+
Requires-Dist: asyncmy>=0.2.10; extra == 'all'
|
|
13
|
+
Requires-Dist: mysql-replication>=1.0.15; extra == 'all'
|
|
14
|
+
Requires-Dist: psycopg[binary]>=3.2.0; extra == 'all'
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
18
|
+
Provides-Extra: mysql
|
|
19
|
+
Requires-Dist: asyncmy>=0.2.10; extra == 'mysql'
|
|
20
|
+
Requires-Dist: mysql-replication>=1.0.15; extra == 'mysql'
|
|
21
|
+
Provides-Extra: postgres
|
|
22
|
+
Requires-Dist: psycopg[binary]>=3.2.0; extra == 'postgres'
|
|
23
|
+
Provides-Extra: sqlite
|
|
24
|
+
Requires-Dist: aiosqlite>=0.20.0; extra == 'sqlite'
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'test'
|
|
27
|
+
Requires-Dist: pytest>=8.0.0; extra == 'test'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# onestep-sql
|
|
31
|
+
|
|
32
|
+
Canonical, unified MySQL **and** PostgreSQL connector plugin for
|
|
33
|
+
[onestep](https://github.com/mic1on/onestep).
|
|
34
|
+
|
|
35
|
+
This package merges the previously separate `onestep-mysql` and
|
|
36
|
+
`onestep-postgres` distributions behind a single entry point and a single
|
|
37
|
+
namespace (`onestep_sql`). It is introduced incrementally per
|
|
38
|
+
[the consolidation design](../../docs/superpowers/specs/2026-08-20-onestep-sql-consolidation-design.md)
|
|
39
|
+
(tracking issue #133).
|
|
40
|
+
|
|
41
|
+
## Status
|
|
42
|
+
|
|
43
|
+
**Phase 3 — canonical distribution; legacy packages are thin forwarding shims.**
|
|
44
|
+
|
|
45
|
+
* `onestep-sql[mysql]`, `[postgres]`, `[sqlite]`, and `[all]` build, discover
|
|
46
|
+
their resources, and pass their suites.
|
|
47
|
+
* `onestep_sql.mysql` and `onestep_sql.postgres` carry the canonical
|
|
48
|
+
implementations; shared SQL behaviour lives once in `onestep_sql._shared`.
|
|
49
|
+
* The root `onestep` extras (`mysql`, `postgres`, `sql`, `all`, `dev`,
|
|
50
|
+
`integration`) resolve through `onestep-sql`.
|
|
51
|
+
* The legacy `onestep-mysql` / `onestep-postgres` distributions remain
|
|
52
|
+
available as thin forwarding shims without their own resource entry points;
|
|
53
|
+
existing `pip install onestep-mysql` and `from onestep_mysql import ...`
|
|
54
|
+
imports keep working unchanged.
|
|
55
|
+
* All 14 YAML resource type names, catalog roles, fields, defaults, and
|
|
56
|
+
connector boundaries are unchanged.
|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install "onestep-sql[all]" # MySQL + PostgreSQL
|
|
62
|
+
pip install "onestep-sql[mysql]" # MySQL only
|
|
63
|
+
pip install "onestep-sql[postgres]" # PostgreSQL only
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
The package registers all 14 YAML resource types through a single entry point
|
|
69
|
+
(`sql` in the `onestep.resources` group). No import is required to use the
|
|
70
|
+
types in a YAML pipeline — onestep discovers them automatically.
|
|
71
|
+
|
|
72
|
+
For programmatic access to the connector classes:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from onestep_sql.mysql import MySQLConnector, BinlogSource
|
|
76
|
+
from onestep_sql.postgres import PostgresConnector, PostgresExecutionSource
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## What is NOT changing
|
|
80
|
+
|
|
81
|
+
* The 14 YAML type names (`mysql_*`, `postgres_*`) and their catalog roles,
|
|
82
|
+
fields, defaults, and connector boundaries are unchanged.
|
|
83
|
+
* `mysql_binlog` stays MySQL-only; `postgres_execution_source` / tracked
|
|
84
|
+
execution stays PostgreSQL-only.
|
|
85
|
+
* See the design doc for the full non-goals and the phased rollout plan.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# onestep-sql
|
|
2
|
+
|
|
3
|
+
Canonical, unified MySQL **and** PostgreSQL connector plugin for
|
|
4
|
+
[onestep](https://github.com/mic1on/onestep).
|
|
5
|
+
|
|
6
|
+
This package merges the previously separate `onestep-mysql` and
|
|
7
|
+
`onestep-postgres` distributions behind a single entry point and a single
|
|
8
|
+
namespace (`onestep_sql`). It is introduced incrementally per
|
|
9
|
+
[the consolidation design](../../docs/superpowers/specs/2026-08-20-onestep-sql-consolidation-design.md)
|
|
10
|
+
(tracking issue #133).
|
|
11
|
+
|
|
12
|
+
## Status
|
|
13
|
+
|
|
14
|
+
**Phase 3 — canonical distribution; legacy packages are thin forwarding shims.**
|
|
15
|
+
|
|
16
|
+
* `onestep-sql[mysql]`, `[postgres]`, `[sqlite]`, and `[all]` build, discover
|
|
17
|
+
their resources, and pass their suites.
|
|
18
|
+
* `onestep_sql.mysql` and `onestep_sql.postgres` carry the canonical
|
|
19
|
+
implementations; shared SQL behaviour lives once in `onestep_sql._shared`.
|
|
20
|
+
* The root `onestep` extras (`mysql`, `postgres`, `sql`, `all`, `dev`,
|
|
21
|
+
`integration`) resolve through `onestep-sql`.
|
|
22
|
+
* The legacy `onestep-mysql` / `onestep-postgres` distributions remain
|
|
23
|
+
available as thin forwarding shims without their own resource entry points;
|
|
24
|
+
existing `pip install onestep-mysql` and `from onestep_mysql import ...`
|
|
25
|
+
imports keep working unchanged.
|
|
26
|
+
* All 14 YAML resource type names, catalog roles, fields, defaults, and
|
|
27
|
+
connector boundaries are unchanged.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install "onestep-sql[all]" # MySQL + PostgreSQL
|
|
33
|
+
pip install "onestep-sql[mysql]" # MySQL only
|
|
34
|
+
pip install "onestep-sql[postgres]" # PostgreSQL only
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
The package registers all 14 YAML resource types through a single entry point
|
|
40
|
+
(`sql` in the `onestep.resources` group). No import is required to use the
|
|
41
|
+
types in a YAML pipeline — onestep discovers them automatically.
|
|
42
|
+
|
|
43
|
+
For programmatic access to the connector classes:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from onestep_sql.mysql import MySQLConnector, BinlogSource
|
|
47
|
+
from onestep_sql.postgres import PostgresConnector, PostgresExecutionSource
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## What is NOT changing
|
|
51
|
+
|
|
52
|
+
* The 14 YAML type names (`mysql_*`, `postgres_*`) and their catalog roles,
|
|
53
|
+
fields, defaults, and connector boundaries are unchanged.
|
|
54
|
+
* `mysql_binlog` stays MySQL-only; `postgres_execution_source` / tracked
|
|
55
|
+
execution stays PostgreSQL-only.
|
|
56
|
+
* See the design doc for the full non-goals and the phased rollout plan.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "onestep-sql"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Unified MySQL and PostgreSQL connector plugin for onestep."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
dependencies = [
|
|
9
|
+
"onestep>=1.9.0",
|
|
10
|
+
"SQLAlchemy[asyncio]>=2.0.0",
|
|
11
|
+
"aiosqlite>=0.20.0",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.optional-dependencies]
|
|
15
|
+
mysql = [
|
|
16
|
+
"asyncmy>=0.2.10",
|
|
17
|
+
"mysql-replication>=1.0.15",
|
|
18
|
+
]
|
|
19
|
+
postgres = [
|
|
20
|
+
"psycopg[binary]>=3.2.0",
|
|
21
|
+
]
|
|
22
|
+
sqlite = [
|
|
23
|
+
"aiosqlite>=0.20.0",
|
|
24
|
+
]
|
|
25
|
+
all = [
|
|
26
|
+
"asyncmy>=0.2.10",
|
|
27
|
+
"mysql-replication>=1.0.15",
|
|
28
|
+
"psycopg[binary]>=3.2.0",
|
|
29
|
+
"aiosqlite>=0.20.0",
|
|
30
|
+
]
|
|
31
|
+
test = [
|
|
32
|
+
"pytest>=8.0.0",
|
|
33
|
+
"pytest-asyncio>=0.23.0",
|
|
34
|
+
]
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest>=8.0.0",
|
|
37
|
+
"pytest-asyncio>=0.23.0",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.entry-points."onestep.resources"]
|
|
41
|
+
sql = "onestep_sql:register_resources"
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["hatchling"]
|
|
45
|
+
build-backend = "hatchling.build"
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
packages = ["src/onestep_sql"]
|
|
49
|
+
|
|
50
|
+
[tool.uv.sources]
|
|
51
|
+
onestep = { workspace = true }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from onestep.resource_registry import ResourceRegistry
|
|
4
|
+
|
|
5
|
+
from . import mysql, postgres
|
|
6
|
+
from .mysql import register_resources as _register_mysql
|
|
7
|
+
from .postgres import register_resources as _register_postgres
|
|
8
|
+
|
|
9
|
+
__all__ = ["register_resources", "mysql", "postgres"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register_resources(registry: ResourceRegistry) -> None:
|
|
13
|
+
"""Register every onestep-sql resource type (MySQL + PostgreSQL).
|
|
14
|
+
|
|
15
|
+
This is the single entry point for the canonical ``onestep-sql``
|
|
16
|
+
distribution (declared as ``sql`` in the ``onestep.resources`` group).
|
|
17
|
+
It delegates to the per-backend registrars without changing any of the
|
|
18
|
+
14 existing YAML type names, their catalog roles, or their fields.
|
|
19
|
+
"""
|
|
20
|
+
_register_mysql(registry)
|
|
21
|
+
_register_postgres(registry)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Internal shared SQL behaviour for the canonical ``onestep-sql`` package.
|
|
2
|
+
|
|
3
|
+
Phase 2 of the ``onestep-mysql`` / ``onestep-postgres`` consolidation
|
|
4
|
+
(issue #133, design in PR #134) keeps exactly one copy of the machinery that
|
|
5
|
+
previously lived in parallel in both backend subpackages (as monitored by the
|
|
6
|
+
now-retired ``scripts/check_plugin_drift.py``):
|
|
7
|
+
|
|
8
|
+
* :mod:`onestep_sql._shared.state_sqlalchemy` — the SQLAlchemy state/cursor
|
|
9
|
+
stores; backends subclass them and override only the asyncio driver mapping
|
|
10
|
+
(``mysql+asyncmy`` vs ``postgresql+psycopg``) and install hint;
|
|
11
|
+
* :mod:`onestep_sql._shared.table_sink_policy` — the table-sink
|
|
12
|
+
``update_columns`` write policies (``overwrite`` / ``skip_null`` /
|
|
13
|
+
``backfill``), the ``_normalize_update_columns`` validator and the
|
|
14
|
+
``_update_payload`` / ``_coerce_json_values`` mixin;
|
|
15
|
+
* :mod:`onestep_sql._shared.state_keys` — the default incremental state-key
|
|
16
|
+
derivation feeding the at-least-once cursor state;
|
|
17
|
+
* :mod:`onestep_sql._shared.resilience` — the secret-token collection, message
|
|
18
|
+
redaction, redacted error-cause base class and the
|
|
19
|
+
``ConnectorOperationError`` factory; the dialect-specific SQLAlchemy error
|
|
20
|
+
classification tables stay in each backend's ``resilience.py``.
|
|
21
|
+
|
|
22
|
+
This package is **not** public API. Backend capabilities never move here:
|
|
23
|
+
MySQL binlog CDC stays in ``onestep_sql.mysql`` and the PostgreSQL tracked
|
|
24
|
+
execution modules stay in ``onestep_sql.postgres``. Dual-backend contract
|
|
25
|
+
tests live in ``tests/contract/test_onestep_sql_shared.py``.
|
|
26
|
+
"""
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Shared secret-redaction scaffolding for the onestep-sql backends.
|
|
2
|
+
|
|
3
|
+
Phase 2 of the mysql/postgres consolidation (issue #133, design §3.1) keeps
|
|
4
|
+
exactly one copy of the resilience machinery that previously existed in
|
|
5
|
+
parallel in both backend ``resilience.py`` modules and differed only in the
|
|
6
|
+
backend name (see the retired ``scripts/check_plugin_drift.py``):
|
|
7
|
+
|
|
8
|
+
* the secret-token collection used to scrub error causes
|
|
9
|
+
(:func:`collect_sensitive_tokens`);
|
|
10
|
+
* the message redaction itself (:func:`redact_message`);
|
|
11
|
+
* the redacted error-cause base class (:class:`SQLErrorCause`) that backends
|
|
12
|
+
subclass with their own ``backend`` label so ``str(cause)`` keeps its
|
|
13
|
+
per-database prefix;
|
|
14
|
+
* the :class:`~onestep.resilience.ConnectorOperationError` factory
|
|
15
|
+
(:func:`as_connector_operation_error`).
|
|
16
|
+
|
|
17
|
+
The SQLAlchemy error-classification tables stay in each backend's
|
|
18
|
+
``resilience.py`` because they encode genuinely different server error
|
|
19
|
+
messages (``server has gone away`` vs ``server closed the connection``,
|
|
20
|
+
``deadlock found`` vs ``deadlock detected`` and so on).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from collections.abc import Callable, Mapping
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from typing import ClassVar
|
|
28
|
+
from urllib.parse import urlsplit
|
|
29
|
+
|
|
30
|
+
from onestep.resilience import (
|
|
31
|
+
ConnectorErrorKind,
|
|
32
|
+
ConnectorOperation,
|
|
33
|
+
ConnectorOperationError,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
REDACTED = "<redacted>"
|
|
37
|
+
MAX_MESSAGE_LENGTH = 500
|
|
38
|
+
SECRET_OPTION_KEYS = frozenset(
|
|
39
|
+
{
|
|
40
|
+
"password",
|
|
41
|
+
"passwd",
|
|
42
|
+
"pwd",
|
|
43
|
+
"secret",
|
|
44
|
+
"token",
|
|
45
|
+
"access_token",
|
|
46
|
+
"api_key",
|
|
47
|
+
"apikey",
|
|
48
|
+
"credentials",
|
|
49
|
+
"authorization",
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def collect_sensitive_tokens(*config_values: object) -> list[str]:
|
|
55
|
+
"""Collect secret substrings that may surface in SQL backend error messages.
|
|
56
|
+
|
|
57
|
+
SQLAlchemy masks the password in its rendered URL, but the underlying DBAPI
|
|
58
|
+
``orig`` exception can still echo credentials (e.g. after echoing the full
|
|
59
|
+
connection string). Tokens are derived from connector config (raw DSN +
|
|
60
|
+
parsed userinfo + known secret option values) and used to scrub error
|
|
61
|
+
causes before they leave the plugin.
|
|
62
|
+
"""
|
|
63
|
+
tokens: list[str] = []
|
|
64
|
+
seen: set[str] = set()
|
|
65
|
+
|
|
66
|
+
def add(value: object) -> None:
|
|
67
|
+
if value is None:
|
|
68
|
+
return
|
|
69
|
+
text = str(value)
|
|
70
|
+
if text and text not in seen:
|
|
71
|
+
seen.add(text)
|
|
72
|
+
tokens.append(text)
|
|
73
|
+
|
|
74
|
+
def collect_mapping(value: Mapping[object, object]) -> None:
|
|
75
|
+
for key, item in value.items():
|
|
76
|
+
if str(key).lower() in SECRET_OPTION_KEYS:
|
|
77
|
+
add(item)
|
|
78
|
+
elif isinstance(item, Mapping):
|
|
79
|
+
collect_mapping(item)
|
|
80
|
+
|
|
81
|
+
for value in config_values:
|
|
82
|
+
if isinstance(value, Mapping):
|
|
83
|
+
collect_mapping(value)
|
|
84
|
+
continue
|
|
85
|
+
add(value)
|
|
86
|
+
try:
|
|
87
|
+
parsed = urlsplit(str(value))
|
|
88
|
+
except ValueError:
|
|
89
|
+
continue
|
|
90
|
+
username = parsed.username
|
|
91
|
+
password = parsed.password
|
|
92
|
+
if username and password:
|
|
93
|
+
add(f"{username}:{password}")
|
|
94
|
+
add(f"{username}:{password}@")
|
|
95
|
+
add(password)
|
|
96
|
+
return tokens
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def redact_message(message: str, tokens: list[str]) -> str:
|
|
100
|
+
redacted = message
|
|
101
|
+
for token in sorted(tokens, key=len, reverse=True):
|
|
102
|
+
if token:
|
|
103
|
+
redacted = redacted.replace(token, REDACTED)
|
|
104
|
+
return redacted[:MAX_MESSAGE_LENGTH]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True)
|
|
108
|
+
class SQLErrorCause(Exception):
|
|
109
|
+
"""Redacted cause attached to normalized SQL connector errors.
|
|
110
|
+
|
|
111
|
+
Backends subclass this with their own ``backend`` label so the rendered
|
|
112
|
+
message keeps its per-database prefix (``mysql error: ...`` /
|
|
113
|
+
``postgres error: ...``) while the dataclass shape, freezing and identity
|
|
114
|
+
semantics are implemented once here.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
message: str
|
|
118
|
+
|
|
119
|
+
backend: ClassVar[str] = "sql"
|
|
120
|
+
|
|
121
|
+
def __str__(self) -> str:
|
|
122
|
+
return f"{self.backend} error: {self.message}"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def as_connector_operation_error(
|
|
126
|
+
*,
|
|
127
|
+
backend: str,
|
|
128
|
+
operation: ConnectorOperation,
|
|
129
|
+
exc: BaseException,
|
|
130
|
+
classify: Callable[[BaseException], ConnectorErrorKind | None],
|
|
131
|
+
redacted_cause: Callable[..., Exception],
|
|
132
|
+
source_name: str | None = None,
|
|
133
|
+
retry_delay_s: float | None = None,
|
|
134
|
+
secrets: list[str] | None = None,
|
|
135
|
+
) -> ConnectorOperationError | None:
|
|
136
|
+
"""Normalize ``exc`` using the backend's classifier and redacted cause."""
|
|
137
|
+
kind = classify(exc)
|
|
138
|
+
if kind is None:
|
|
139
|
+
return None
|
|
140
|
+
return ConnectorOperationError(
|
|
141
|
+
backend=backend,
|
|
142
|
+
operation=operation,
|
|
143
|
+
kind=kind,
|
|
144
|
+
source_name=source_name,
|
|
145
|
+
retry_delay_s=retry_delay_s,
|
|
146
|
+
cause=redacted_cause(exc, secrets=secrets),
|
|
147
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Shared default incremental state-key derivation for the onestep-sql backends.
|
|
2
|
+
|
|
3
|
+
Phase 2 of the mysql/postgres consolidation (issue #133, design §3.1) keeps
|
|
4
|
+
exactly one copy of ``_default_incremental_state_key``, which previously
|
|
5
|
+
existed in parallel in both backend ``connector.py`` modules (see the retired
|
|
6
|
+
``scripts/check_plugin_drift.py``). The derived key feeds the at-least-once
|
|
7
|
+
cursor state, so its format is stable contract: changing it would orphan
|
|
8
|
+
existing persisted cursors.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import hashlib
|
|
14
|
+
from collections.abc import Sequence
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _default_incremental_state_key(
|
|
18
|
+
*,
|
|
19
|
+
table: str,
|
|
20
|
+
cursor: Sequence[str],
|
|
21
|
+
key: str,
|
|
22
|
+
where: str | None,
|
|
23
|
+
) -> str:
|
|
24
|
+
normalized_where = " ".join((where or "").split())
|
|
25
|
+
if normalized_where:
|
|
26
|
+
where_fragment = normalized_where
|
|
27
|
+
if len(where_fragment) > 64:
|
|
28
|
+
where_fragment = f"sha1:{hashlib.sha1(where_fragment.encode('utf-8')).hexdigest()}"
|
|
29
|
+
else:
|
|
30
|
+
where_fragment = "-"
|
|
31
|
+
return f"{table}:{','.join(cursor)}:key={key}:where={where_fragment}"
|