singularitysql 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.
Files changed (35) hide show
  1. singularitysql-0.1.0/LICENSE +21 -0
  2. singularitysql-0.1.0/PKG-INFO +220 -0
  3. singularitysql-0.1.0/README.md +198 -0
  4. singularitysql-0.1.0/pyproject.toml +56 -0
  5. singularitysql-0.1.0/setup.cfg +4 -0
  6. singularitysql-0.1.0/singularity/__init__.py +18 -0
  7. singularitysql-0.1.0/singularity/cli/__init__.py +8 -0
  8. singularitysql-0.1.0/singularity/cli/_app.py +25 -0
  9. singularitysql-0.1.0/singularity/cli/config.py +142 -0
  10. singularitysql-0.1.0/singularity/cli/docs_generate.py +196 -0
  11. singularitysql-0.1.0/singularity/cli/generate.py +210 -0
  12. singularitysql-0.1.0/singularity/exceptions.py +13 -0
  13. singularitysql-0.1.0/singularity/executor.py +209 -0
  14. singularitysql-0.1.0/singularity/introspector.py +108 -0
  15. singularitysql-0.1.0/singularity/model_generator.py +551 -0
  16. singularitysql-0.1.0/singularity/types.py +73 -0
  17. singularitysql-0.1.0/singularity/version/__init__.py +80 -0
  18. singularitysql-0.1.0/singularity/version/_azure.py +122 -0
  19. singularitysql-0.1.0/singularity/version/_base.py +73 -0
  20. singularitysql-0.1.0/singularity/version/_legacy.py +150 -0
  21. singularitysql-0.1.0/singularity/version/_modern.py +122 -0
  22. singularitysql-0.1.0/singularitysql.egg-info/PKG-INFO +220 -0
  23. singularitysql-0.1.0/singularitysql.egg-info/SOURCES.txt +33 -0
  24. singularitysql-0.1.0/singularitysql.egg-info/dependency_links.txt +1 -0
  25. singularitysql-0.1.0/singularitysql.egg-info/entry_points.txt +2 -0
  26. singularitysql-0.1.0/singularitysql.egg-info/requires.txt +12 -0
  27. singularitysql-0.1.0/singularitysql.egg-info/top_level.txt +2 -0
  28. singularitysql-0.1.0/tests/test_cli_generate.py +61 -0
  29. singularitysql-0.1.0/tests/test_config_loading.py +197 -0
  30. singularitysql-0.1.0/tests/test_field_sanitization.py +104 -0
  31. singularitysql-0.1.0/tests/test_introspector.py +119 -0
  32. singularitysql-0.1.0/tests/test_model_generation.py +127 -0
  33. singularitysql-0.1.0/tests/test_type_mapping.py +90 -0
  34. singularitysql-0.1.0/tests/test_version_detection.py +69 -0
  35. singularitysql-0.1.0/tests/test_version_strategies.py +256 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Samuel Urrego
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,220 @@
1
+ Metadata-Version: 2.4
2
+ Name: singularitysql
3
+ Version: 0.1.0
4
+ Summary: SQL Server stored procedure introspection and Pydantic model generator
5
+ Author: Samuel Urrego
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: pyodbc>=5.0
11
+ Requires-Dist: pydantic>=2.0
12
+ Requires-Dist: typer>=0.9
13
+ Requires-Dist: tomli>=2.0
14
+ Requires-Dist: rich>=15.0.0
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=8.0; extra == "dev"
17
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
18
+ Requires-Dist: pytest-mock>=3.14; extra == "dev"
19
+ Requires-Dist: ruff>=0.4; extra == "dev"
20
+ Requires-Dist: mypy>=1.10; extra == "dev"
21
+ Dynamic: license-file
22
+
23
+ <p align="center">
24
+ <img src="assets/logo.png" alt="Singularity" width="400">
25
+ </p>
26
+
27
+ > SQL Server → Pydantic v2. Automatically.
28
+
29
+ [![CI](https://github.com/Samuel-Urrego/Singularity/actions/workflows/ci.yml/badge.svg)](https://github.com/Samuel-Urrego/Singularity/actions/workflows/ci.yml)
30
+ [![Python](https://img.shields.io/pypi/pyversions/singularity.svg)](https://pypi.org/project/singularity/)
31
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
32
+
33
+ Singularity bridges the gap between SQL Server and Python. Connect to your database, point at a stored procedure, and get a **Pydantic v2 model** — as a runtime class or a `.py` file you can commit.
34
+
35
+ ```bash
36
+ singularity --config config.toml
37
+ ```
38
+
39
+ ```python
40
+ from singularity import SQLServerIntrospector, generate_model
41
+
42
+ introspector = SQLServerIntrospector("DRIVER={ODBC Driver 17};SERVER=...;DATABASE=...")
43
+ meta = introspector.introspect("usp_GetOrders")
44
+ model = generate_model(meta, mode="dynamic")
45
+ # <class 'pydantic.main.UspGetOrders'>
46
+ ```
47
+
48
+ ## Why Singularity?
49
+
50
+ **The problem**: You have dozens of complex stored procedures in SQL Server. Calling them from Python means manually writing Pydantic models for every parameter and result set. One typo, and you get a runtime error.
51
+
52
+ **The solution**: Singularity reads SQL Server's system catalog (`sys.parameters`, `sp_describe_first_result_set`) and generates the models for you. Zero manual mapping.
53
+
54
+ | Approach | Lines of code | Maintainable | Type-safe |
55
+ |---|---|---|---|
56
+ | Manual Pydantic models | 100s–1000s | ❌ | ⚠️ (manual) |
57
+ | Raw dicts / tuples | Fewer | ❌ | ❌ |
58
+ | **Singularity** | **Zero** | ✅ | ✅ |
59
+
60
+ ## Features
61
+
62
+ - 🔌 **Auto-connect** — pyodbc connection with `@@VERSION` detection
63
+ - 🧠 **Version-aware** — Modern (2016+), Legacy (2008–2014), and Azure SQL strategies
64
+ - 📦 **Two output modes**:
65
+ - `"dynamic"` — `create_model()` at runtime, usable immediately
66
+ - `"source"` — `.py` files you can commit and review
67
+ - 🏷️ **Full type mapping** — `INT`→`int`, `VARCHAR`→`str`, `DATETIME`→`datetime`, `BIT`→`bool`, etc.
68
+ - 🎨 **Configurable naming** — snake_case, camelCase, PascalCase for field names
69
+ - 🗂️ **File naming templates** — `{schema}_{sp_name}.py`, `{database}_{sp_name}.py`, etc.
70
+ - 🛡️ **Nullable awareness** — `Optional[T]` for nullable columns
71
+ - ⚡ **UV-first** — fast dependency management
72
+
73
+ ## Installation
74
+
75
+ ```bash
76
+ uv add singularity
77
+ # or
78
+ pip install singularity
79
+ ```
80
+
81
+ **Prerequisite**: [ODBC Driver for SQL Server](https://learn.microsoft.com/en-us/sql/connect/odbc/download-odbc-driver-for-sql-server) (17 or 18).
82
+
83
+ ## Quick Start
84
+
85
+ ### 1. Create a config file
86
+
87
+ ```toml
88
+ # config.toml
89
+ [connection]
90
+ server = "localhost"
91
+ database = "AdventureWorks"
92
+ driver = "ODBC Driver 18 for SQL Server"
93
+ trusted_connection = true
94
+
95
+ [sp_selection]
96
+ pattern = "usp_%"
97
+
98
+ [output]
99
+ directory = "generated_models"
100
+ mode = "source"
101
+ file_naming = "{schema}_{sp_name}.py"
102
+ naming_convention = "snake_case"
103
+ ```
104
+
105
+ ### 2. Generate models
106
+
107
+ ```bash
108
+ singularity --config config.toml
109
+ ```
110
+
111
+ ```
112
+ Connected. Detected version: modern
113
+ Introspecting usp_GetOrders... → generated_models/dbo_usp_GetOrders.py
114
+ Introspecting usp_GetCustomers... → generated_models/dbo_usp_GetCustomers.py
115
+
116
+ Done. 2 succeeded, 0 failed.
117
+ ```
118
+
119
+ ### 3. Use the generated models
120
+
121
+ ```python
122
+ from generated_models.dbo_usp_GetOrders import UspGetOrders
123
+
124
+ order = UspGetOrders(id=1, customer_name="Acme Corp", total=99.99)
125
+ ```
126
+
127
+ ## Library Usage
128
+
129
+ ```python
130
+ from singularity import SQLServerIntrospector, generate_model
131
+
132
+ # Connect and introspect
133
+ introspector = SQLServerIntrospector(conn_str)
134
+ introspector.connect()
135
+ version = introspector.detect_version() # ServerVersion.MODERN
136
+ metadata = introspector.introspect("usp_GetOrders")
137
+
138
+ # Runtime model
139
+ DynamicModel = generate_model(metadata, mode="dynamic")
140
+ instance = DynamicModel(id=1, customer_name="Acme")
141
+
142
+ # Source code string
143
+ source_code = generate_model(metadata, mode="source")
144
+ with open("models/usp_GetOrders.py", "w") as f:
145
+ f.write(source_code)
146
+ ```
147
+
148
+ ## Configuration Reference
149
+
150
+ ### `[connection]`
151
+
152
+ | Field | Required | Default | Description |
153
+ |---|---|---|---|
154
+ | `server` | ✅ | — | Server hostname or IP |
155
+ | `database` | ✅ | — | Database name |
156
+ | `driver` | ❌ | `ODBC Driver 18 for SQL Server` | ODBC driver name |
157
+ | `trusted_connection` | ❌ | `true` | Use Windows auth |
158
+ | `username` | ❌ | — | SQL auth username |
159
+ | `password` | ❌ | — | SQL auth password |
160
+
161
+ ### `[sp_selection]`
162
+
163
+ | Field | Required | Description |
164
+ |---|---|---|
165
+ | `procedures` | ❌ | Explicit list of SP names |
166
+ | `pattern` | ❌ | Wildcard pattern (e.g. `usp_%`) |
167
+
168
+ At least one of `procedures` or `pattern` must be specified.
169
+
170
+ ### `[output]`
171
+
172
+ | Field | Required | Default | Description |
173
+ |---|---|---|---|
174
+ | `directory` | ❌ | `.` | Output directory |
175
+ | `mode` | ❌ | `source` | `source` or `dynamic` |
176
+ | `file_naming` | ❌ | `{sp_name}.py` | Template with `{schema}`, `{database}`, `{sp_name}` |
177
+ | `naming_convention` | ❌ | `snake_case` | `snake_case`, `camelCase`, or `PascalCase` |
178
+
179
+ ## Supported SQL Server Versions
180
+
181
+ | Version | Strategy | Parameter introspection | Result set metadata |
182
+ |---|---|---|---|
183
+ | 2016+ | Modern | `sys.parameters` | `sp_describe_first_result_set` |
184
+ | 2008–2014 | Legacy | `sys.parameters` | `sp_describe_first_result_set` + `sys.columns` fallback |
185
+ | Azure SQL | Azure | `sys.parameters` | `sys.dm_exec_describe_first_result_set` |
186
+
187
+ ## Type Mapping
188
+
189
+ | SQL Server | Python | Pydantic |
190
+ |---|---|---|
191
+ | `INT`, `BIGINT`, `SMALLINT`, `TINYINT` | `int` | `int` |
192
+ | `VARCHAR`, `NVARCHAR`, `CHAR`, `NCHAR`, `TEXT` | `str` | `str` |
193
+ | `DATETIME`, `DATETIME2`, `DATE`, `SMALLDATETIME` | `datetime` | `datetime` |
194
+ | `BIT` | `bool` | `bool` |
195
+ | `DECIMAL`, `NUMERIC`, `FLOAT`, `REAL`, `MONEY` | `float` | `float` |
196
+ | `UNIQUEIDENTIFIER` | `str` | `str` |
197
+ | Unknown types | `str` + warning | `str` |
198
+
199
+ ## Development
200
+
201
+ ```bash
202
+ # Clone and install
203
+ git clone https://github.com/Samuel-Urrego/Singularity
204
+ cd singularity
205
+ uv sync
206
+
207
+ # Run tests
208
+ uv run pytest
209
+
210
+ # Lint and type-check
211
+ uv run ruff check .
212
+ uv run mypy singularity/
213
+
214
+ # Install pre-commit hooks
215
+ uv run pre-commit install
216
+ ```
217
+
218
+ ## License
219
+
220
+ MIT — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,198 @@
1
+ <p align="center">
2
+ <img src="assets/logo.png" alt="Singularity" width="400">
3
+ </p>
4
+
5
+ > SQL Server → Pydantic v2. Automatically.
6
+
7
+ [![CI](https://github.com/Samuel-Urrego/Singularity/actions/workflows/ci.yml/badge.svg)](https://github.com/Samuel-Urrego/Singularity/actions/workflows/ci.yml)
8
+ [![Python](https://img.shields.io/pypi/pyversions/singularity.svg)](https://pypi.org/project/singularity/)
9
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
10
+
11
+ Singularity bridges the gap between SQL Server and Python. Connect to your database, point at a stored procedure, and get a **Pydantic v2 model** — as a runtime class or a `.py` file you can commit.
12
+
13
+ ```bash
14
+ singularity --config config.toml
15
+ ```
16
+
17
+ ```python
18
+ from singularity import SQLServerIntrospector, generate_model
19
+
20
+ introspector = SQLServerIntrospector("DRIVER={ODBC Driver 17};SERVER=...;DATABASE=...")
21
+ meta = introspector.introspect("usp_GetOrders")
22
+ model = generate_model(meta, mode="dynamic")
23
+ # <class 'pydantic.main.UspGetOrders'>
24
+ ```
25
+
26
+ ## Why Singularity?
27
+
28
+ **The problem**: You have dozens of complex stored procedures in SQL Server. Calling them from Python means manually writing Pydantic models for every parameter and result set. One typo, and you get a runtime error.
29
+
30
+ **The solution**: Singularity reads SQL Server's system catalog (`sys.parameters`, `sp_describe_first_result_set`) and generates the models for you. Zero manual mapping.
31
+
32
+ | Approach | Lines of code | Maintainable | Type-safe |
33
+ |---|---|---|---|
34
+ | Manual Pydantic models | 100s–1000s | ❌ | ⚠️ (manual) |
35
+ | Raw dicts / tuples | Fewer | ❌ | ❌ |
36
+ | **Singularity** | **Zero** | ✅ | ✅ |
37
+
38
+ ## Features
39
+
40
+ - 🔌 **Auto-connect** — pyodbc connection with `@@VERSION` detection
41
+ - 🧠 **Version-aware** — Modern (2016+), Legacy (2008–2014), and Azure SQL strategies
42
+ - 📦 **Two output modes**:
43
+ - `"dynamic"` — `create_model()` at runtime, usable immediately
44
+ - `"source"` — `.py` files you can commit and review
45
+ - 🏷️ **Full type mapping** — `INT`→`int`, `VARCHAR`→`str`, `DATETIME`→`datetime`, `BIT`→`bool`, etc.
46
+ - 🎨 **Configurable naming** — snake_case, camelCase, PascalCase for field names
47
+ - 🗂️ **File naming templates** — `{schema}_{sp_name}.py`, `{database}_{sp_name}.py`, etc.
48
+ - 🛡️ **Nullable awareness** — `Optional[T]` for nullable columns
49
+ - ⚡ **UV-first** — fast dependency management
50
+
51
+ ## Installation
52
+
53
+ ```bash
54
+ uv add singularity
55
+ # or
56
+ pip install singularity
57
+ ```
58
+
59
+ **Prerequisite**: [ODBC Driver for SQL Server](https://learn.microsoft.com/en-us/sql/connect/odbc/download-odbc-driver-for-sql-server) (17 or 18).
60
+
61
+ ## Quick Start
62
+
63
+ ### 1. Create a config file
64
+
65
+ ```toml
66
+ # config.toml
67
+ [connection]
68
+ server = "localhost"
69
+ database = "AdventureWorks"
70
+ driver = "ODBC Driver 18 for SQL Server"
71
+ trusted_connection = true
72
+
73
+ [sp_selection]
74
+ pattern = "usp_%"
75
+
76
+ [output]
77
+ directory = "generated_models"
78
+ mode = "source"
79
+ file_naming = "{schema}_{sp_name}.py"
80
+ naming_convention = "snake_case"
81
+ ```
82
+
83
+ ### 2. Generate models
84
+
85
+ ```bash
86
+ singularity --config config.toml
87
+ ```
88
+
89
+ ```
90
+ Connected. Detected version: modern
91
+ Introspecting usp_GetOrders... → generated_models/dbo_usp_GetOrders.py
92
+ Introspecting usp_GetCustomers... → generated_models/dbo_usp_GetCustomers.py
93
+
94
+ Done. 2 succeeded, 0 failed.
95
+ ```
96
+
97
+ ### 3. Use the generated models
98
+
99
+ ```python
100
+ from generated_models.dbo_usp_GetOrders import UspGetOrders
101
+
102
+ order = UspGetOrders(id=1, customer_name="Acme Corp", total=99.99)
103
+ ```
104
+
105
+ ## Library Usage
106
+
107
+ ```python
108
+ from singularity import SQLServerIntrospector, generate_model
109
+
110
+ # Connect and introspect
111
+ introspector = SQLServerIntrospector(conn_str)
112
+ introspector.connect()
113
+ version = introspector.detect_version() # ServerVersion.MODERN
114
+ metadata = introspector.introspect("usp_GetOrders")
115
+
116
+ # Runtime model
117
+ DynamicModel = generate_model(metadata, mode="dynamic")
118
+ instance = DynamicModel(id=1, customer_name="Acme")
119
+
120
+ # Source code string
121
+ source_code = generate_model(metadata, mode="source")
122
+ with open("models/usp_GetOrders.py", "w") as f:
123
+ f.write(source_code)
124
+ ```
125
+
126
+ ## Configuration Reference
127
+
128
+ ### `[connection]`
129
+
130
+ | Field | Required | Default | Description |
131
+ |---|---|---|---|
132
+ | `server` | ✅ | — | Server hostname or IP |
133
+ | `database` | ✅ | — | Database name |
134
+ | `driver` | ❌ | `ODBC Driver 18 for SQL Server` | ODBC driver name |
135
+ | `trusted_connection` | ❌ | `true` | Use Windows auth |
136
+ | `username` | ❌ | — | SQL auth username |
137
+ | `password` | ❌ | — | SQL auth password |
138
+
139
+ ### `[sp_selection]`
140
+
141
+ | Field | Required | Description |
142
+ |---|---|---|
143
+ | `procedures` | ❌ | Explicit list of SP names |
144
+ | `pattern` | ❌ | Wildcard pattern (e.g. `usp_%`) |
145
+
146
+ At least one of `procedures` or `pattern` must be specified.
147
+
148
+ ### `[output]`
149
+
150
+ | Field | Required | Default | Description |
151
+ |---|---|---|---|
152
+ | `directory` | ❌ | `.` | Output directory |
153
+ | `mode` | ❌ | `source` | `source` or `dynamic` |
154
+ | `file_naming` | ❌ | `{sp_name}.py` | Template with `{schema}`, `{database}`, `{sp_name}` |
155
+ | `naming_convention` | ❌ | `snake_case` | `snake_case`, `camelCase`, or `PascalCase` |
156
+
157
+ ## Supported SQL Server Versions
158
+
159
+ | Version | Strategy | Parameter introspection | Result set metadata |
160
+ |---|---|---|---|
161
+ | 2016+ | Modern | `sys.parameters` | `sp_describe_first_result_set` |
162
+ | 2008–2014 | Legacy | `sys.parameters` | `sp_describe_first_result_set` + `sys.columns` fallback |
163
+ | Azure SQL | Azure | `sys.parameters` | `sys.dm_exec_describe_first_result_set` |
164
+
165
+ ## Type Mapping
166
+
167
+ | SQL Server | Python | Pydantic |
168
+ |---|---|---|
169
+ | `INT`, `BIGINT`, `SMALLINT`, `TINYINT` | `int` | `int` |
170
+ | `VARCHAR`, `NVARCHAR`, `CHAR`, `NCHAR`, `TEXT` | `str` | `str` |
171
+ | `DATETIME`, `DATETIME2`, `DATE`, `SMALLDATETIME` | `datetime` | `datetime` |
172
+ | `BIT` | `bool` | `bool` |
173
+ | `DECIMAL`, `NUMERIC`, `FLOAT`, `REAL`, `MONEY` | `float` | `float` |
174
+ | `UNIQUEIDENTIFIER` | `str` | `str` |
175
+ | Unknown types | `str` + warning | `str` |
176
+
177
+ ## Development
178
+
179
+ ```bash
180
+ # Clone and install
181
+ git clone https://github.com/Samuel-Urrego/Singularity
182
+ cd singularity
183
+ uv sync
184
+
185
+ # Run tests
186
+ uv run pytest
187
+
188
+ # Lint and type-check
189
+ uv run ruff check .
190
+ uv run mypy singularity/
191
+
192
+ # Install pre-commit hooks
193
+ uv run pre-commit install
194
+ ```
195
+
196
+ ## License
197
+
198
+ MIT — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,56 @@
1
+ [project]
2
+ name = "singularitysql"
3
+ version = "0.1.0"
4
+ description = "SQL Server stored procedure introspection and Pydantic model generator"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = "MIT"
8
+ authors = [
9
+ { name = "Samuel Urrego" },
10
+ ]
11
+
12
+ dependencies = [
13
+ "pyodbc>=5.0",
14
+ "pydantic>=2.0",
15
+ "typer>=0.9",
16
+ "tomli>=2.0",
17
+ "rich>=15.0.0",
18
+ ]
19
+
20
+ [project.scripts]
21
+ singularity = "singularity.cli._app:_main"
22
+
23
+ [project.optional-dependencies]
24
+ dev = [
25
+ "pytest>=8.0",
26
+ "pytest-cov>=5.0",
27
+ "pytest-mock>=3.14",
28
+ "ruff>=0.4",
29
+ "mypy>=1.10",
30
+ ]
31
+
32
+ [tool.pytest.ini_options]
33
+ testpaths = ["tests"]
34
+ markers = [
35
+ "integration: marks tests that require a real SQL Server database (deselect with '-m \"not integration\"')",
36
+ ]
37
+
38
+ [tool.ruff]
39
+ target-version = "py310"
40
+ line-length = 100
41
+
42
+ [tool.ruff.lint]
43
+ select = ["E", "F", "W", "I", "N", "UP", "B", "SIM"]
44
+
45
+ [tool.mypy]
46
+ python_version = "3.10"
47
+ strict = true
48
+ warn_unused_ignores = true
49
+ ignore_missing_imports = true
50
+
51
+ [build-system]
52
+ requires = ["setuptools>=68.0"]
53
+ build-backend = "setuptools.build_meta"
54
+
55
+ [tool.setuptools.packages.find]
56
+ exclude = ["assets", "assets.*", "tests", "tests.*", "generated_models", "docs"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,18 @@
1
+ """Singularity — SQL Server stored procedure introspection and Pydantic model generation."""
2
+
3
+ from singularity.exceptions import SingularityError, SpNotFoundError
4
+ from singularity.introspector import SQLServerIntrospector
5
+ from singularity.model_generator import generate_model
6
+ from singularity.types import ColumnInfo, Parameter, SPMetadata
7
+ from singularity.version import ServerVersion
8
+
9
+ __all__ = [
10
+ "SQLServerIntrospector",
11
+ "generate_model",
12
+ "SPMetadata",
13
+ "ColumnInfo",
14
+ "Parameter",
15
+ "ServerVersion",
16
+ "SingularityError",
17
+ "SpNotFoundError",
18
+ ]
@@ -0,0 +1,8 @@
1
+ """Singularity CLI — Typer app and commands."""
2
+
3
+ from singularity.cli._app import _main, app
4
+
5
+ __all__ = ["app", "main"]
6
+
7
+ # main is an alias for _main for entry point consistency
8
+ main = _main
@@ -0,0 +1,25 @@
1
+ """Singularity Typer app instance — isolated to avoid circular imports."""
2
+
3
+ import sys
4
+
5
+ import typer
6
+
7
+
8
+ def _main() -> None:
9
+ """Entry point with UTF-8 encoding support for Windows consoles."""
10
+ if sys.platform == "win32":
11
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
12
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
13
+ app()
14
+
15
+
16
+ app = typer.Typer(
17
+ name="singularity",
18
+ help="SQL Server stored procedure introspection and Pydantic model generator.",
19
+ no_args_is_help=True,
20
+ invoke_without_command=True,
21
+ )
22
+
23
+ # Register commands — import triggers @app.command() decorators
24
+ import singularity.cli.docs_generate # noqa: E402, F401 — registers docs command
25
+ import singularity.cli.generate # noqa: E402, F401 — registers generate command
@@ -0,0 +1,142 @@
1
+ """TOML configuration loading and Pydantic config models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Literal
7
+
8
+ from pydantic import BaseModel, field_validator
9
+
10
+
11
+ class ConnectionConfig(BaseModel):
12
+ """SQL Server connection settings."""
13
+
14
+ server: str
15
+ """Server hostname or IP address (required)."""
16
+
17
+ database: str
18
+ """Database name (required)."""
19
+
20
+ driver: str = "ODBC Driver 18 for SQL Server"
21
+ """ODBC driver name."""
22
+
23
+ trusted_connection: bool = True
24
+ """Use Windows Authentication when True."""
25
+
26
+ username: str | None = None
27
+ """SQL Server login username (used when trusted_connection is False)."""
28
+
29
+ password: str | None = None
30
+ """SQL Server login password (used when trusted_connection is False)."""
31
+
32
+ def build_connection_string(self) -> str:
33
+ """Build a pyodbc-compatible connection string from the config.
34
+
35
+ Returns:
36
+ A DRIVER=SERVER=DATABASE=... connection string.
37
+ """
38
+ parts = [
39
+ f"DRIVER={{{self.driver}}}",
40
+ f"SERVER={self.server}",
41
+ f"DATABASE={self.database}",
42
+ ]
43
+ if self.trusted_connection:
44
+ parts.append("Trusted_Connection=yes")
45
+ else:
46
+ if self.username:
47
+ parts.append(f"UID={self.username}")
48
+ if self.password:
49
+ parts.append(f"PWD={self.password}")
50
+ return ";".join(parts)
51
+
52
+
53
+ class SpSelectionConfig(BaseModel):
54
+ """Stored procedure selection criteria."""
55
+
56
+ procedures: list[str] | None = None
57
+ """Explicit list of stored procedure names to introspect."""
58
+
59
+ pattern: str | None = None
60
+ """Wildcard pattern (e.g. 'usp_%') to resolve against the database."""
61
+
62
+ @field_validator("procedures", "pattern")
63
+ @classmethod
64
+ def _at_least_one(cls, v: list[str] | str | None) -> list[str] | str | None:
65
+ return v
66
+
67
+
68
+ class OutputConfig(BaseModel):
69
+ """Output preferences for generated models."""
70
+
71
+ directory: str = "."
72
+ """Output directory for generated files."""
73
+
74
+ mode: Literal["dynamic", "source"] = "source"
75
+ """Generation mode: 'dynamic' for runtime models, 'source' for .py files."""
76
+
77
+ file_naming: str = "{sp_name}.py"
78
+ """File naming template with {sp_name}, {schema}, {database} variables."""
79
+
80
+ naming_convention: Literal["snake_case", "camelCase", "PascalCase"] = "snake_case"
81
+ """Field naming convention for generated models."""
82
+
83
+ docs_directory: str | None = None
84
+ """Output directory for generated Markdown documentation.
85
+ If set, ``singularity docs`` writes docs here. Defaults to ``docs/``."""
86
+
87
+ @field_validator("mode")
88
+ @classmethod
89
+ def _validate_mode(cls, v: str) -> str:
90
+ allowed = {"dynamic", "source"}
91
+ if v not in allowed:
92
+ raise ValueError(f"mode must be one of {allowed}, got '{v}'")
93
+ return v
94
+
95
+ @field_validator("naming_convention")
96
+ @classmethod
97
+ def _validate_convention(cls, v: str) -> str:
98
+ allowed = {"snake_case", "camelCase", "PascalCase"}
99
+ if v not in allowed:
100
+ raise ValueError(
101
+ f"naming_convention must be one of {allowed}, got '{v}'"
102
+ )
103
+ return v
104
+
105
+
106
+ class SingularityConfig(BaseModel):
107
+ """Top-level configuration model."""
108
+
109
+ connection: ConnectionConfig
110
+ """SQL Server connection settings."""
111
+
112
+ sp_selection: SpSelectionConfig
113
+ """Stored procedure selection criteria."""
114
+
115
+ output: OutputConfig = OutputConfig()
116
+ """Output preferences (defaults apply if section omitted)."""
117
+
118
+
119
+ def load_config(path: str | Path) -> SingularityConfig:
120
+ """Load and validate a TOML configuration file.
121
+
122
+ Args:
123
+ path: Path to the TOML config file.
124
+
125
+ Returns:
126
+ A validated SingularityConfig instance.
127
+
128
+ Raises:
129
+ FileNotFoundError: If the config file does not exist.
130
+ pydantic.ValidationError: If the config is invalid.
131
+ """
132
+ import tomli
133
+
134
+ path = Path(path)
135
+
136
+ if not path.exists():
137
+ raise FileNotFoundError(f"Config file not found: {path}")
138
+
139
+ raw = path.read_text(encoding="utf-8")
140
+ data = tomli.loads(raw)
141
+
142
+ return SingularityConfig(**data)