syncy 0.1.3__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.3/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.3/PKG-INFO ADDED
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: syncy
3
+ Version: 0.1.3
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
+ Requires-Dist: ttkbootstrap>=1.10.1
18
+ Dynamic: classifier
19
+ Dynamic: license-file
20
+ Dynamic: requires-dist
21
+ Dynamic: requires-python
22
+ Dynamic: summary
syncy-0.1.3/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,8 @@
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
+ ttkbootstrap>=1.10.1
8
+ pytest>=7.4.0
@@ -0,0 +1,43 @@
1
+ ---
2
+ # Example rule pack for Syncy. You can add/remove rules or edit hints.
3
+ # Each rule is a simple detector for common cross-engine differences.
4
+
5
+ - id: R01
6
+ desc: TOP vs LIMIT
7
+ hint: Replace SQL Server TOP with PostgreSQL LIMIT (or FETCH FIRST)
8
+
9
+ - id: R02
10
+ desc: ISNULL() vs COALESCE()
11
+ hint: Prefer COALESCE for portability; ISNULL is SQL Server-only
12
+
13
+ - id: R03
14
+ desc: IDENTITY vs SEQUENCE
15
+ hint: Use GENERATED AS IDENTITY or a SEQUENCE with nextval() default
16
+
17
+ - id: R04
18
+ desc: BIT vs BOOLEAN
19
+ hint: Map BIT(1) to BOOLEAN; convert 0/1 to FALSE/TRUE
20
+
21
+ - id: R05
22
+ desc: DATETIME vs TIMESTAMP
23
+ hint: Map DATETIME to TIMESTAMP; review precision/timezone
24
+
25
+ - id: R06
26
+ desc: NVARCHAR length mismatch
27
+ hint: Align target VARCHAR length to be >= source
28
+
29
+ - id: R07
30
+ desc: UUID vs UNIQUEIDENTIFIER
31
+ hint: Use PostgreSQL UUID; enable uuid-ossp if generating values
32
+
33
+ - id: R08
34
+ desc: Collation differences
35
+ hint: Remove/adjust COLLATE to target equivalent collation
36
+
37
+ - id: R09
38
+ desc: Trigger timing mismatch
39
+ hint: Convert INSTEAD OF triggers to BEFORE/AFTER with equivalent logic
40
+
41
+ - id: R10
42
+ desc: Function name mismatch
43
+ hint: Translate functions (e.g., LEN -> length/char_length)
syncy-0.1.3/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
syncy-0.1.3/setup.py ADDED
@@ -0,0 +1,40 @@
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
+ "ttkbootstrap>=1.10.1",
27
+ ],
28
+ entry_points={
29
+ "console_scripts": [
30
+ "syncy=syncy.cli:cli",
31
+ ]
32
+ },
33
+ python_requires=">=3.10",
34
+ classifiers=[
35
+ "Programming Language :: Python :: 3",
36
+ "License :: OSI Approved :: MIT License",
37
+ "Operating System :: OS Independent",
38
+ ],
39
+ )
40
+
@@ -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.3"
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
+ ]