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.
- singularitysql-0.1.0/LICENSE +21 -0
- singularitysql-0.1.0/PKG-INFO +220 -0
- singularitysql-0.1.0/README.md +198 -0
- singularitysql-0.1.0/pyproject.toml +56 -0
- singularitysql-0.1.0/setup.cfg +4 -0
- singularitysql-0.1.0/singularity/__init__.py +18 -0
- singularitysql-0.1.0/singularity/cli/__init__.py +8 -0
- singularitysql-0.1.0/singularity/cli/_app.py +25 -0
- singularitysql-0.1.0/singularity/cli/config.py +142 -0
- singularitysql-0.1.0/singularity/cli/docs_generate.py +196 -0
- singularitysql-0.1.0/singularity/cli/generate.py +210 -0
- singularitysql-0.1.0/singularity/exceptions.py +13 -0
- singularitysql-0.1.0/singularity/executor.py +209 -0
- singularitysql-0.1.0/singularity/introspector.py +108 -0
- singularitysql-0.1.0/singularity/model_generator.py +551 -0
- singularitysql-0.1.0/singularity/types.py +73 -0
- singularitysql-0.1.0/singularity/version/__init__.py +80 -0
- singularitysql-0.1.0/singularity/version/_azure.py +122 -0
- singularitysql-0.1.0/singularity/version/_base.py +73 -0
- singularitysql-0.1.0/singularity/version/_legacy.py +150 -0
- singularitysql-0.1.0/singularity/version/_modern.py +122 -0
- singularitysql-0.1.0/singularitysql.egg-info/PKG-INFO +220 -0
- singularitysql-0.1.0/singularitysql.egg-info/SOURCES.txt +33 -0
- singularitysql-0.1.0/singularitysql.egg-info/dependency_links.txt +1 -0
- singularitysql-0.1.0/singularitysql.egg-info/entry_points.txt +2 -0
- singularitysql-0.1.0/singularitysql.egg-info/requires.txt +12 -0
- singularitysql-0.1.0/singularitysql.egg-info/top_level.txt +2 -0
- singularitysql-0.1.0/tests/test_cli_generate.py +61 -0
- singularitysql-0.1.0/tests/test_config_loading.py +197 -0
- singularitysql-0.1.0/tests/test_field_sanitization.py +104 -0
- singularitysql-0.1.0/tests/test_introspector.py +119 -0
- singularitysql-0.1.0/tests/test_model_generation.py +127 -0
- singularitysql-0.1.0/tests/test_type_mapping.py +90 -0
- singularitysql-0.1.0/tests/test_version_detection.py +69 -0
- 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
|
+
[](https://github.com/Samuel-Urrego/Singularity/actions/workflows/ci.yml)
|
|
30
|
+
[](https://pypi.org/project/singularity/)
|
|
31
|
+
[](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
|
+
[](https://github.com/Samuel-Urrego/Singularity/actions/workflows/ci.yml)
|
|
8
|
+
[](https://pypi.org/project/singularity/)
|
|
9
|
+
[](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,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,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)
|