sqlacodegen 3.2.0__tar.gz → 4.0.0rc2__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.
- sqlacodegen-4.0.0rc2/.github/FUNDING.yml +1 -0
- sqlacodegen-4.0.0rc2/.github/dependabot.yml +13 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/.github/workflows/publish.yml +8 -5
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/.github/workflows/test.yml +3 -3
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/.pre-commit-config.yaml +2 -2
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/CHANGES.rst +16 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/PKG-INFO +14 -7
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/README.rst +13 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/pyproject.toml +10 -8
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen/generators.py +182 -39
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen/utils.py +1 -7
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen.egg-info/PKG-INFO +14 -7
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen.egg-info/SOURCES.txt +2 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen.egg-info/requires.txt +0 -7
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/tests/test_generator_declarative.py +390 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/tests/test_generator_sqlmodel.py +65 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/tests/test_generator_tables.py +98 -4
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/.github/ISSUE_TEMPLATE/bug_report.yaml +0 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/.github/ISSUE_TEMPLATE/features_request.yaml +0 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/.github/pull_request_template.md +0 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/.gitignore +0 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/CONTRIBUTING.rst +0 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/LICENSE +0 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/setup.cfg +0 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen/__init__.py +0 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen/__main__.py +0 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen/cli.py +0 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen/models.py +0 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen/py.typed +0 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen.egg-info/dependency_links.txt +0 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen.egg-info/entry_points.txt +0 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen.egg-info/top_level.txt +0 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/tests/__init__.py +0 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/tests/conftest.py +0 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/tests/test_cli.py +0 -0
- {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/tests/test_generator_dataclass.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tidelift: pypi/sqlacodegen
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Keep GitHub Actions up to date with GitHub's Dependabot...
|
|
2
|
+
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
|
|
3
|
+
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
|
|
4
|
+
version: 2
|
|
5
|
+
updates:
|
|
6
|
+
- package-ecosystem: github-actions
|
|
7
|
+
directory: /
|
|
8
|
+
groups:
|
|
9
|
+
github-actions:
|
|
10
|
+
patterns:
|
|
11
|
+
- "*" # Group all Actions updates into a single larger pull request
|
|
12
|
+
schedule:
|
|
13
|
+
interval: quarterly
|
|
@@ -14,9 +14,9 @@ jobs:
|
|
|
14
14
|
runs-on: ubuntu-latest
|
|
15
15
|
environment: release
|
|
16
16
|
steps:
|
|
17
|
-
- uses: actions/checkout@
|
|
17
|
+
- uses: actions/checkout@v6
|
|
18
18
|
- name: Set up Python
|
|
19
|
-
uses: actions/setup-python@
|
|
19
|
+
uses: actions/setup-python@v6
|
|
20
20
|
with:
|
|
21
21
|
python-version: 3.x
|
|
22
22
|
- name: Install dependencies
|
|
@@ -24,7 +24,7 @@ jobs:
|
|
|
24
24
|
- name: Create packages
|
|
25
25
|
run: python -m build
|
|
26
26
|
- name: Archive packages
|
|
27
|
-
uses: actions/upload-artifact@
|
|
27
|
+
uses: actions/upload-artifact@v6
|
|
28
28
|
with:
|
|
29
29
|
name: dist
|
|
30
30
|
path: dist
|
|
@@ -38,7 +38,10 @@ jobs:
|
|
|
38
38
|
id-token: write
|
|
39
39
|
steps:
|
|
40
40
|
- name: Retrieve packages
|
|
41
|
-
uses: actions/download-artifact@
|
|
41
|
+
uses: actions/download-artifact@v7
|
|
42
|
+
with:
|
|
43
|
+
name: dist
|
|
44
|
+
path: dist
|
|
42
45
|
- name: Upload packages
|
|
43
46
|
uses: pypa/gh-action-pypi-publish@release/v1
|
|
44
47
|
|
|
@@ -49,7 +52,7 @@ jobs:
|
|
|
49
52
|
permissions:
|
|
50
53
|
contents: write
|
|
51
54
|
steps:
|
|
52
|
-
- uses: actions/checkout@
|
|
55
|
+
- uses: actions/checkout@v6
|
|
53
56
|
- id: changelog
|
|
54
57
|
uses: agronholm/release-notes@v1
|
|
55
58
|
with:
|
|
@@ -13,16 +13,16 @@ jobs:
|
|
|
13
13
|
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
14
14
|
runs-on: ubuntu-latest
|
|
15
15
|
steps:
|
|
16
|
-
- uses: actions/checkout@
|
|
16
|
+
- uses: actions/checkout@v6
|
|
17
17
|
- name: Set up Python ${{ matrix.python-version }}
|
|
18
|
-
uses: actions/setup-python@
|
|
18
|
+
uses: actions/setup-python@v6
|
|
19
19
|
with:
|
|
20
20
|
python-version: ${{ matrix.python-version }}
|
|
21
21
|
allow-prereleases: true
|
|
22
22
|
cache: pip
|
|
23
23
|
cache-dependency-path: pyproject.toml
|
|
24
24
|
- name: Install dependencies
|
|
25
|
-
run: pip install -e .[
|
|
25
|
+
run: pip install --group test -e .[sqlmodel,citext,geoalchemy2,pgvector]
|
|
26
26
|
- name: Test with pytest
|
|
27
27
|
run: coverage run -m pytest
|
|
28
28
|
- name: Upload Coverage
|
|
@@ -16,14 +16,14 @@ repos:
|
|
|
16
16
|
- id: trailing-whitespace
|
|
17
17
|
|
|
18
18
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
19
|
-
rev: v0.
|
|
19
|
+
rev: v0.14.10
|
|
20
20
|
hooks:
|
|
21
21
|
- id: ruff
|
|
22
22
|
args: [--fix, --show-fixes]
|
|
23
23
|
- id: ruff-format
|
|
24
24
|
|
|
25
25
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
26
|
-
rev: v1.
|
|
26
|
+
rev: v1.19.1
|
|
27
27
|
hooks:
|
|
28
28
|
- id: mypy
|
|
29
29
|
additional_dependencies:
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
Version history
|
|
2
2
|
===============
|
|
3
3
|
|
|
4
|
+
**4.0.0rc2**
|
|
5
|
+
|
|
6
|
+
- Add ``values_callable`` lambda to generated native enums column definitions.
|
|
7
|
+
This allows for proper enum value insertion when working with ORM models (PR by @sheinbergon)
|
|
8
|
+
|
|
9
|
+
**4.0.0rc1**
|
|
10
|
+
|
|
11
|
+
- **BACKWARD INCOMPATIBLE** ``TablesGenerator.render_column_type()`` was changed to
|
|
12
|
+
receive the ``Column`` object instead of the column type object as its sole argument
|
|
13
|
+
- Added Python enum generation for native database ENUM types (e.g., PostgreSQL / MySQL ENUM).
|
|
14
|
+
Retained synthetic Python enum generation from CHECK constraints with
|
|
15
|
+
IN clauses (e.g., ``column IN ('val1', 'val2', ...)``). Use ``--options nonativeenums`` to
|
|
16
|
+
disable enum generation for native database enums. Use ``--options nosyntheticenums`` to
|
|
17
|
+
disable enum generation for synthetic database enums (VARCHAR columns with check constraints).
|
|
18
|
+
(PR by @sheinbergon)
|
|
19
|
+
|
|
4
20
|
**3.2.0**
|
|
5
21
|
|
|
6
22
|
- Dropped support for Python 3.9
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlacodegen
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.0rc2
|
|
4
4
|
Summary: Automatic model code generator for SQLAlchemy
|
|
5
5
|
Author-email: Alex Grönholm <alex.gronholm@nextday.fi>
|
|
6
6
|
Maintainer-email: Idan Sheinberg <ishinberg0@gmail.com>
|
|
@@ -25,12 +25,6 @@ Description-Content-Type: text/x-rst
|
|
|
25
25
|
License-File: LICENSE
|
|
26
26
|
Requires-Dist: SQLAlchemy>=2.0.29
|
|
27
27
|
Requires-Dist: inflect>=4.0.0
|
|
28
|
-
Provides-Extra: test
|
|
29
|
-
Requires-Dist: sqlacodegen[geoalchemy2,pgvector,sqlmodel]; extra == "test"
|
|
30
|
-
Requires-Dist: pytest>=7.4; extra == "test"
|
|
31
|
-
Requires-Dist: coverage>=7; extra == "test"
|
|
32
|
-
Requires-Dist: psycopg[binary]; extra == "test"
|
|
33
|
-
Requires-Dist: mysql-connector-python; extra == "test"
|
|
34
28
|
Provides-Extra: sqlmodel
|
|
35
29
|
Requires-Dist: sqlmodel>=0.0.22; extra == "sqlmodel"
|
|
36
30
|
Provides-Extra: citext
|
|
@@ -47,6 +41,9 @@ Dynamic: license-file
|
|
|
47
41
|
.. image:: https://coveralls.io/repos/github/agronholm/sqlacodegen/badge.svg?branch=master
|
|
48
42
|
:target: https://coveralls.io/github/agronholm/sqlacodegen?branch=master
|
|
49
43
|
:alt: Code Coverage
|
|
44
|
+
.. image:: https://tidelift.com/badges/package/pypi/sqlacodegen
|
|
45
|
+
:target: https://tidelift.com/subscription/pkg/pypi-sqlacodegen
|
|
46
|
+
:alt: Tidelift
|
|
50
47
|
|
|
51
48
|
This is a tool that reads the structure of an existing database and generates the
|
|
52
49
|
appropriate SQLAlchemy model code, using the declarative style if possible.
|
|
@@ -146,6 +143,8 @@ values must be delimited by commas, e.g. ``--options noconstraints,nobidi``):
|
|
|
146
143
|
* ``noconstraints``: ignore constraints (foreign key, unique etc.)
|
|
147
144
|
* ``nocomments``: ignore table/column comments
|
|
148
145
|
* ``noindexes``: ignore indexes
|
|
146
|
+
* ``nonativeenums``: don't generate Python enum classes for native database ENUM types (e.g., PostgreSQL ENUM); use plain string mapping instead
|
|
147
|
+
* ``nosyntheticenums``: don't generate Python enum classes from CHECK constraints with IN clauses (e.g., ``column IN ('value1', 'value2', ...)``); preserves CHECK constraints as-is
|
|
149
148
|
* ``noidsuffix``: prevent the special naming logic for single column many-to-one
|
|
150
149
|
and one-to-one relationships (see `Relationship naming logic`_ for details)
|
|
151
150
|
* ``include_dialect_options``: render a table' dialect options, such as ``starrocks_partition`` for StarRocks' specific options.
|
|
@@ -251,3 +250,11 @@ sqlalchemy_ room on Gitter.
|
|
|
251
250
|
|
|
252
251
|
.. _sqlacodegen discussion forum: https://github.com/agronholm/sqlacodegen/discussions/categories/q-a
|
|
253
252
|
.. _sqlalchemy: https://app.gitter.im/#/room/#sqlalchemy_community:gitter.im
|
|
253
|
+
|
|
254
|
+
Security contact information
|
|
255
|
+
============================
|
|
256
|
+
|
|
257
|
+
To report a security vulnerability, please use the `Tidelift security contact`_.
|
|
258
|
+
Tidelift will coordinate the fix and disclosure.
|
|
259
|
+
|
|
260
|
+
.. _Tidelift security contact: https://tidelift.com/security
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
.. image:: https://coveralls.io/repos/github/agronholm/sqlacodegen/badge.svg?branch=master
|
|
5
5
|
:target: https://coveralls.io/github/agronholm/sqlacodegen?branch=master
|
|
6
6
|
:alt: Code Coverage
|
|
7
|
+
.. image:: https://tidelift.com/badges/package/pypi/sqlacodegen
|
|
8
|
+
:target: https://tidelift.com/subscription/pkg/pypi-sqlacodegen
|
|
9
|
+
:alt: Tidelift
|
|
7
10
|
|
|
8
11
|
This is a tool that reads the structure of an existing database and generates the
|
|
9
12
|
appropriate SQLAlchemy model code, using the declarative style if possible.
|
|
@@ -103,6 +106,8 @@ values must be delimited by commas, e.g. ``--options noconstraints,nobidi``):
|
|
|
103
106
|
* ``noconstraints``: ignore constraints (foreign key, unique etc.)
|
|
104
107
|
* ``nocomments``: ignore table/column comments
|
|
105
108
|
* ``noindexes``: ignore indexes
|
|
109
|
+
* ``nonativeenums``: don't generate Python enum classes for native database ENUM types (e.g., PostgreSQL ENUM); use plain string mapping instead
|
|
110
|
+
* ``nosyntheticenums``: don't generate Python enum classes from CHECK constraints with IN clauses (e.g., ``column IN ('value1', 'value2', ...)``); preserves CHECK constraints as-is
|
|
106
111
|
* ``noidsuffix``: prevent the special naming logic for single column many-to-one
|
|
107
112
|
and one-to-one relationships (see `Relationship naming logic`_ for details)
|
|
108
113
|
* ``include_dialect_options``: render a table' dialect options, such as ``starrocks_partition`` for StarRocks' specific options.
|
|
@@ -208,3 +213,11 @@ sqlalchemy_ room on Gitter.
|
|
|
208
213
|
|
|
209
214
|
.. _sqlacodegen discussion forum: https://github.com/agronholm/sqlacodegen/discussions/categories/q-a
|
|
210
215
|
.. _sqlalchemy: https://app.gitter.im/#/room/#sqlalchemy_community:gitter.im
|
|
216
|
+
|
|
217
|
+
Security contact information
|
|
218
|
+
============================
|
|
219
|
+
|
|
220
|
+
To report a security vulnerability, please use the `Tidelift security contact`_.
|
|
221
|
+
Tidelift will coordinate the fix and disclosure.
|
|
222
|
+
|
|
223
|
+
.. _Tidelift security contact: https://tidelift.com/security
|
|
@@ -39,13 +39,6 @@ dynamic = ["version"]
|
|
|
39
39
|
"Source Code" = "https://github.com/agronholm/sqlacodegen"
|
|
40
40
|
|
|
41
41
|
[project.optional-dependencies]
|
|
42
|
-
test = [
|
|
43
|
-
"sqlacodegen[sqlmodel,pgvector,geoalchemy2]",
|
|
44
|
-
"pytest >= 7.4",
|
|
45
|
-
"coverage >= 7",
|
|
46
|
-
"psycopg[binary]",
|
|
47
|
-
"mysql-connector-python",
|
|
48
|
-
]
|
|
49
42
|
sqlmodel = ["sqlmodel >= 0.0.22"]
|
|
50
43
|
citext = ["sqlalchemy-citext >= 1.7.0"]
|
|
51
44
|
geoalchemy2 = ["geoalchemy2 >= 0.17.0"]
|
|
@@ -60,6 +53,14 @@ sqlmodels = "sqlacodegen.generators:SQLModelGenerator"
|
|
|
60
53
|
[project.scripts]
|
|
61
54
|
sqlacodegen = "sqlacodegen.cli:main"
|
|
62
55
|
|
|
56
|
+
[dependency-groups]
|
|
57
|
+
test = [
|
|
58
|
+
"pytest >= 7.4",
|
|
59
|
+
"coverage >= 7",
|
|
60
|
+
"psycopg[binary]",
|
|
61
|
+
"mysql-connector-python",
|
|
62
|
+
]
|
|
63
|
+
|
|
63
64
|
[tool.setuptools_scm]
|
|
64
65
|
version_scheme = "post-release"
|
|
65
66
|
local_scheme = "dirty-tag"
|
|
@@ -99,4 +100,5 @@ skip_missing_interpreters = true
|
|
|
99
100
|
[tool.tox.env_run_base]
|
|
100
101
|
package = "editable"
|
|
101
102
|
commands = [["python", "-m", "pytest", { replace = "posargs", extend = true }]]
|
|
102
|
-
|
|
103
|
+
dependency_groups = ["test"]
|
|
104
|
+
extras = ["sqlmodel", "citext", "geoalchemy2", "pgvector"]
|
|
@@ -123,6 +123,8 @@ class TablesGenerator(CodeGenerator):
|
|
|
123
123
|
"noindexes",
|
|
124
124
|
"noconstraints",
|
|
125
125
|
"nocomments",
|
|
126
|
+
"nonativeenums",
|
|
127
|
+
"nosyntheticenums",
|
|
126
128
|
"include_dialect_options",
|
|
127
129
|
"keep_dialect_types",
|
|
128
130
|
}
|
|
@@ -148,6 +150,11 @@ class TablesGenerator(CodeGenerator):
|
|
|
148
150
|
# Keep dialect-specific types instead of adapting to generic SQLAlchemy types
|
|
149
151
|
self.keep_dialect_types: bool = "keep_dialect_types" in self.options
|
|
150
152
|
|
|
153
|
+
# Track Python enum classes: maps (table_name, column_name) -> enum_class_name
|
|
154
|
+
self.enum_classes: dict[tuple[str, str], str] = {}
|
|
155
|
+
# Track enum values: maps enum_class_name -> list of values
|
|
156
|
+
self.enum_values: dict[str, list[str]] = {}
|
|
157
|
+
|
|
151
158
|
@property
|
|
152
159
|
def views_supported(self) -> bool:
|
|
153
160
|
return True
|
|
@@ -192,19 +199,22 @@ class TablesGenerator(CodeGenerator):
|
|
|
192
199
|
models: list[Model] = self.generate_models()
|
|
193
200
|
|
|
194
201
|
# Render module level variables
|
|
195
|
-
variables
|
|
196
|
-
if variables:
|
|
202
|
+
if variables := self.render_module_variables(models):
|
|
197
203
|
sections.append(variables + "\n")
|
|
198
204
|
|
|
205
|
+
# Render enum classes
|
|
206
|
+
if enum_classes := self.render_enum_classes():
|
|
207
|
+
sections.append(enum_classes + "\n")
|
|
208
|
+
|
|
199
209
|
# Render models
|
|
200
|
-
rendered_models
|
|
201
|
-
if rendered_models:
|
|
210
|
+
if rendered_models := self.render_models(models):
|
|
202
211
|
sections.append(rendered_models)
|
|
203
212
|
|
|
204
213
|
# Render collected imports
|
|
205
214
|
groups = self.group_imports()
|
|
206
|
-
imports
|
|
207
|
-
|
|
215
|
+
if imports := "\n\n".join(
|
|
216
|
+
"\n".join(line for line in group) for group in groups
|
|
217
|
+
):
|
|
208
218
|
sections.insert(0, imports)
|
|
209
219
|
|
|
210
220
|
return "\n\n".join(sections) + "\n"
|
|
@@ -467,7 +477,7 @@ class TablesGenerator(CodeGenerator):
|
|
|
467
477
|
# Render the column type if there are no foreign keys on it or any of them
|
|
468
478
|
# points back to itself
|
|
469
479
|
if not dedicated_fks or any(fk.column is column for fk in dedicated_fks):
|
|
470
|
-
args.append(self.render_column_type(column
|
|
480
|
+
args.append(self.render_column_type(column))
|
|
471
481
|
|
|
472
482
|
for fk in dedicated_fks:
|
|
473
483
|
args.append(self.render_constraint(fk))
|
|
@@ -528,10 +538,20 @@ class TablesGenerator(CodeGenerator):
|
|
|
528
538
|
else:
|
|
529
539
|
return render_callable("mapped_column", *args, kwargs=kwargs)
|
|
530
540
|
|
|
531
|
-
def render_column_type(self,
|
|
541
|
+
def render_column_type(self, column: Column[Any]) -> str:
|
|
542
|
+
column_type = column.type
|
|
543
|
+
# Check if this is an enum column with a Python enum class
|
|
544
|
+
if isinstance(column_type, Enum) and column is not None:
|
|
545
|
+
if enum_class_name := self.enum_classes.get(
|
|
546
|
+
(column.table.name, column.name)
|
|
547
|
+
):
|
|
548
|
+
# Import SQLAlchemy Enum (will be handled in collect_imports)
|
|
549
|
+
self.add_import(Enum)
|
|
550
|
+
return f"Enum({enum_class_name}, values_callable=lambda cls: [member.value for member in cls])"
|
|
551
|
+
|
|
532
552
|
args = []
|
|
533
553
|
kwargs: dict[str, Any] = {}
|
|
534
|
-
sig = inspect.signature(
|
|
554
|
+
sig = inspect.signature(column_type.__class__.__init__)
|
|
535
555
|
defaults = {param.name: param.default for param in sig.parameters.values()}
|
|
536
556
|
missing = object()
|
|
537
557
|
use_kwargs = False
|
|
@@ -543,7 +563,7 @@ class TablesGenerator(CodeGenerator):
|
|
|
543
563
|
use_kwargs = True
|
|
544
564
|
continue
|
|
545
565
|
|
|
546
|
-
value = getattr(
|
|
566
|
+
value = getattr(column_type, param.name, missing)
|
|
547
567
|
|
|
548
568
|
if isinstance(value, (JSONB, JSON)):
|
|
549
569
|
# Remove astext_type if it's the default
|
|
@@ -577,28 +597,28 @@ class TablesGenerator(CodeGenerator):
|
|
|
577
597
|
),
|
|
578
598
|
None,
|
|
579
599
|
)
|
|
580
|
-
if vararg and hasattr(
|
|
581
|
-
varargs_repr = [repr(arg) for arg in getattr(
|
|
600
|
+
if vararg and hasattr(column_type, vararg):
|
|
601
|
+
varargs_repr = [repr(arg) for arg in getattr(column_type, vararg)]
|
|
582
602
|
args.extend(varargs_repr)
|
|
583
603
|
|
|
584
604
|
# These arguments cannot be autodetected from the Enum initializer
|
|
585
|
-
if isinstance(
|
|
605
|
+
if isinstance(column_type, Enum):
|
|
586
606
|
for colname in "name", "schema":
|
|
587
|
-
if (value := getattr(
|
|
607
|
+
if (value := getattr(column_type, colname)) is not None:
|
|
588
608
|
kwargs[colname] = repr(value)
|
|
589
609
|
|
|
590
|
-
if isinstance(
|
|
610
|
+
if isinstance(column_type, (JSONB, JSON)):
|
|
591
611
|
# Remove astext_type if it's the default
|
|
592
612
|
if (
|
|
593
|
-
isinstance(
|
|
594
|
-
and
|
|
613
|
+
isinstance(column_type.astext_type, Text)
|
|
614
|
+
and column_type.astext_type.length is None
|
|
595
615
|
):
|
|
596
616
|
del kwargs["astext_type"]
|
|
597
617
|
|
|
598
618
|
if args or kwargs:
|
|
599
|
-
return render_callable(
|
|
619
|
+
return render_callable(column_type.__class__.__name__, *args, kwargs=kwargs)
|
|
600
620
|
else:
|
|
601
|
-
return
|
|
621
|
+
return column_type.__class__.__name__
|
|
602
622
|
|
|
603
623
|
def render_constraint(self, constraint: Constraint | ForeignKey) -> str:
|
|
604
624
|
def add_fk_options(*opts: Any) -> None:
|
|
@@ -709,6 +729,81 @@ class TablesGenerator(CodeGenerator):
|
|
|
709
729
|
|
|
710
730
|
return name
|
|
711
731
|
|
|
732
|
+
def _enum_name_to_class_name(self, enum_name: str) -> str:
|
|
733
|
+
"""Convert a database enum name to a Python class name (PascalCase)."""
|
|
734
|
+
return "".join(part.capitalize() for part in enum_name.split("_") if part)
|
|
735
|
+
|
|
736
|
+
def _create_enum_class(
|
|
737
|
+
self, table_name: str, column_name: str, values: list[str]
|
|
738
|
+
) -> str:
|
|
739
|
+
"""
|
|
740
|
+
Create a Python enum class name and register it.
|
|
741
|
+
|
|
742
|
+
Returns the enum class name to use in generated code.
|
|
743
|
+
"""
|
|
744
|
+
# Generate enum class name from table and column names
|
|
745
|
+
# Convert to PascalCase: user_status -> UserStatus
|
|
746
|
+
base_name = "".join(
|
|
747
|
+
part.capitalize()
|
|
748
|
+
for part in table_name.split("_") + column_name.split("_")
|
|
749
|
+
if part
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
# Ensure uniqueness
|
|
753
|
+
enum_class_name = base_name
|
|
754
|
+
for counter in count(1):
|
|
755
|
+
if enum_class_name not in self.enum_values:
|
|
756
|
+
break
|
|
757
|
+
|
|
758
|
+
# Check if it's the same enum (same values)
|
|
759
|
+
if self.enum_values[enum_class_name] == values:
|
|
760
|
+
# Reuse existing enum class
|
|
761
|
+
return enum_class_name
|
|
762
|
+
|
|
763
|
+
enum_class_name = f"{base_name}{counter}"
|
|
764
|
+
|
|
765
|
+
# Register the new enum class
|
|
766
|
+
self.enum_values[enum_class_name] = values
|
|
767
|
+
return enum_class_name
|
|
768
|
+
|
|
769
|
+
def render_enum_classes(self) -> str:
|
|
770
|
+
"""Render Python enum class definitions."""
|
|
771
|
+
if not self.enum_values:
|
|
772
|
+
return ""
|
|
773
|
+
|
|
774
|
+
self.add_module_import("enum")
|
|
775
|
+
|
|
776
|
+
enum_defs = []
|
|
777
|
+
for enum_class_name, values in sorted(self.enum_values.items()):
|
|
778
|
+
# Create enum members with valid Python identifiers
|
|
779
|
+
members = []
|
|
780
|
+
for value in values:
|
|
781
|
+
# Unescape SQL escape sequences (e.g., \' -> ')
|
|
782
|
+
# The value from the CHECK constraint has SQL escaping
|
|
783
|
+
unescaped_value = value.replace("\\'", "'").replace("\\\\", "\\")
|
|
784
|
+
|
|
785
|
+
# Create a valid identifier from the enum value
|
|
786
|
+
member_name = _re_invalid_identifier.sub("_", unescaped_value).upper()
|
|
787
|
+
if not member_name:
|
|
788
|
+
member_name = "EMPTY"
|
|
789
|
+
elif member_name[0].isdigit():
|
|
790
|
+
member_name = "_" + member_name
|
|
791
|
+
elif iskeyword(member_name):
|
|
792
|
+
member_name += "_"
|
|
793
|
+
#
|
|
794
|
+
# # Re-escape for Python string literal
|
|
795
|
+
# python_escaped = unescaped_value.replace("\\", "\\\\").replace(
|
|
796
|
+
# "'", "\\'"
|
|
797
|
+
# )
|
|
798
|
+
members.append(f" {member_name} = {unescaped_value!r}")
|
|
799
|
+
|
|
800
|
+
enum_def = f"class {enum_class_name}(str, enum.Enum):\n" + "\n".join(
|
|
801
|
+
members
|
|
802
|
+
)
|
|
803
|
+
enum_defs.append(enum_def)
|
|
804
|
+
|
|
805
|
+
return "\n\n\n".join(enum_defs)
|
|
806
|
+
|
|
712
807
|
def fix_column_types(self, table: Table) -> None:
|
|
713
808
|
"""Adjust the reflected column types."""
|
|
714
809
|
# Detect check constraints for boolean and enum columns
|
|
@@ -718,34 +813,74 @@ class TablesGenerator(CodeGenerator):
|
|
|
718
813
|
|
|
719
814
|
# Turn any integer-like column with a CheckConstraint like
|
|
720
815
|
# "column IN (0, 1)" into a Boolean
|
|
721
|
-
match
|
|
722
|
-
|
|
723
|
-
colname_match = _re_column_name.match(match.group(1))
|
|
724
|
-
if colname_match:
|
|
816
|
+
if match := _re_boolean_check_constraint.match(sqltext):
|
|
817
|
+
if colname_match := _re_column_name.match(match.group(1)):
|
|
725
818
|
colname = colname_match.group(3)
|
|
726
819
|
table.constraints.remove(constraint)
|
|
727
820
|
table.c[colname].type = Boolean()
|
|
728
821
|
continue
|
|
729
822
|
|
|
730
|
-
# Turn
|
|
731
|
-
#
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
823
|
+
# Turn VARCHAR columns with CHECK constraints like "column IN ('a', 'b')"
|
|
824
|
+
# into synthetic Enum types with Python enum classes
|
|
825
|
+
if (
|
|
826
|
+
"nosyntheticenums" not in self.options
|
|
827
|
+
and (match := _re_enum_check_constraint.match(sqltext))
|
|
828
|
+
and (colname_match := _re_column_name.match(match.group(1)))
|
|
829
|
+
):
|
|
830
|
+
colname = colname_match.group(3)
|
|
831
|
+
items = match.group(2)
|
|
832
|
+
if isinstance(table.c[colname].type, String) and not isinstance(
|
|
833
|
+
table.c[colname].type, Enum
|
|
834
|
+
):
|
|
835
|
+
options = _re_enum_item.findall(items)
|
|
836
|
+
# Create Python enum class
|
|
837
|
+
enum_class_name = self._create_enum_class(
|
|
838
|
+
table.name, colname, options
|
|
839
|
+
)
|
|
840
|
+
self.enum_classes[(table.name, colname)] = enum_class_name
|
|
841
|
+
# Convert to Enum type but KEEP the constraint
|
|
842
|
+
table.c[colname].type = Enum(*options, native_enum=False)
|
|
843
|
+
continue
|
|
844
|
+
|
|
845
|
+
for column in table.c:
|
|
846
|
+
# Handle native database Enum types (e.g., PostgreSQL ENUM)
|
|
847
|
+
if (
|
|
848
|
+
"nonativeenums" not in self.options
|
|
849
|
+
and isinstance(column.type, Enum)
|
|
850
|
+
and column.type.enums
|
|
851
|
+
):
|
|
852
|
+
if column.type.name:
|
|
853
|
+
# Named enum - create shared enum class if not already created
|
|
854
|
+
if (table.name, column.name) not in self.enum_classes:
|
|
855
|
+
# Check if we've already created an enum for this name
|
|
856
|
+
existing_class = None
|
|
857
|
+
for (t, c), cls in self.enum_classes.items():
|
|
858
|
+
if cls == self._enum_name_to_class_name(column.type.name):
|
|
859
|
+
existing_class = cls
|
|
860
|
+
break
|
|
861
|
+
|
|
862
|
+
if existing_class:
|
|
863
|
+
enum_class_name = existing_class
|
|
864
|
+
else:
|
|
865
|
+
# Create new enum class from the enum's name
|
|
866
|
+
enum_class_name = self._enum_name_to_class_name(
|
|
867
|
+
column.type.name
|
|
868
|
+
)
|
|
869
|
+
# Register the enum values if not already registered
|
|
870
|
+
if enum_class_name not in self.enum_values:
|
|
871
|
+
self.enum_values[enum_class_name] = list(
|
|
872
|
+
column.type.enums
|
|
744
873
|
)
|
|
745
874
|
|
|
746
|
-
|
|
875
|
+
self.enum_classes[(table.name, column.name)] = enum_class_name
|
|
876
|
+
else:
|
|
877
|
+
# Unnamed enum - create enum class per column
|
|
878
|
+
if (table.name, column.name) not in self.enum_classes:
|
|
879
|
+
enum_class_name = self._create_enum_class(
|
|
880
|
+
table.name, column.name, list(column.type.enums)
|
|
881
|
+
)
|
|
882
|
+
self.enum_classes[(table.name, column.name)] = enum_class_name
|
|
747
883
|
|
|
748
|
-
for column in table.c:
|
|
749
884
|
if not self.keep_dialect_types:
|
|
750
885
|
try:
|
|
751
886
|
column.type = self.get_adapted_type(column.type)
|
|
@@ -1326,6 +1461,14 @@ class DeclarativeGenerator(TablesGenerator):
|
|
|
1326
1461
|
return "".join(pre), column_type, "]" * post_size
|
|
1327
1462
|
|
|
1328
1463
|
def render_python_type(column_type: TypeEngine[Any]) -> str:
|
|
1464
|
+
# Check if this is an enum column with a Python enum class
|
|
1465
|
+
if isinstance(column_type, Enum):
|
|
1466
|
+
table_name = column.table.name
|
|
1467
|
+
column_name = column.name
|
|
1468
|
+
if (table_name, column_name) in self.enum_classes:
|
|
1469
|
+
enum_class_name = self.enum_classes[(table_name, column_name)]
|
|
1470
|
+
return enum_class_name
|
|
1471
|
+
|
|
1329
1472
|
if isinstance(column_type, DOMAIN):
|
|
1330
1473
|
column_type = column_type.data_type
|
|
1331
1474
|
|
|
@@ -210,10 +210,4 @@ def decode_postgresql_sequence(clause: TextClause) -> tuple[str | None, str | No
|
|
|
210
210
|
|
|
211
211
|
|
|
212
212
|
def get_stdlib_module_names() -> set[str]:
|
|
213
|
-
|
|
214
|
-
if (major, minor) > (3, 9):
|
|
215
|
-
return set(sys.builtin_module_names) | set(sys.stdlib_module_names)
|
|
216
|
-
else:
|
|
217
|
-
from stdlib_list import stdlib_list
|
|
218
|
-
|
|
219
|
-
return set(sys.builtin_module_names) | set(stdlib_list(f"{major}.{minor}"))
|
|
213
|
+
return set(sys.builtin_module_names) | set(sys.stdlib_module_names)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlacodegen
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.0rc2
|
|
4
4
|
Summary: Automatic model code generator for SQLAlchemy
|
|
5
5
|
Author-email: Alex Grönholm <alex.gronholm@nextday.fi>
|
|
6
6
|
Maintainer-email: Idan Sheinberg <ishinberg0@gmail.com>
|
|
@@ -25,12 +25,6 @@ Description-Content-Type: text/x-rst
|
|
|
25
25
|
License-File: LICENSE
|
|
26
26
|
Requires-Dist: SQLAlchemy>=2.0.29
|
|
27
27
|
Requires-Dist: inflect>=4.0.0
|
|
28
|
-
Provides-Extra: test
|
|
29
|
-
Requires-Dist: sqlacodegen[geoalchemy2,pgvector,sqlmodel]; extra == "test"
|
|
30
|
-
Requires-Dist: pytest>=7.4; extra == "test"
|
|
31
|
-
Requires-Dist: coverage>=7; extra == "test"
|
|
32
|
-
Requires-Dist: psycopg[binary]; extra == "test"
|
|
33
|
-
Requires-Dist: mysql-connector-python; extra == "test"
|
|
34
28
|
Provides-Extra: sqlmodel
|
|
35
29
|
Requires-Dist: sqlmodel>=0.0.22; extra == "sqlmodel"
|
|
36
30
|
Provides-Extra: citext
|
|
@@ -47,6 +41,9 @@ Dynamic: license-file
|
|
|
47
41
|
.. image:: https://coveralls.io/repos/github/agronholm/sqlacodegen/badge.svg?branch=master
|
|
48
42
|
:target: https://coveralls.io/github/agronholm/sqlacodegen?branch=master
|
|
49
43
|
:alt: Code Coverage
|
|
44
|
+
.. image:: https://tidelift.com/badges/package/pypi/sqlacodegen
|
|
45
|
+
:target: https://tidelift.com/subscription/pkg/pypi-sqlacodegen
|
|
46
|
+
:alt: Tidelift
|
|
50
47
|
|
|
51
48
|
This is a tool that reads the structure of an existing database and generates the
|
|
52
49
|
appropriate SQLAlchemy model code, using the declarative style if possible.
|
|
@@ -146,6 +143,8 @@ values must be delimited by commas, e.g. ``--options noconstraints,nobidi``):
|
|
|
146
143
|
* ``noconstraints``: ignore constraints (foreign key, unique etc.)
|
|
147
144
|
* ``nocomments``: ignore table/column comments
|
|
148
145
|
* ``noindexes``: ignore indexes
|
|
146
|
+
* ``nonativeenums``: don't generate Python enum classes for native database ENUM types (e.g., PostgreSQL ENUM); use plain string mapping instead
|
|
147
|
+
* ``nosyntheticenums``: don't generate Python enum classes from CHECK constraints with IN clauses (e.g., ``column IN ('value1', 'value2', ...)``); preserves CHECK constraints as-is
|
|
149
148
|
* ``noidsuffix``: prevent the special naming logic for single column many-to-one
|
|
150
149
|
and one-to-one relationships (see `Relationship naming logic`_ for details)
|
|
151
150
|
* ``include_dialect_options``: render a table' dialect options, such as ``starrocks_partition`` for StarRocks' specific options.
|
|
@@ -251,3 +250,11 @@ sqlalchemy_ room on Gitter.
|
|
|
251
250
|
|
|
252
251
|
.. _sqlacodegen discussion forum: https://github.com/agronholm/sqlacodegen/discussions/categories/q-a
|
|
253
252
|
.. _sqlalchemy: https://app.gitter.im/#/room/#sqlalchemy_community:gitter.im
|
|
253
|
+
|
|
254
|
+
Security contact information
|
|
255
|
+
============================
|
|
256
|
+
|
|
257
|
+
To report a security vulnerability, please use the `Tidelift security contact`_.
|
|
258
|
+
Tidelift will coordinate the fix and disclosure.
|
|
259
|
+
|
|
260
|
+
.. _Tidelift security contact: https://tidelift.com/security
|