fastapi-toolsets 4.1.3__tar.gz → 5.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/PKG-INFO +2 -7
  2. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/README.md +0 -2
  3. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/pyproject.toml +2 -6
  4. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/__init__.py +6 -3
  5. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/cli/commands/fixtures.py +21 -18
  6. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/cli/config.py +32 -21
  7. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/cli/utils.py +2 -1
  8. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/crud/factory.py +7 -6
  9. fastapi_toolsets-5.0.0/src/fastapi_toolsets/db/__init__.py +18 -0
  10. fastapi_toolsets-5.0.0/src/fastapi_toolsets/db/core.py +323 -0
  11. fastapi_toolsets-5.0.0/src/fastapi_toolsets/db/locks.py +185 -0
  12. fastapi_toolsets-5.0.0/src/fastapi_toolsets/db/m2m.py +170 -0
  13. fastapi_toolsets-5.0.0/src/fastapi_toolsets/db/testing.py +69 -0
  14. fastapi_toolsets-5.0.0/src/fastapi_toolsets/db/watch.py +106 -0
  15. fastapi_toolsets-5.0.0/src/fastapi_toolsets/fixtures/__init__.py +34 -0
  16. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/fixtures/registry.py +86 -34
  17. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/fixtures/utils.py +126 -89
  18. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/models/watched.py +7 -2
  19. fastapi_toolsets-5.0.0/src/fastapi_toolsets/pytest/plugin.py +128 -0
  20. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/pytest/utils.py +1 -1
  21. fastapi_toolsets-4.1.3/src/fastapi_toolsets/db.py +0 -591
  22. fastapi_toolsets-4.1.3/src/fastapi_toolsets/fixtures/__init__.py +0 -21
  23. fastapi_toolsets-4.1.3/src/fastapi_toolsets/pytest/plugin.py +0 -210
  24. fastapi_toolsets-4.1.3/src/fastapi_toolsets/security/__init__.py +0 -26
  25. fastapi_toolsets-4.1.3/src/fastapi_toolsets/security/abc.py +0 -55
  26. fastapi_toolsets-4.1.3/src/fastapi_toolsets/security/oauth.py +0 -197
  27. fastapi_toolsets-4.1.3/src/fastapi_toolsets/security/sources/__init__.py +0 -8
  28. fastapi_toolsets-4.1.3/src/fastapi_toolsets/security/sources/bearer.py +0 -120
  29. fastapi_toolsets-4.1.3/src/fastapi_toolsets/security/sources/cookie.py +0 -148
  30. fastapi_toolsets-4.1.3/src/fastapi_toolsets/security/sources/header.py +0 -67
  31. fastapi_toolsets-4.1.3/src/fastapi_toolsets/security/sources/multi.py +0 -71
  32. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/LICENSE +0 -0
  33. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/_imports.py +0 -0
  34. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/cli/__init__.py +0 -0
  35. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/cli/app.py +0 -0
  36. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/cli/commands/__init__.py +0 -0
  37. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/cli/pyproject.py +0 -0
  38. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/crud/__init__.py +0 -0
  39. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/crud/search.py +0 -0
  40. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/dependencies.py +0 -0
  41. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/exceptions/__init__.py +0 -0
  42. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/exceptions/exceptions.py +0 -0
  43. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/exceptions/handler.py +0 -0
  44. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/fixtures/enum.py +0 -0
  45. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/logger.py +0 -0
  46. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/metrics/__init__.py +0 -0
  47. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/metrics/handler.py +0 -0
  48. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/metrics/registry.py +0 -0
  49. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/models/__init__.py +0 -0
  50. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/models/columns.py +0 -0
  51. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/py.typed +0 -0
  52. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/pytest/__init__.py +0 -0
  53. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/schemas.py +0 -0
  54. {fastapi_toolsets-4.1.3 → fastapi_toolsets-5.0.0}/src/fastapi_toolsets/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-toolsets
3
- Version: 4.1.3
3
+ Version: 5.0.0
4
4
  Summary: Production-ready utilities for FastAPI applications
5
5
  Keywords: fastapi,sqlalchemy,postgresql
6
6
  Author: d3vyce
@@ -29,14 +29,12 @@ Requires-Dist: asyncpg>=0.29.0
29
29
  Requires-Dist: fastapi>=0.100.0
30
30
  Requires-Dist: pydantic>=2.0
31
31
  Requires-Dist: sqlalchemy[asyncio]>=2.0
32
- Requires-Dist: fastapi-toolsets[cli,metrics,pytest,security] ; extra == 'all'
32
+ Requires-Dist: fastapi-toolsets[cli,metrics,pytest] ; extra == 'all'
33
33
  Requires-Dist: typer>=0.9.0 ; extra == 'cli'
34
34
  Requires-Dist: prometheus-client>=0.20.0 ; extra == 'metrics'
35
35
  Requires-Dist: httpx>=0.25.0 ; extra == 'pytest'
36
36
  Requires-Dist: pytest-xdist>=3.0.0 ; extra == 'pytest'
37
37
  Requires-Dist: pytest>=8.0.0 ; extra == 'pytest'
38
- Requires-Dist: async-lru>=1.0 ; extra == 'security'
39
- Requires-Dist: httpx>=0.25.0 ; extra == 'security'
40
38
  Requires-Python: >=3.11
41
39
  Project-URL: Homepage, https://github.com/d3vyce/fastapi-toolsets
42
40
  Project-URL: Documentation, https://fastapi-toolsets.d3vyce.fr/
@@ -46,7 +44,6 @@ Provides-Extra: all
46
44
  Provides-Extra: cli
47
45
  Provides-Extra: metrics
48
46
  Provides-Extra: pytest
49
- Provides-Extra: security
50
47
  Description-Content-Type: text/markdown
51
48
 
52
49
  # FastAPI Toolsets
@@ -82,7 +79,6 @@ Install only the extras you need:
82
79
  ```bash
83
80
  uv add "fastapi-toolsets[cli]"
84
81
  uv add "fastapi-toolsets[metrics]"
85
- uv add "fastapi-toolsets[security]"
86
82
  uv add "fastapi-toolsets[pytest]"
87
83
  ```
88
84
 
@@ -108,7 +104,6 @@ uv add "fastapi-toolsets[all]"
108
104
 
109
105
  ### Optional
110
106
 
111
- - **Security**: Composable authentication sources (`BearerTokenAuth`, `CookieAuth`, `APIKeyHeaderAuth`, `MultiAuth`) with HMAC-signed cookies and OAuth 2.0 / OIDC helpers
112
107
  - **CLI**: Django-like command-line interface with fixture management and custom commands support
113
108
  - **Metrics**: Prometheus metrics endpoint with provider/collector registry
114
109
  - **Pytest Helpers**: Async test client, database session management, `pytest-xdist` support, and table cleanup utilities
@@ -31,7 +31,6 @@ Install only the extras you need:
31
31
  ```bash
32
32
  uv add "fastapi-toolsets[cli]"
33
33
  uv add "fastapi-toolsets[metrics]"
34
- uv add "fastapi-toolsets[security]"
35
34
  uv add "fastapi-toolsets[pytest]"
36
35
  ```
37
36
 
@@ -57,7 +56,6 @@ uv add "fastapi-toolsets[all]"
57
56
 
58
57
  ### Optional
59
58
 
60
- - **Security**: Composable authentication sources (`BearerTokenAuth`, `CookieAuth`, `APIKeyHeaderAuth`, `MultiAuth`) with HMAC-signed cookies and OAuth 2.0 / OIDC helpers
61
59
  - **CLI**: Django-like command-line interface with fixture management and custom commands support
62
60
  - **Metrics**: Prometheus metrics endpoint with provider/collector registry
63
61
  - **Pytest Helpers**: Async test client, database session management, `pytest-xdist` support, and table cleanup utilities
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fastapi-toolsets"
3
- version = "4.1.3"
3
+ version = "5.0.0"
4
4
  description = "Production-ready utilities for FastAPI applications"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -50,17 +50,13 @@ cli = [
50
50
  metrics = [
51
51
  "prometheus_client>=0.20.0",
52
52
  ]
53
- security = [
54
- "async-lru>=1.0",
55
- "httpx>=0.25.0",
56
- ]
57
53
  pytest = [
58
54
  "httpx>=0.25.0",
59
55
  "pytest-xdist>=3.0.0",
60
56
  "pytest>=8.0.0",
61
57
  ]
62
58
  all = [
63
- "fastapi-toolsets[cli,metrics,pytest,security]",
59
+ "fastapi-toolsets[cli,metrics,pytest]",
64
60
  ]
65
61
 
66
62
  [project.scripts]
@@ -7,18 +7,21 @@ Example usage:
7
7
  from fastapi import FastAPI, Depends
8
8
  from fastapi_toolsets.exceptions import init_exceptions_handlers
9
9
  from fastapi_toolsets.crud import CrudFactory
10
- from fastapi_toolsets.db import create_db_dependency
10
+ from fastapi_toolsets.db import Database
11
11
  from fastapi_toolsets.schemas import Response
12
12
 
13
+ db = Database("postgresql+asyncpg://postgres:postgres@localhost/app")
14
+
13
15
  app = FastAPI()
16
+ db.install(app)
14
17
  init_exceptions_handlers(app)
15
18
 
16
19
  UserCrud = CrudFactory(User)
17
20
 
18
21
  @app.get("/users/{user_id}", response_model=Response[dict])
19
- async def get_user(user_id: int, session = Depends(get_db)):
22
+ async def get_user(user_id: int, session = Depends(db)):
20
23
  user = await UserCrud.get(session, [User.id == user_id])
21
24
  return Response(data={"user": user.username}, message="Success")
22
25
  """
23
26
 
24
- __version__ = "4.1.3"
27
+ __version__ = "5.0.0"
@@ -6,7 +6,8 @@ import typer
6
6
  from rich.console import Console
7
7
  from rich.table import Table
8
8
 
9
- from ...fixtures import Context, LoadStrategy, load_fixtures_by_context
9
+ from ...fixtures import Context, LoadStrategy
10
+ from ...logger import get_logger
10
11
  from ..config import get_db_context, get_fixtures_registry
11
12
  from ..utils import async_command
12
13
 
@@ -16,13 +17,14 @@ fixture_cli = typer.Typer(
16
17
  no_args_is_help=True,
17
18
  )
18
19
  console = Console()
20
+ logger = get_logger()
19
21
 
20
22
 
21
23
  @fixture_cli.command("list")
22
24
  def list_fixtures(
23
25
  ctx: typer.Context,
24
26
  context: Annotated[
25
- Context | None,
27
+ str | None,
26
28
  typer.Option(
27
29
  "--context",
28
30
  "-c",
@@ -32,10 +34,10 @@ def list_fixtures(
32
34
  ) -> None:
33
35
  """List all registered fixtures."""
34
36
  registry = get_fixtures_registry()
35
- fixtures = registry.get_by_context(context.value) if context else registry.get_all()
37
+ fixtures = registry.get_by_context(context) if context else registry.get_all()
36
38
 
37
39
  if not fixtures:
38
- print("No fixtures found.")
40
+ logger.info("No fixtures found.")
39
41
  return
40
42
 
41
43
  table = Table("Name", "Contexts", "Dependencies")
@@ -46,7 +48,7 @@ def list_fixtures(
46
48
  table.add_row(fixture.name, contexts, deps)
47
49
 
48
50
  console.print(table)
49
- print(f"\nTotal: {len(fixtures)} fixture(s)")
51
+ logger.info("Total: %d fixture(s)", len(fixtures))
50
52
 
51
53
 
52
54
  @fixture_cli.command("load")
@@ -54,8 +56,8 @@ def list_fixtures(
54
56
  async def load(
55
57
  ctx: typer.Context,
56
58
  contexts: Annotated[
57
- list[Context] | None,
58
- typer.Argument(help="Contexts to load."),
59
+ list[str] | None,
60
+ typer.Argument(help="Contexts to load.", envvar="FIXTURES_CONTEXT"),
59
61
  ] = None,
60
62
  strategy: Annotated[
61
63
  LoadStrategy,
@@ -69,26 +71,27 @@ async def load(
69
71
  ] = False,
70
72
  ) -> None:
71
73
  """Load fixtures into the database."""
74
+ from ...fixtures import load_fixtures_by_context
75
+
72
76
  registry = get_fixtures_registry()
73
77
  db_context = get_db_context()
74
78
 
75
- context_list = list(contexts) if contexts else [Context.BASE]
79
+ context_list = contexts or [Context.BASE.value]
76
80
 
77
81
  ordered = registry.resolve_context_dependencies(*context_list)
78
82
 
79
83
  if not ordered:
80
- print("No fixtures to load for the specified context(s).")
84
+ logger.info("No fixtures to load for the specified context(s).")
81
85
  return
82
86
 
83
- print(f"\nFixtures to load ({strategy.value} strategy):")
84
- for name in ordered:
85
- fixture = registry.get(name)
86
- instances = list(fixture.func())
87
- model_name = type(instances[0]).__name__ if instances else "?"
88
- print(f" - {name}: {len(instances)} {model_name}(s)")
89
-
90
87
  if dry_run:
91
- print("\n[Dry run - no changes made]")
88
+ logger.info("Fixtures to load (%s strategy):", strategy.value)
89
+ for name in ordered:
90
+ variants = registry.get_load_variants(name, *context_list)
91
+ instances = [inst for v in variants for inst in v.func()]
92
+ model_name = type(instances[0]).__name__ if instances else "?"
93
+ logger.info(" - %s: %d %s(s)", name, len(instances), model_name)
94
+ logger.info("[Dry run - no changes made]")
92
95
  return
93
96
 
94
97
  async with db_context() as session:
@@ -97,4 +100,4 @@ async def load(
97
100
  )
98
101
 
99
102
  total = sum(len(items) for items in result.values())
100
- print(f"\nLoaded {total} record(s) successfully.")
103
+ logger.info("Loaded %d record(s) successfully.", total)
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import importlib
6
6
  import sys
7
- from typing import TYPE_CHECKING, Any, Literal, overload
7
+ from typing import TYPE_CHECKING, Any, Literal, TypeVar, overload
8
8
 
9
9
  import typer
10
10
 
@@ -13,6 +13,8 @@ from .pyproject import find_pyproject, load_pyproject
13
13
  if TYPE_CHECKING:
14
14
  from ..fixtures import FixtureRegistry
15
15
 
16
+ T = TypeVar("T")
17
+
16
18
 
17
19
  def _ensure_project_in_path():
18
20
  """Add project root to sys.path if not installed in editable mode."""
@@ -88,19 +90,39 @@ def get_config_value(key: str, required: bool = False) -> Any | None:
88
90
  return value
89
91
 
90
92
 
91
- def get_fixtures_registry() -> FixtureRegistry:
92
- """Import and return the fixtures registry from config."""
93
- from ..fixtures import FixtureRegistry
93
+ @overload
94
+ def _import_typed(
95
+ key: str, expected_type: type[T], *, required: Literal[True]
96
+ ) -> T: ... # pragma: no cover
97
+ @overload
98
+ def _import_typed(
99
+ key: str, expected_type: type[T], *, required: bool
100
+ ) -> T | None: ... # pragma: no cover
101
+ def _import_typed(key: str, expected_type: type[T], *, required: bool) -> T | None:
102
+ """Import a config value by key and validate its type.
94
103
 
95
- import_path = get_config_value("fixtures", required=True)
96
- registry = import_from_string(import_path)
104
+ Raises:
105
+ typer.BadParameter: If required and missing, or if the imported
106
+ value isn't an instance of *expected_type*.
107
+ """
108
+ import_path = get_config_value(key, required=required)
109
+ if not import_path:
110
+ return None
97
111
 
98
- if not isinstance(registry, FixtureRegistry):
112
+ obj = import_from_string(import_path)
113
+ if not isinstance(obj, expected_type):
99
114
  raise typer.BadParameter(
100
- f"'fixtures' must be a FixtureRegistry instance, got {type(registry).__name__}"
115
+ f"'{key}' must be a {expected_type.__name__} instance, got {type(obj).__name__}"
101
116
  )
102
117
 
103
- return registry
118
+ return obj
119
+
120
+
121
+ def get_fixtures_registry() -> FixtureRegistry:
122
+ """Import and return the fixtures registry from config."""
123
+ from ..fixtures import FixtureRegistry
124
+
125
+ return _import_typed("fixtures", FixtureRegistry, required=True)
104
126
 
105
127
 
106
128
  def get_db_context() -> Any:
@@ -111,15 +133,4 @@ def get_db_context() -> Any:
111
133
 
112
134
  def get_custom_cli() -> typer.Typer | None:
113
135
  """Import and return the custom CLI Typer instance from config."""
114
- import_path = get_config_value("custom_cli")
115
- if not import_path:
116
- return None
117
-
118
- custom = import_from_string(import_path)
119
-
120
- if not isinstance(custom, typer.Typer):
121
- raise typer.BadParameter(
122
- f"'custom_cli' must be a Typer instance, got {type(custom).__name__}"
123
- )
124
-
125
- return custom
136
+ return _import_typed("custom_cli", typer.Typer, required=False)
@@ -1,6 +1,5 @@
1
1
  """CLI utility functions."""
2
2
 
3
- import asyncio
4
3
  import functools
5
4
  from collections.abc import Callable, Coroutine
6
5
  from typing import Any, ParamSpec, TypeVar
@@ -24,6 +23,8 @@ def async_command(func: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]:
24
23
 
25
24
  @functools.wraps(func)
26
25
  def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
26
+ import asyncio
27
+
27
28
  return asyncio.run(func(*args, **kwargs))
28
29
 
29
30
  return wrapper
@@ -22,7 +22,7 @@ from sqlalchemy.orm import DeclarativeBase, QueryableAttribute, selectinload
22
22
  from sqlalchemy.sql.base import ExecutableOption
23
23
  from sqlalchemy.sql.roles import WhereHavingRole
24
24
 
25
- from ..db import get_transaction
25
+ from ..db import transaction
26
26
  from ..exceptions import InvalidOrderFieldError, NotFoundError
27
27
  from ..schemas import (
28
28
  CursorPaginatedResponse,
@@ -190,7 +190,8 @@ class AsyncCrud(Generic[ModelType]):
190
190
  """Re-query instance by PK with default_load_options applied."""
191
191
  mapper = cls.model.__mapper__
192
192
  pk_filters = [
193
- getattr(cls.model, col.key) == getattr(instance, col.key)
193
+ getattr(cls.model, cast(str, col.key))
194
+ == getattr(instance, cast(str, col.key))
194
195
  for col in mapper.primary_key
195
196
  ]
196
197
  return await cls.get(session, filters=pk_filters)
@@ -716,7 +717,7 @@ class AsyncCrud(Generic[ModelType]):
716
717
  Returns:
717
718
  Created model instance, or ``Response[schema]`` when ``schema`` is given.
718
719
  """
719
- async with get_transaction(session):
720
+ async with transaction(session):
720
721
  m2m_exclude = cls._m2m_schema_fields()
721
722
  data = (
722
723
  obj.model_dump(exclude=m2m_exclude) if m2m_exclude else obj.model_dump()
@@ -1067,7 +1068,7 @@ class AsyncCrud(Generic[ModelType]):
1067
1068
  Raises:
1068
1069
  NotFoundError: If no record found
1069
1070
  """
1070
- async with get_transaction(session):
1071
+ async with transaction(session):
1071
1072
  m2m_exclude = cls._m2m_schema_fields()
1072
1073
 
1073
1074
  # Eagerly load M2M relationships that will be updated so that
@@ -1127,7 +1128,7 @@ class AsyncCrud(Generic[ModelType]):
1127
1128
  Returns:
1128
1129
  Model instance
1129
1130
  """
1130
- async with get_transaction(session):
1131
+ async with transaction(session):
1131
1132
  values = obj.model_dump(exclude_unset=True)
1132
1133
  q = insert(cls.model).values(**values)
1133
1134
  if set_:
@@ -1189,7 +1190,7 @@ class AsyncCrud(Generic[ModelType]):
1189
1190
  Returns:
1190
1191
  ``None``, or ``Response[None]`` when ``return_response=True``.
1191
1192
  """
1192
- async with get_transaction(session):
1193
+ async with transaction(session):
1193
1194
  result = await session.execute(select(cls.model).where(and_(*filters)))
1194
1195
  objects = result.scalars().all()
1195
1196
  for obj in objects:
@@ -0,0 +1,18 @@
1
+ """Database package: the ``Database`` facade plus PostgreSQL power-tools."""
2
+
3
+ from .core import Database, transaction
4
+ from .locks import LockMode, advisory_lock, lock_tables
5
+ from .m2m import m2m_add, m2m_remove, m2m_set
6
+ from .watch import wait_for_row_change
7
+
8
+ __all__ = [
9
+ "Database",
10
+ "LockMode",
11
+ "advisory_lock",
12
+ "lock_tables",
13
+ "m2m_add",
14
+ "m2m_remove",
15
+ "m2m_set",
16
+ "transaction",
17
+ "wait_for_row_change",
18
+ ]