plain.models 0.49.2__py3-none-any.whl → 0.50.0__py3-none-any.whl

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 (105) hide show
  1. plain/models/CHANGELOG.md +13 -0
  2. plain/models/aggregates.py +42 -19
  3. plain/models/backends/base/base.py +125 -105
  4. plain/models/backends/base/client.py +11 -3
  5. plain/models/backends/base/creation.py +22 -12
  6. plain/models/backends/base/features.py +10 -4
  7. plain/models/backends/base/introspection.py +29 -16
  8. plain/models/backends/base/operations.py +187 -91
  9. plain/models/backends/base/schema.py +267 -165
  10. plain/models/backends/base/validation.py +12 -3
  11. plain/models/backends/ddl_references.py +85 -43
  12. plain/models/backends/mysql/base.py +29 -26
  13. plain/models/backends/mysql/client.py +7 -2
  14. plain/models/backends/mysql/compiler.py +12 -3
  15. plain/models/backends/mysql/creation.py +5 -2
  16. plain/models/backends/mysql/features.py +24 -22
  17. plain/models/backends/mysql/introspection.py +22 -13
  18. plain/models/backends/mysql/operations.py +106 -39
  19. plain/models/backends/mysql/schema.py +48 -24
  20. plain/models/backends/mysql/validation.py +13 -6
  21. plain/models/backends/postgresql/base.py +41 -34
  22. plain/models/backends/postgresql/client.py +7 -2
  23. plain/models/backends/postgresql/creation.py +10 -5
  24. plain/models/backends/postgresql/introspection.py +15 -8
  25. plain/models/backends/postgresql/operations.py +109 -42
  26. plain/models/backends/postgresql/schema.py +85 -46
  27. plain/models/backends/sqlite3/_functions.py +151 -115
  28. plain/models/backends/sqlite3/base.py +37 -23
  29. plain/models/backends/sqlite3/client.py +7 -1
  30. plain/models/backends/sqlite3/creation.py +9 -5
  31. plain/models/backends/sqlite3/features.py +5 -3
  32. plain/models/backends/sqlite3/introspection.py +32 -16
  33. plain/models/backends/sqlite3/operations.py +125 -42
  34. plain/models/backends/sqlite3/schema.py +82 -58
  35. plain/models/backends/utils.py +52 -29
  36. plain/models/backups/cli.py +8 -6
  37. plain/models/backups/clients.py +16 -7
  38. plain/models/backups/core.py +24 -13
  39. plain/models/base.py +113 -74
  40. plain/models/cli.py +94 -63
  41. plain/models/config.py +1 -1
  42. plain/models/connections.py +23 -7
  43. plain/models/constraints.py +65 -47
  44. plain/models/database_url.py +1 -1
  45. plain/models/db.py +6 -2
  46. plain/models/deletion.py +66 -43
  47. plain/models/entrypoints.py +1 -1
  48. plain/models/enums.py +22 -11
  49. plain/models/exceptions.py +23 -8
  50. plain/models/expressions.py +440 -257
  51. plain/models/fields/__init__.py +253 -202
  52. plain/models/fields/json.py +120 -54
  53. plain/models/fields/mixins.py +12 -8
  54. plain/models/fields/related.py +284 -252
  55. plain/models/fields/related_descriptors.py +31 -22
  56. plain/models/fields/related_lookups.py +23 -11
  57. plain/models/fields/related_managers.py +81 -47
  58. plain/models/fields/reverse_related.py +58 -55
  59. plain/models/forms.py +89 -63
  60. plain/models/functions/comparison.py +71 -18
  61. plain/models/functions/datetime.py +79 -29
  62. plain/models/functions/math.py +43 -10
  63. plain/models/functions/mixins.py +24 -7
  64. plain/models/functions/text.py +104 -25
  65. plain/models/functions/window.py +12 -6
  66. plain/models/indexes.py +52 -28
  67. plain/models/lookups.py +228 -153
  68. plain/models/migrations/autodetector.py +86 -43
  69. plain/models/migrations/exceptions.py +7 -3
  70. plain/models/migrations/executor.py +33 -7
  71. plain/models/migrations/graph.py +79 -50
  72. plain/models/migrations/loader.py +45 -22
  73. plain/models/migrations/migration.py +23 -18
  74. plain/models/migrations/operations/base.py +37 -19
  75. plain/models/migrations/operations/fields.py +89 -42
  76. plain/models/migrations/operations/models.py +245 -143
  77. plain/models/migrations/operations/special.py +82 -25
  78. plain/models/migrations/optimizer.py +7 -2
  79. plain/models/migrations/questioner.py +58 -31
  80. plain/models/migrations/recorder.py +18 -11
  81. plain/models/migrations/serializer.py +50 -39
  82. plain/models/migrations/state.py +220 -133
  83. plain/models/migrations/utils.py +29 -13
  84. plain/models/migrations/writer.py +17 -14
  85. plain/models/options.py +63 -56
  86. plain/models/otel.py +16 -6
  87. plain/models/preflight.py +35 -12
  88. plain/models/query.py +323 -228
  89. plain/models/query_utils.py +93 -58
  90. plain/models/registry.py +34 -16
  91. plain/models/sql/compiler.py +146 -97
  92. plain/models/sql/datastructures.py +38 -25
  93. plain/models/sql/query.py +255 -169
  94. plain/models/sql/subqueries.py +32 -21
  95. plain/models/sql/where.py +54 -29
  96. plain/models/test/pytest.py +15 -11
  97. plain/models/test/utils.py +4 -2
  98. plain/models/transaction.py +20 -7
  99. plain/models/utils.py +13 -5
  100. {plain_models-0.49.2.dist-info → plain_models-0.50.0.dist-info}/METADATA +1 -1
  101. plain_models-0.50.0.dist-info/RECORD +122 -0
  102. plain_models-0.49.2.dist-info/RECORD +0 -122
  103. {plain_models-0.49.2.dist-info → plain_models-0.50.0.dist-info}/WHEEL +0 -0
  104. {plain_models-0.49.2.dist-info → plain_models-0.50.0.dist-info}/entry_points.txt +0 -0
  105. {plain_models-0.49.2.dist-info → plain_models-0.50.0.dist-info}/licenses/LICENSE +0 -0
plain/models/cli.py CHANGED
@@ -1,7 +1,10 @@
1
+ from __future__ import annotations
2
+
1
3
  import os
2
4
  import subprocess
3
5
  import sys
4
6
  import time
7
+ from typing import TYPE_CHECKING, Any, cast
5
8
 
6
9
  import click
7
10
 
@@ -13,7 +16,8 @@ from plain.utils.text import Truncator
13
16
  from . import migrations
14
17
  from .backups.cli import cli as backups_cli
15
18
  from .backups.cli import create_backup
16
- from .db import OperationalError, db_connection
19
+ from .db import OperationalError
20
+ from .db import db_connection as _db_connection
17
21
  from .migrations.autodetector import MigrationAutodetector
18
22
  from .migrations.executor import MigrationExecutor
19
23
  from .migrations.loader import AmbiguityError, MigrationLoader
@@ -28,10 +32,17 @@ from .migrations.state import ModelState, ProjectState
28
32
  from .migrations.writer import MigrationWriter
29
33
  from .registry import models_registry
30
34
 
35
+ if TYPE_CHECKING:
36
+ from .backends.base.base import BaseDatabaseWrapper
37
+
38
+ db_connection = cast("BaseDatabaseWrapper", _db_connection)
39
+ else:
40
+ db_connection = _db_connection
41
+
31
42
 
32
43
  @register_cli("models")
33
44
  @click.group()
34
- def cli():
45
+ def cli() -> None:
35
46
  pass
36
47
 
37
48
 
@@ -40,10 +51,10 @@ cli.add_command(backups_cli)
40
51
 
41
52
  @cli.command()
42
53
  @click.argument("parameters", nargs=-1)
43
- def db_shell(parameters):
54
+ def db_shell(parameters: tuple[str, ...]) -> None:
44
55
  """Runs the command-line client for specified database, or the default database if none is provided."""
45
56
  try:
46
- db_connection.client.runshell(parameters)
57
+ db_connection.client.runshell(list(parameters))
47
58
  except FileNotFoundError:
48
59
  # Note that we're assuming the FileNotFoundError relates to the
49
60
  # command missing. It could be raised for some other reason, in
@@ -68,7 +79,7 @@ def db_shell(parameters):
68
79
 
69
80
 
70
81
  @cli.command()
71
- def db_wait():
82
+ def db_wait() -> None:
72
83
  """Wait for the database to be ready"""
73
84
  attempts = 0
74
85
  while True:
@@ -100,7 +111,7 @@ def db_wait():
100
111
  is_flag=True,
101
112
  help="Only show models from packages that start with 'app'.",
102
113
  )
103
- def list_models(package_labels, app_only):
114
+ def list_models(package_labels: tuple[str, ...], app_only: bool) -> None:
104
115
  """List installed models."""
105
116
 
106
117
  packages = set(package_labels)
@@ -153,19 +164,30 @@ def list_models(package_labels, app_only):
153
164
  default=1,
154
165
  help="Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output",
155
166
  )
156
- def makemigrations(package_labels, dry_run, empty, no_input, name, check, verbosity):
167
+ def makemigrations(
168
+ package_labels: tuple[str, ...],
169
+ dry_run: bool,
170
+ empty: bool,
171
+ no_input: bool,
172
+ name: str | None,
173
+ check: bool,
174
+ verbosity: int,
175
+ ) -> None:
157
176
  """Creates new migration(s) for packages."""
158
177
 
159
- written_files = []
178
+ written_files: list[str] = []
160
179
  interactive = not no_input
161
180
  migration_name = name
162
181
  check_changes = check
163
182
 
164
- def log(msg, level=1):
183
+ def log(msg: str, level: int = 1) -> None:
165
184
  if verbosity >= level:
166
185
  click.echo(msg)
167
186
 
168
- def write_migration_files(changes, update_previous_migration_paths=None):
187
+ def write_migration_files(
188
+ changes: dict[str, list[Migration]],
189
+ update_previous_migration_paths: dict[str, str] | None = None,
190
+ ) -> None:
169
191
  """Take a changes dict and write them out as migration files."""
170
192
  directory_created = {}
171
193
  for package_label, package_migrations in changes.items():
@@ -221,9 +243,9 @@ def makemigrations(package_labels, dry_run, empty, no_input, name, check, verbos
221
243
  log(writer.as_string(), level=3)
222
244
 
223
245
  # Validate package labels
224
- package_labels = set(package_labels)
246
+ package_labels_set = set(package_labels)
225
247
  has_bad_labels = False
226
- for package_label in package_labels:
248
+ for package_label in package_labels_set:
227
249
  try:
228
250
  packages_registry.get_package_config(package_label)
229
251
  except LookupError as err:
@@ -241,11 +263,11 @@ def makemigrations(package_labels, dry_run, empty, no_input, name, check, verbos
241
263
 
242
264
  # Check for conflicts
243
265
  conflicts = loader.detect_conflicts()
244
- if package_labels:
266
+ if package_labels_set:
245
267
  conflicts = {
246
268
  package_label: conflict
247
269
  for package_label, conflict in conflicts.items()
248
- if package_label in package_labels
270
+ if package_label in package_labels_set
249
271
  }
250
272
 
251
273
  if conflicts:
@@ -261,12 +283,12 @@ def makemigrations(package_labels, dry_run, empty, no_input, name, check, verbos
261
283
  # Set up questioner
262
284
  if interactive:
263
285
  questioner = InteractiveMigrationQuestioner(
264
- specified_packages=package_labels,
286
+ specified_packages=package_labels_set,
265
287
  dry_run=dry_run,
266
288
  )
267
289
  else:
268
290
  questioner = NonInteractiveMigrationQuestioner(
269
- specified_packages=package_labels,
291
+ specified_packages=package_labels_set,
270
292
  dry_run=dry_run,
271
293
  verbosity=verbosity,
272
294
  )
@@ -280,12 +302,12 @@ def makemigrations(package_labels, dry_run, empty, no_input, name, check, verbos
280
302
 
281
303
  # Handle empty migrations if requested
282
304
  if empty:
283
- if not package_labels:
305
+ if not package_labels_set:
284
306
  raise click.ClickException(
285
307
  "You must supply at least one package label when using --empty."
286
308
  )
287
309
  changes = {
288
- package: [Migration("custom", package)] for package in package_labels
310
+ package: [Migration("custom", package)] for package in package_labels_set
289
311
  }
290
312
  changes = autodetector.arrange_for_graph(
291
313
  changes=changes,
@@ -298,17 +320,17 @@ def makemigrations(package_labels, dry_run, empty, no_input, name, check, verbos
298
320
  # Detect changes
299
321
  changes = autodetector.changes(
300
322
  graph=loader.graph,
301
- trim_to_packages=package_labels or None,
302
- convert_packages=package_labels or None,
323
+ trim_to_packages=package_labels_set or None,
324
+ convert_packages=package_labels_set or None,
303
325
  migration_name=migration_name,
304
326
  )
305
327
 
306
328
  if not changes:
307
329
  log(
308
330
  "No changes detected"
309
- if not package_labels
310
- else f"No changes detected in {'package' if len(package_labels) == 1 else 'packages'} "
311
- f"'{', '.join(package_labels)}'",
331
+ if not package_labels_set
332
+ else f"No changes detected in {'package' if len(package_labels_set) == 1 else 'packages'} "
333
+ f"'{', '.join(package_labels_set)}'",
312
334
  level=1,
313
335
  )
314
336
  else:
@@ -368,20 +390,22 @@ def makemigrations(package_labels, dry_run, empty, no_input, name, check, verbos
368
390
  help="Run migrations in a single transaction (auto-detected by default)",
369
391
  )
370
392
  def migrate(
371
- package_label,
372
- migration_name,
373
- fake,
374
- plan,
375
- check_unapplied,
376
- backup,
377
- prune,
378
- no_input,
379
- verbosity,
380
- atomic_batch,
381
- ):
393
+ package_label: str | None,
394
+ migration_name: str | None,
395
+ fake: bool,
396
+ plan: bool,
397
+ check_unapplied: bool,
398
+ backup: bool | None,
399
+ prune: bool,
400
+ no_input: bool,
401
+ verbosity: int,
402
+ atomic_batch: bool | None,
403
+ ) -> None:
382
404
  """Updates database schema. Manages both packages with migrations and those without."""
383
405
 
384
- def migration_progress_callback(action, migration=None, fake=False):
406
+ def migration_progress_callback(
407
+ action: str, migration: Migration | None = None, fake: bool = False
408
+ ) -> None:
385
409
  if verbosity >= 1:
386
410
  if action == "apply_start":
387
411
  click.echo(f" Applying {migration}...", nl=False)
@@ -395,7 +419,7 @@ def migrate(
395
419
  elif action == "render_success":
396
420
  click.echo(click.style(" DONE", fg="green"))
397
421
 
398
- def describe_operation(operation):
422
+ def describe_operation(operation: Any) -> tuple[str, bool]:
399
423
  """Return a string that describes a migration operation for --plan."""
400
424
  prefix = ""
401
425
  is_error = False
@@ -438,6 +462,7 @@ def migrate(
438
462
 
439
463
  # If they supplied command line arguments, work out what they mean.
440
464
  target_package_labels_only = True
465
+ targets: list[tuple[str, str]]
441
466
  if package_label:
442
467
  try:
443
468
  packages_registry.get_package_config(package_label)
@@ -463,13 +488,13 @@ def migrate(
463
488
  raise click.ClickException(
464
489
  f"Cannot find a migration matching '{migration_name}' from package '{package_label}'."
465
490
  )
466
- target = (package_label, migration.name)
491
+ target: tuple[str, str] = (package_label, migration.name)
467
492
  if (
468
493
  target not in executor.loader.graph.nodes
469
494
  and target in executor.loader.replacements
470
495
  ):
471
496
  incomplete_migration = executor.loader.replacements[target]
472
- target = incomplete_migration.replaces[-1]
497
+ target = incomplete_migration.replaces[-1] # type: ignore[assignment]
473
498
  targets = [target]
474
499
  target_package_labels_only = False
475
500
  elif package_label:
@@ -477,7 +502,7 @@ def migrate(
477
502
  key for key in executor.loader.graph.leaf_nodes() if key[0] == package_label
478
503
  ]
479
504
  else:
480
- targets = executor.loader.graph.leaf_nodes()
505
+ targets = list(executor.loader.graph.leaf_nodes())
481
506
 
482
507
  if prune:
483
508
  if not package_label:
@@ -486,8 +511,8 @@ def migrate(
486
511
  )
487
512
  if verbosity > 0:
488
513
  click.secho("Pruning migrations:", fg="cyan")
489
- to_prune = set(executor.loader.applied_migrations) - set(
490
- executor.loader.disk_migrations
514
+ to_prune = set(executor.loader.applied_migrations) - set( # type: ignore[arg-type]
515
+ executor.loader.disk_migrations # type: ignore[arg-type]
491
516
  )
492
517
  squashed_migrations_with_deleted_replaced_migrations = [
493
518
  migration_key
@@ -729,10 +754,12 @@ def migrate(
729
754
  default=1,
730
755
  help="Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output",
731
756
  )
732
- def show_migrations(package_labels, format, verbosity):
757
+ def show_migrations(
758
+ package_labels: tuple[str, ...], format: str, verbosity: int
759
+ ) -> None:
733
760
  """Shows all available migrations for the current project"""
734
761
 
735
- def _validate_package_names(package_names):
762
+ def _validate_package_names(package_names: tuple[str, ...]) -> None:
736
763
  has_bad_names = False
737
764
  for package_name in package_names:
738
765
  try:
@@ -743,7 +770,7 @@ def show_migrations(package_labels, format, verbosity):
743
770
  if has_bad_names:
744
771
  sys.exit(2)
745
772
 
746
- def show_list(db_connection, package_names):
773
+ def show_list(db_connection: Any, package_names: tuple[str, ...]) -> None:
747
774
  """
748
775
  Show a list of all migrations on the system, or only those of
749
776
  some named packages.
@@ -755,14 +782,16 @@ def show_migrations(package_labels, format, verbosity):
755
782
 
756
783
  graph = loader.graph
757
784
  # If we were passed a list of packages, validate it
785
+ package_names_list: list[str]
758
786
  if package_names:
759
787
  _validate_package_names(package_names)
788
+ package_names_list = list(package_names)
760
789
  # Otherwise, show all packages in alphabetic order
761
790
  else:
762
- package_names = sorted(loader.migrated_packages)
791
+ package_names_list = sorted(loader.migrated_packages)
763
792
  # For each app, print its migrations in order from oldest (roots) to
764
793
  # newest (leaves).
765
- for package_name in package_names:
794
+ for package_name in package_names_list:
766
795
  click.secho(package_name, fg="cyan", bold=True)
767
796
  shown = set()
768
797
  for node in graph.leaf_nodes(package_name):
@@ -770,9 +799,9 @@ def show_migrations(package_labels, format, verbosity):
770
799
  if plan_node not in shown and plan_node[0] == package_name:
771
800
  # Give it a nice title if it's a squashed one
772
801
  title = plan_node[1]
773
- if graph.nodes[plan_node].replaces:
774
- title += f" ({len(graph.nodes[plan_node].replaces)} squashed migrations)"
775
- applied_migration = loader.applied_migrations.get(plan_node)
802
+ if graph.nodes[plan_node].replaces: # type: ignore[union-attr]
803
+ title += f" ({len(graph.nodes[plan_node].replaces)} squashed migrations)" # type: ignore[union-attr]
804
+ applied_migration = loader.applied_migrations.get(plan_node) # type: ignore[union-attr]
776
805
  # Mark it as applied/unapplied
777
806
  if applied_migration:
778
807
  if plan_node in recorded_migrations:
@@ -795,8 +824,8 @@ def show_migrations(package_labels, format, verbosity):
795
824
  migration
796
825
  for migration in recorded_migrations
797
826
  if (
798
- migration not in loader.disk_migrations
799
- and (not package_names or migration[0] in package_names)
827
+ migration not in loader.disk_migrations # type: ignore[operator]
828
+ and (not package_names_list or migration[0] in package_names_list)
800
829
  )
801
830
  ]
802
831
 
@@ -819,7 +848,7 @@ def show_migrations(package_labels, format, verbosity):
819
848
  for name in sorted(prunable_by_package[package]):
820
849
  click.echo(f" - {name}")
821
850
 
822
- def show_plan(db_connection, package_names):
851
+ def show_plan(db_connection: Any, package_names: tuple[str, ...]) -> None:
823
852
  """
824
853
  Show all known migrations (or only those of the specified package_names)
825
854
  in the order they will be applied.
@@ -844,7 +873,7 @@ def show_migrations(package_labels, format, verbosity):
844
873
  seen.add(migration)
845
874
 
846
875
  # Output
847
- def print_deps(node):
876
+ def print_deps(node: Any) -> str:
848
877
  out = []
849
878
  for parent in sorted(node.parents):
850
879
  out.append(f"{parent.key[0]}.{parent.key[1]}")
@@ -856,7 +885,7 @@ def show_migrations(package_labels, format, verbosity):
856
885
  deps = ""
857
886
  if verbosity >= 2:
858
887
  deps = print_deps(node)
859
- if node.key in loader.applied_migrations:
888
+ if node.key in loader.applied_migrations: # type: ignore[operator]
860
889
  click.echo(f"[X] {node.key[0]}.{node.key[1]}{deps}")
861
890
  else:
862
891
  click.echo(f"[ ] {node.key[0]}.{node.key[1]}{deps}")
@@ -896,20 +925,22 @@ def show_migrations(package_labels, format, verbosity):
896
925
  help="Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output",
897
926
  )
898
927
  def squash_migrations(
899
- package_label,
900
- start_migration_name,
901
- migration_name,
902
- no_optimize,
903
- no_input,
904
- squashed_name,
905
- verbosity,
906
- ):
928
+ package_label: str,
929
+ start_migration_name: str | None,
930
+ migration_name: str,
931
+ no_optimize: bool,
932
+ no_input: bool,
933
+ squashed_name: str | None,
934
+ verbosity: int,
935
+ ) -> None:
907
936
  """
908
937
  Squashes an existing set of migrations (from first until specified) into a single new one.
909
938
  """
910
939
  interactive = not no_input
911
940
 
912
- def find_migration(loader, package_label, name):
941
+ def find_migration(
942
+ loader: MigrationLoader, package_label: str, name: str
943
+ ) -> Migration:
913
944
  try:
914
945
  return loader.get_migration_by_prefix(package_label, name)
915
946
  except AmbiguityError:
plain/models/config.py CHANGED
@@ -9,7 +9,7 @@ from .registry import models_registry
9
9
 
10
10
  @register_config
11
11
  class Config(PackageConfig):
12
- def ready(self):
12
+ def ready(self) -> None:
13
13
  # Trigger register calls to fire by importing the modules
14
14
  packages_registry.autodiscover_modules("models", include_app=False)
15
15
 
@@ -1,9 +1,14 @@
1
+ from __future__ import annotations
2
+
1
3
  from importlib import import_module
2
4
  from threading import local
3
- from typing import Any, TypedDict
5
+ from typing import TYPE_CHECKING, Any, TypedDict
4
6
 
5
7
  from plain.runtime import settings as plain_settings
6
8
 
9
+ if TYPE_CHECKING:
10
+ from plain.models.backends.base.base import BaseDatabaseWrapper
11
+
7
12
 
8
13
  class DatabaseConfig(TypedDict, total=False):
9
14
  AUTOCOMMIT: bool
@@ -26,7 +31,7 @@ class DatabaseConnection:
26
31
 
27
32
  __slots__ = ("_settings", "_local")
28
33
 
29
- def __init__(self):
34
+ def __init__(self) -> None:
30
35
  self._settings: DatabaseConfig = {}
31
36
  self._local = local()
32
37
 
@@ -53,21 +58,32 @@ class DatabaseConnection:
53
58
 
54
59
  return database
55
60
 
56
- def create_connection(self):
61
+ def create_connection(self) -> BaseDatabaseWrapper:
57
62
  database_config = self.configure_settings()
58
63
  backend = import_module(f"{database_config['ENGINE']}.base")
59
- return backend.DatabaseWrapper(database_config)
60
64
 
61
- def has_connection(self):
65
+ # Map vendor to wrapper class name
66
+ vendor_map = {
67
+ "plain.models.backends.sqlite3": "SQLiteDatabaseWrapper",
68
+ "plain.models.backends.mysql": "MySQLDatabaseWrapper",
69
+ "plain.models.backends.postgresql": "PostgreSQLDatabaseWrapper",
70
+ }
71
+ wrapper_class_name = vendor_map.get(
72
+ database_config["ENGINE"], "DatabaseWrapper"
73
+ )
74
+ wrapper_class = getattr(backend, wrapper_class_name)
75
+ return wrapper_class(database_config)
76
+
77
+ def has_connection(self) -> bool:
62
78
  return hasattr(self._local, "conn")
63
79
 
64
- def __getattr__(self, attr):
80
+ def __getattr__(self, attr: str) -> Any:
65
81
  if not self.has_connection():
66
82
  self._local.conn = self.create_connection()
67
83
 
68
84
  return getattr(self._local.conn, attr)
69
85
 
70
- def __setattr__(self, name, value):
86
+ def __setattr__(self, name: str, value: Any) -> None:
71
87
  if name.startswith("_"):
72
88
  super().__setattr__(name, value)
73
89
  else: