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.
Files changed (37) hide show
  1. sqlacodegen-4.0.0rc2/.github/FUNDING.yml +1 -0
  2. sqlacodegen-4.0.0rc2/.github/dependabot.yml +13 -0
  3. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/.github/workflows/publish.yml +8 -5
  4. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/.github/workflows/test.yml +3 -3
  5. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/.pre-commit-config.yaml +2 -2
  6. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/CHANGES.rst +16 -0
  7. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/PKG-INFO +14 -7
  8. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/README.rst +13 -0
  9. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/pyproject.toml +10 -8
  10. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen/generators.py +182 -39
  11. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen/utils.py +1 -7
  12. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen.egg-info/PKG-INFO +14 -7
  13. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen.egg-info/SOURCES.txt +2 -0
  14. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen.egg-info/requires.txt +0 -7
  15. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/tests/test_generator_declarative.py +390 -0
  16. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/tests/test_generator_sqlmodel.py +65 -0
  17. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/tests/test_generator_tables.py +98 -4
  18. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/.github/ISSUE_TEMPLATE/bug_report.yaml +0 -0
  19. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  20. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/.github/ISSUE_TEMPLATE/features_request.yaml +0 -0
  21. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/.github/pull_request_template.md +0 -0
  22. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/.gitignore +0 -0
  23. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/CONTRIBUTING.rst +0 -0
  24. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/LICENSE +0 -0
  25. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/setup.cfg +0 -0
  26. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen/__init__.py +0 -0
  27. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen/__main__.py +0 -0
  28. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen/cli.py +0 -0
  29. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen/models.py +0 -0
  30. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen/py.typed +0 -0
  31. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen.egg-info/dependency_links.txt +0 -0
  32. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen.egg-info/entry_points.txt +0 -0
  33. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/src/sqlacodegen.egg-info/top_level.txt +0 -0
  34. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/tests/__init__.py +0 -0
  35. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/tests/conftest.py +0 -0
  36. {sqlacodegen-3.2.0 → sqlacodegen-4.0.0rc2}/tests/test_cli.py +0 -0
  37. {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@v4
17
+ - uses: actions/checkout@v6
18
18
  - name: Set up Python
19
- uses: actions/setup-python@v5
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@v4
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@v4
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@v4
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@v4
16
+ - uses: actions/checkout@v6
17
17
  - name: Set up Python ${{ matrix.python-version }}
18
- uses: actions/setup-python@v5
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 .[test]
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.13.3
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.18.2
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.2.0
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
- extras = ["test"]
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 = self.render_module_variables(models)
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 = self.render_models(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 = "\n\n".join("\n".join(line for line in group) for group in groups)
207
- if imports:
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.type))
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, coltype: TypeEngine[Any]) -> str:
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(coltype.__class__.__init__)
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(coltype, param.name, missing)
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(coltype, vararg):
581
- varargs_repr = [repr(arg) for arg in getattr(coltype, vararg)]
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(coltype, Enum):
605
+ if isinstance(column_type, Enum):
586
606
  for colname in "name", "schema":
587
- if (value := getattr(coltype, colname)) is not None:
607
+ if (value := getattr(column_type, colname)) is not None:
588
608
  kwargs[colname] = repr(value)
589
609
 
590
- if isinstance(coltype, (JSONB, JSON)):
610
+ if isinstance(column_type, (JSONB, JSON)):
591
611
  # Remove astext_type if it's the default
592
612
  if (
593
- isinstance(coltype.astext_type, Text)
594
- and coltype.astext_type.length is None
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(coltype.__class__.__name__, *args, kwargs=kwargs)
619
+ return render_callable(column_type.__class__.__name__, *args, kwargs=kwargs)
600
620
  else:
601
- return coltype.__class__.__name__
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 = _re_boolean_check_constraint.match(sqltext)
722
- if match:
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 any string-type column with a CheckConstraint like
731
- # "column IN (...)" into an Enum
732
- match = _re_enum_check_constraint.match(sqltext)
733
- if match:
734
- colname_match = _re_column_name.match(match.group(1))
735
- if colname_match:
736
- colname = colname_match.group(3)
737
- items = match.group(2)
738
- if isinstance(table.c[colname].type, String):
739
- table.constraints.remove(constraint)
740
- if not isinstance(table.c[colname].type, Enum):
741
- options = _re_enum_item.findall(items)
742
- table.c[colname].type = Enum(
743
- *options, native_enum=False
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
- continue
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
- major, minor = sys.version_info.major, sys.version_info.minor
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.2.0
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
@@ -5,6 +5,8 @@ CONTRIBUTING.rst
5
5
  LICENSE
6
6
  README.rst
7
7
  pyproject.toml
8
+ .github/FUNDING.yml
9
+ .github/dependabot.yml
8
10
  .github/pull_request_template.md
9
11
  .github/ISSUE_TEMPLATE/bug_report.yaml
10
12
  .github/ISSUE_TEMPLATE/config.yml
@@ -12,10 +12,3 @@ pgvector>=0.2.4
12
12
 
13
13
  [sqlmodel]
14
14
  sqlmodel>=0.0.22
15
-
16
- [test]
17
- sqlacodegen[geoalchemy2,pgvector,sqlmodel]
18
- pytest>=7.4
19
- coverage>=7
20
- psycopg[binary]
21
- mysql-connector-python