syncy 0.1.1__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.
syncy-0.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 hakambing
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,18 @@
1
+ include README.md
2
+ include LICENSE
3
+ include requirements.txt
4
+ include rules.yaml.example
5
+ include validator.yaml.example
6
+
7
+ prune reports
8
+ prune build
9
+ prune dist
10
+ prune .git
11
+ prune .venv
12
+ prune syncy.egg-info
13
+ prune .pytest_cache
14
+ prune .mypy_cache
15
+
16
+ global-exclude __pycache__ *.py[cod] .DS_Store
17
+ C:\Users\hakam\AppData\Roaming\Python\Python312\Scripts
18
+
syncy-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: syncy
3
+ Version: 0.1.1
4
+ Summary: Database migration validation tool (SQL Server ↔ PostgreSQL)
5
+ Author:
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.10
10
+ License-File: LICENSE
11
+ Requires-Dist: click>=8.1.0
12
+ Requires-Dist: pyodbc>=5.0.0
13
+ Requires-Dist: psycopg2-binary>=2.9.9
14
+ Requires-Dist: Jinja2>=3.1.2
15
+ Requires-Dist: PyYAML>=6.0.1
16
+ Requires-Dist: sqlparse>=0.4.4
17
+ Dynamic: classifier
18
+ Dynamic: license-file
19
+ Dynamic: requires-dist
20
+ Dynamic: requires-python
21
+ Dynamic: summary
syncy-0.1.1/README.md ADDED
@@ -0,0 +1,55 @@
1
+ Syncy
2
+ =====
3
+
4
+ Syncy is a lightweight database migration validation tool for comparing logic and schema objects between SQL Server and PostgreSQL. It focuses on safe, read-only validation and produces HTML + JSON reports.
5
+
6
+ Features
7
+ - Read-only connectors for MSSQL (pyodbc) and Postgres (psycopg2-binary)
8
+ - Extract views, functions, procedures, triggers metadata/definitions
9
+ - Built-in rule pack (10 cross-engine mismatch checks)
10
+ - Simple behaviour tests on views (sample shape comparison)
11
+ - Timestamped HTML + JSON reports
12
+
13
+ Quick Start
14
+ 0. (Recommended) Create and activate a virtual environment
15
+ - Windows (PowerShell):
16
+ python -m venv .venv
17
+ .\.venv\Scripts\Activate.ps1
18
+ - macOS/Linux:
19
+ python3 -m venv .venv
20
+ source .venv/bin/activate
21
+
22
+ 1. Install dependencies (Python 3.10+):
23
+ pip install -r requirements.txt
24
+
25
+ 2. Provide connection URLs via flags, YAML, or env:
26
+ - Flags: --source, --target
27
+ - YAML: validator.yaml (see example below)
28
+ - Env: SYNCY_SOURCE_URL, SYNCY_TARGET_URL
29
+
30
+ 3. Seed sample schemas into your own SQL Server/Postgres (optional):
31
+ python demo/load_samples.py --source "mssql://sa:Your_password123@localhost:1433/master?driver=ODBC+Driver+18+for+SQL+Server&TrustServerCertificate=yes" --target "postgresql://postgres:postgres@localhost:5432/syncy?sslmode=disable"
32
+
33
+ 4. Run validation:
34
+ syncy validate --source "mssql://sa:Your_password123@localhost:1433/master?driver=ODBC+Driver+18+for+SQL+Server&TrustServerCertificate=yes" \
35
+ --target "postgresql://postgres:postgres@localhost:5432/syncy?sslmode=disable" \
36
+ --out ./reports/demo/
37
+
38
+ Config Resolution Order
39
+ 1) CLI flags > 2) validator.yaml > 3) environment variables.
40
+
41
+ validator.yaml example
42
+ source:
43
+ url: ${SYNCY_SOURCE_URL}
44
+ target:
45
+ url: ${SYNCY_TARGET_URL}
46
+ include_schemas: [public]
47
+ exclude_schemas: []
48
+
49
+ Notes
50
+ - All queries run in safe mode. Obvious write operations are blocked.
51
+ - Reports are written to a timestamped folder under the provided --out or ./reports/.
52
+ - For production environments, you may swap psycopg2-binary for psycopg2 in requirements.
53
+
54
+ License
55
+ MIT
@@ -0,0 +1,7 @@
1
+ click>=8.1.0
2
+ pyodbc>=5.0.0
3
+ psycopg2-binary>=2.9.9
4
+ Jinja2>=3.1.2
5
+ PyYAML>=6.0.1
6
+ sqlparse>=0.4.4
7
+ pytest>=7.4.0
@@ -0,0 +1,53 @@
1
+ ---
2
+ # Example rule pack for Syncy. You can customize severities or add/remove rules.
3
+ # Each rule is a simple detector for common cross-engine differences.
4
+
5
+ - id: R01
6
+ desc: TOP vs LIMIT
7
+ severity: major
8
+ hint: Replace SQL Server TOP with PostgreSQL LIMIT (or FETCH FIRST)
9
+
10
+ - id: R02
11
+ desc: ISNULL() vs COALESCE()
12
+ severity: minor
13
+ hint: Prefer COALESCE for portability; ISNULL is SQL Server-only
14
+
15
+ - id: R03
16
+ desc: IDENTITY vs SEQUENCE
17
+ severity: major
18
+ hint: Use GENERATED AS IDENTITY or a SEQUENCE with nextval() default
19
+
20
+ - id: R04
21
+ desc: BIT vs BOOLEAN
22
+ severity: minor
23
+ hint: Map BIT(1) to BOOLEAN; convert 0/1 to FALSE/TRUE
24
+
25
+ - id: R05
26
+ desc: DATETIME vs TIMESTAMP
27
+ severity: minor
28
+ hint: Map DATETIME to TIMESTAMP; review precision/timezone
29
+
30
+ - id: R06
31
+ desc: NVARCHAR length mismatch
32
+ severity: minor
33
+ hint: Align target VARCHAR length to be >= source
34
+
35
+ - id: R07
36
+ desc: UUID vs UNIQUEIDENTIFIER
37
+ severity: major
38
+ hint: Use PostgreSQL UUID; enable uuid-ossp if generating values
39
+
40
+ - id: R08
41
+ desc: Collation differences
42
+ severity: minor
43
+ hint: Remove/adjust COLLATE to target equivalent collation
44
+
45
+ - id: R09
46
+ desc: Trigger timing mismatch
47
+ severity: major
48
+ hint: Convert INSTEAD OF triggers to BEFORE/AFTER with equivalent logic
49
+
50
+ - id: R10
51
+ desc: Function name mismatch
52
+ severity: minor
53
+ hint: Translate functions (e.g., LEN -> length/char_length)
syncy-0.1.1/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
syncy-0.1.1/setup.py ADDED
@@ -0,0 +1,39 @@
1
+ from pathlib import Path
2
+ from setuptools import setup, find_packages
3
+
4
+
5
+ def read_version() -> str:
6
+ ns = {}
7
+ with open("syncy/__init__.py", "r", encoding="utf-8") as f:
8
+ exec(f.read(), ns)
9
+ return ns.get("__version__", "0.0.0")
10
+
11
+
12
+ setup(
13
+ name="syncy",
14
+ version=read_version(),
15
+ description="Database migration validation tool (SQL Server ↔ PostgreSQL)",
16
+ author="",
17
+ packages=find_packages(exclude=("tests", "reports")),
18
+ include_package_data=True,
19
+ install_requires=[
20
+ "click>=8.1.0",
21
+ "pyodbc>=5.0.0",
22
+ "psycopg2-binary>=2.9.9",
23
+ "Jinja2>=3.1.2",
24
+ "PyYAML>=6.0.1",
25
+ "sqlparse>=0.4.4",
26
+ ],
27
+ entry_points={
28
+ "console_scripts": [
29
+ "syncy=syncy.cli:cli",
30
+ ]
31
+ },
32
+ python_requires=">=3.10",
33
+ classifiers=[
34
+ "Programming Language :: Python :: 3",
35
+ "License :: OSI Approved :: MIT License",
36
+ "Operating System :: OS Independent",
37
+ ],
38
+ )
39
+
@@ -0,0 +1,9 @@
1
+ """Syncy - Database migration validation tool.
2
+
3
+ Exposes the package version and common constants.
4
+ """
5
+
6
+ __all__ = ["__version__"]
7
+
8
+ __version__ = "0.1.1"
9
+
@@ -0,0 +1,24 @@
1
+ from ..config.runtime import read_rules_yaml_file as _read_rules_yaml_file
2
+ from ..config.runtime import resolve_rule_pack as _resolve_rule_pack
3
+ from .base import cli
4
+ from .utils import (
5
+ DEFAULT_BEHAVIOUR_LIMIT,
6
+ DEFAULT_BEHAVIOUR_TIMEOUT_S,
7
+ _engine_info,
8
+ _make_output_dir,
9
+ )
10
+
11
+ # Import submodules so commands register on the group
12
+ from . import gui as _gui # noqa: F401
13
+ from . import validate as _validate # noqa: F401
14
+ from . import wizard as _wizard # noqa: F401
15
+
16
+ __all__ = [
17
+ "cli",
18
+ "_resolve_rule_pack",
19
+ "_read_rules_yaml_file",
20
+ "DEFAULT_BEHAVIOUR_LIMIT",
21
+ "DEFAULT_BEHAVIOUR_TIMEOUT_S",
22
+ "_engine_info",
23
+ "_make_output_dir",
24
+ ]
@@ -0,0 +1,12 @@
1
+ import click
2
+
3
+ from .. import __version__
4
+
5
+
6
+ @click.group()
7
+ @click.version_option(__version__, prog_name="syncy")
8
+ def cli() -> None:
9
+ """Syncy CLI - Validate cross-engine DB migrations."""
10
+
11
+
12
+ __all__ = ["cli"]
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ import click
4
+
5
+ from .base import cli
6
+
7
+
8
+ @cli.command("gui")
9
+ def gui() -> None:
10
+ """Launch the desktop GUI wrapper."""
11
+ try:
12
+ from ..gui.app import main as gui_main
13
+ except Exception as exc: # pragma: no cover - GUI-only failure
14
+ raise click.ClickException(f"GUI not available: {exc}")
15
+ gui_main()
16
+
17
+
18
+ __all__ = ["gui"]
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone, timedelta
4
+ from pathlib import Path
5
+ from typing import Dict, Optional
6
+ from urllib.parse import quote_plus
7
+
8
+ import click
9
+
10
+
11
+ DEFAULT_REPORTS_DIR = "reports"
12
+ LOCAL_TZ = timezone(timedelta(hours=8))
13
+ DEFAULT_BEHAVIOUR_LIMIT = 5
14
+ DEFAULT_BEHAVIOUR_TIMEOUT_S = 30
15
+
16
+
17
+ def _timestamp() -> str:
18
+ return datetime.now(LOCAL_TZ).strftime("%Y%m%d_%H%M%S%z")
19
+
20
+
21
+ def _prompt_connection_engine(kind: str, default_scheme: str) -> str:
22
+ choice = click.prompt(
23
+ f"Choose {kind} engine",
24
+ type=click.Choice(["mssql", "postgresql"], case_sensitive=False),
25
+ default=default_scheme,
26
+ )
27
+ return choice.lower()
28
+
29
+
30
+ def _prompt_optional_params(scheme: str) -> Dict[str, str]:
31
+ params: Dict[str, str] = {}
32
+ if scheme == "mssql":
33
+ driver = click.prompt(
34
+ "ODBC driver",
35
+ default="ODBC Driver 18 for SQL Server",
36
+ )
37
+ encrypt = "yes" if click.confirm("Encrypt connection?", default=True) else "no"
38
+ trust = (
39
+ "yes"
40
+ if click.confirm("Trust server certificate?", default=True)
41
+ else "no"
42
+ )
43
+ params["driver"] = driver
44
+ params["Encrypt"] = encrypt
45
+ params["TrustServerCertificate"] = trust
46
+ else:
47
+ sslmode = click.prompt(
48
+ "SSL mode (leave blank to skip)",
49
+ default="",
50
+ show_default=False,
51
+ ).strip()
52
+ if sslmode:
53
+ params["sslmode"] = sslmode
54
+
55
+ extra = click.prompt(
56
+ "Additional query params (key=value&key2=value2, blank to skip)",
57
+ default="",
58
+ show_default=False,
59
+ ).strip()
60
+ if extra:
61
+ for chunk in extra.split("&"):
62
+ if "=" not in chunk:
63
+ continue
64
+ key, value = chunk.split("=", 1)
65
+ key = key.strip()
66
+ value = value.strip()
67
+ if key:
68
+ params[key] = value
69
+ return params
70
+
71
+
72
+ def _build_connection_url(kind: str, default_scheme: str) -> str:
73
+ while True:
74
+ scheme = _prompt_connection_engine(kind, default_scheme)
75
+ default_port = 1433 if scheme == "mssql" else 5432
76
+ default_db = "master" if scheme == "mssql" else "postgres"
77
+ host = click.prompt("Host", default="localhost").strip()
78
+ port = click.prompt("Port", default=default_port, type=int)
79
+ database = click.prompt("Database name", default=default_db).strip()
80
+ username = click.prompt(
81
+ "Username (leave blank for none)",
82
+ default="",
83
+ show_default=False,
84
+ ).strip()
85
+ password = click.prompt(
86
+ "Password (leave blank for none)",
87
+ default="",
88
+ show_default=False,
89
+ hide_input=True,
90
+ )
91
+ params = _prompt_optional_params(scheme)
92
+
93
+ auth = ""
94
+ if username:
95
+ auth = quote_plus(username)
96
+ if password:
97
+ auth = f"{auth}:{quote_plus(password)}"
98
+ auth = f"{auth}@"
99
+ elif password:
100
+ auth = f":{quote_plus(password)}@"
101
+
102
+ host_part = host
103
+ if port:
104
+ host_part = f"{host}:{port}"
105
+
106
+ path = f"/{database}" if database else ""
107
+ query = "&".join(
108
+ f"{quote_plus(str(k))}={quote_plus(str(v))}" for k, v in params.items()
109
+ )
110
+ url = f"{scheme}://{auth}{host_part}{path}"
111
+ if query:
112
+ url = f"{url}?{query}"
113
+
114
+ click.echo(f"\nConstructed {kind} URL:\n{url}\n")
115
+ if click.confirm("Use this URL?", default=True):
116
+ return url
117
+ click.echo("Let's try building it again.\n")
118
+
119
+
120
+ def _make_output_dir(base_out: Optional[str]) -> Path:
121
+ base = Path(base_out) if base_out else Path(DEFAULT_REPORTS_DIR)
122
+ out_dir = base / _timestamp()
123
+ out_dir.mkdir(parents=True, exist_ok=True)
124
+ return out_dir
125
+
126
+
127
+ def _engine_info(conn) -> Dict[str, str]:
128
+ """Return a simple engine and version string for overview."""
129
+ try:
130
+ from ..core.connectors import MSSQLConnector, PostgresConnector
131
+
132
+ if isinstance(conn, MSSQLConnector):
133
+ row = conn.fetchone("SELECT @@VERSION")
134
+ return {"engine": "SQL Server", "version": (row[0] if row else "")}
135
+ if isinstance(conn, PostgresConnector):
136
+ row = conn.fetchone("SELECT version()")
137
+ return {"engine": "PostgreSQL", "version": (row[0] if row else "")}
138
+ except Exception:
139
+ pass
140
+ name = conn.__class__.__name__.replace("Connector", "")
141
+ return {"engine": name, "version": ""}
142
+
143
+
144
+ __all__ = [
145
+ "DEFAULT_BEHAVIOUR_LIMIT",
146
+ "DEFAULT_BEHAVIOUR_TIMEOUT_S",
147
+ "DEFAULT_REPORTS_DIR",
148
+ "LOCAL_TZ",
149
+ "_build_connection_url",
150
+ "_engine_info",
151
+ "_make_output_dir",
152
+ ]