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.
@@ -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}"