sqlakit 0.5.0__tar.gz → 0.5.1__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 (26) hide show
  1. {sqlakit-0.5.0 → sqlakit-0.5.1}/PKG-INFO +1 -1
  2. {sqlakit-0.5.0 → sqlakit-0.5.1}/pyproject.toml +1 -1
  3. {sqlakit-0.5.0 → sqlakit-0.5.1}/pyproject.toml.orig +1 -1
  4. {sqlakit-0.5.0 → sqlakit-0.5.1}/sqlakit/__init__.py +4 -0
  5. {sqlakit-0.5.0 → sqlakit-0.5.1}/sqlakit/_base.py +27 -0
  6. {sqlakit-0.5.0 → sqlakit-0.5.1}/sqlakit/_model.py +57 -0
  7. {sqlakit-0.5.0 → sqlakit-0.5.1}/sqlakit/exceptions.py +23 -0
  8. {sqlakit-0.5.0 → sqlakit-0.5.1}/LICENSE +0 -0
  9. {sqlakit-0.5.0 → sqlakit-0.5.1}/README.md +0 -0
  10. {sqlakit-0.5.0 → sqlakit-0.5.1}/sqlakit/_db.py +0 -0
  11. {sqlakit-0.5.0 → sqlakit-0.5.1}/sqlakit/_discovery.py +0 -0
  12. {sqlakit-0.5.0 → sqlakit-0.5.1}/sqlakit/_query.py +0 -0
  13. {sqlakit-0.5.0 → sqlakit-0.5.1}/sqlakit/_recording.py +0 -0
  14. {sqlakit-0.5.0 → sqlakit-0.5.1}/sqlakit/_registry.py +0 -0
  15. {sqlakit-0.5.0 → sqlakit-0.5.1}/sqlakit/_routing.py +0 -0
  16. {sqlakit-0.5.0 → sqlakit-0.5.1}/sqlakit/_sql.py +0 -0
  17. {sqlakit-0.5.0 → sqlakit-0.5.1}/sqlakit/asyncio/__init__.py +0 -0
  18. {sqlakit-0.5.0 → sqlakit-0.5.1}/sqlakit/asyncio/_db.py +0 -0
  19. {sqlakit-0.5.0 → sqlakit-0.5.1}/sqlakit/asyncio/_registry.py +0 -0
  20. {sqlakit-0.5.0 → sqlakit-0.5.1}/sqlakit/asyncio/orm.py +0 -0
  21. {sqlakit-0.5.0 → sqlakit-0.5.1}/sqlakit/asyncio/sql.py +0 -0
  22. {sqlakit-0.5.0 → sqlakit-0.5.1}/sqlakit/orm.py +0 -0
  23. {sqlakit-0.5.0 → sqlakit-0.5.1}/sqlakit/py.typed +0 -0
  24. {sqlakit-0.5.0 → sqlakit-0.5.1}/sqlakit/sql.py +0 -0
  25. {sqlakit-0.5.0 → sqlakit-0.5.1}/sqlakit/testing.py +0 -0
  26. {sqlakit-0.5.0 → sqlakit-0.5.1}/sqlakit/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlakit
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Summary: A toolkit for SQLAlchemy applications.
5
5
  Keywords: sqlalchemy,database,orm,sql,asyncio
6
6
  Author: Anton Ruhlov
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlakit"
3
- version = "0.5.0"
3
+ version = "0.5.1"
4
4
  description = "A toolkit for SQLAlchemy applications."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlakit"
3
- version = "0.5.0"
3
+ version = "0.5.1"
4
4
  description = "A toolkit for SQLAlchemy applications."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -7,11 +7,13 @@ from ._registry import Databases, db
7
7
  from ._routing import Router
8
8
  from .exceptions import (
9
9
  DEFAULT_ALIAS,
10
+ AliasInUseError,
10
11
  AsyncFilterError,
11
12
  BulkQueryError,
12
13
  ConflictingDatabaseUrlError,
13
14
  DatabaseAlreadyConfiguredError,
14
15
  DatabaseNotConfiguredError,
16
+ DefaultAliasError,
15
17
  DetachedInstanceError,
16
18
  InstanceNotFoundError,
17
19
  InvalidCursorError,
@@ -55,6 +57,7 @@ __all__ = [
55
57
  "DEFAULT_ALIAS",
56
58
  "DEFAULT_ENGINE_ARGS",
57
59
  "DEFAULT_SESSION_ARGS",
60
+ "AliasInUseError",
58
61
  "AsyncFilterError",
59
62
  "BulkQueryError",
60
63
  "ConflictingDatabaseUrlError",
@@ -64,6 +67,7 @@ __all__ = [
64
67
  "DatabaseConfig",
65
68
  "DatabaseNotConfiguredError",
66
69
  "Databases",
70
+ "DefaultAliasError",
67
71
  "DetachedInstanceError",
68
72
  "EngineArgs",
69
73
  "InstanceNotFoundError",
@@ -35,9 +35,11 @@ from ._recording import (
35
35
  from ._routing import Router, as_router
36
36
  from .exceptions import (
37
37
  DEFAULT_ALIAS,
38
+ AliasInUseError,
38
39
  ConflictingDatabaseUrlError,
39
40
  DatabaseAlreadyConfiguredError,
40
41
  DatabaseNotConfiguredError,
42
+ DefaultAliasError,
41
43
  MissingConnectionError,
42
44
  MissingDatabaseUrlError,
43
45
  MissingDefaultDatabaseError,
@@ -693,6 +695,31 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
693
695
  def __contains__(self, alias: str) -> bool:
694
696
  return alias == DEFAULT_ALIAS or alias in self._aliased
695
697
 
698
+ def register(self, alias: str, db: DatabaseT) -> None:
699
+ """Put a database already built under an alias.
700
+
701
+ `configure` takes settings and builds the databases. This takes one you
702
+ built yourself, for a shard that only exists once the application is
703
+ running, or for a registry that never reads settings at all:
704
+
705
+ ```python
706
+ db.register("shard-7", Database(SHARD_URL))
707
+ ```
708
+
709
+ The alias has to be free. Replacing one under a name already in use
710
+ would leave the code that holds the old database talking to it.
711
+
712
+ Raises:
713
+ AliasInUseError: if another database holds that alias.
714
+ DefaultAliasError: if the alias is `default`, which this registry is.
715
+
716
+ """
717
+ if alias == DEFAULT_ALIAS:
718
+ raise DefaultAliasError
719
+ if alias in self._aliased:
720
+ raise AliasInUseError(alias)
721
+ self._aliased[alias] = self._named(alias, db)
722
+
696
723
  @contextmanager
697
724
  def recording(
698
725
  self,
@@ -37,7 +37,9 @@ __all__ = [
37
37
  "BaseModel",
38
38
  "BaseSoftDeletes",
39
39
  "DatabaseDescriptor",
40
+ "DatabaseRegistry",
40
41
  "DatabaseSource",
42
+ "RegistryDescriptor",
41
43
  "db_for",
42
44
  "resolve_alias",
43
45
  "soft_delete_column",
@@ -66,6 +68,19 @@ class DatabaseDescriptor(Generic[DatabaseT]):
66
68
  return cast("DatabaseT", db_for(owner))
67
69
 
68
70
 
71
+ class DatabaseRegistry(DatabaseSource, Protocol):
72
+ """A source that also takes a database under an alias it does not have."""
73
+
74
+ def register(self, alias: str, db: Any) -> None: ... # noqa: ANN401
75
+
76
+
77
+ class RegistryDescriptor:
78
+ """Reads ``__dbs__``, on the class as well as on an instance."""
79
+
80
+ def __get__(self, instance: object | None, owner: type[Any]) -> Any: # noqa: ANN401
81
+ return owner.__dbs__
82
+
83
+
69
84
  class BaseModel(Generic[DatabaseT]):
70
85
  """What the sync and async models share: everything that is not IO.
71
86
 
@@ -87,6 +102,7 @@ class BaseModel(Generic[DatabaseT]):
87
102
  # Unannotated on purpose: an annotation here reads as a field to
88
103
  # pydantic, and SQLModel models would refuse to build.
89
104
  db = DatabaseDescriptor[DatabaseT]()
105
+ dbs = RegistryDescriptor()
90
106
 
91
107
  @classmethod
92
108
  def set_db(cls, db: str | DatabaseT) -> None:
@@ -103,6 +119,37 @@ class BaseModel(Generic[DatabaseT]):
103
119
  """
104
120
  cls.__db__ = db
105
121
 
122
+ @classmethod
123
+ def register_db(cls, db: DatabaseT, *, alias: str) -> None:
124
+ """Give this model a database under an alias, and the ones under it too.
125
+
126
+ The registry it goes in belongs to this class, so nothing global is
127
+ configured and two sets of models can each have their own `shard`:
128
+
129
+ ```python
130
+ Base.register_db(Database(DB1_URL), alias="db1")
131
+ Base.register_db(Database(DB2_URL), alias="db2")
132
+
133
+ with Base.dbs.using("db2").transaction():
134
+ User(name="ada").save()
135
+ ```
136
+
137
+ Which database a model resolves to is still `__db__`, the routers and
138
+ the open `using()` block, in that order. A model left on the default
139
+ alias follows `using()`, which is what makes the switch above work.
140
+
141
+ Raises:
142
+ AliasInUseError: if another database holds that alias.
143
+ DefaultAliasError: if the alias is `default`.
144
+
145
+ """
146
+ if _owns_no_registry(cls):
147
+ # A registry of its own: registering into the importable one would
148
+ # configure it for every model in the process. A class under one
149
+ # that already has its own registers into that one.
150
+ cls.__dbs__ = type(cls.__dbs__)()
151
+ cast("DatabaseRegistry", cls.__dbs__).register(alias, db)
152
+
106
153
  def update(self, values: Mapping[str, Any]) -> Self:
107
154
  """Set these fields on this instance, and return it.
108
155
 
@@ -187,6 +234,16 @@ class BaseModel(Generic[DatabaseT]):
187
234
  self.db.session.add(self)
188
235
 
189
236
 
237
+ def _owns_no_registry(model: type[Any]) -> bool:
238
+ """Whether this model still looks its aliases up in the importable registry."""
239
+ declared = next(
240
+ (klass for klass in model.__mro__ if "__dbs__" in klass.__dict__), None
241
+ )
242
+ return (
243
+ declared is None or declared.__module__.split(".")[0] == __name__.split(".")[0]
244
+ )
245
+
246
+
190
247
  def db_for(model: type[Any]) -> BaseDatabase[Any, Any]:
191
248
  """Return the database a model lives on.
192
249
 
@@ -5,11 +5,13 @@ from sqlalchemy.orm import exc as sa_exc
5
5
  DEFAULT_ALIAS = "default"
6
6
 
7
7
  __all__ = [
8
+ "AliasInUseError",
8
9
  "AsyncFilterError",
9
10
  "BulkQueryError",
10
11
  "ConflictingDatabaseUrlError",
11
12
  "DatabaseAlreadyConfiguredError",
12
13
  "DatabaseNotConfiguredError",
14
+ "DefaultAliasError",
13
15
  "DetachedInstanceError",
14
16
  "InstanceNotFoundError",
15
17
  "InvalidCursorError",
@@ -107,6 +109,27 @@ class DatabaseAlreadyConfiguredError(SQLAKitError, RuntimeError):
107
109
  super().__init__(message)
108
110
 
109
111
 
112
+ class AliasInUseError(SQLAKitError, ValueError):
113
+ """Raised when registering a database under an alias another one holds."""
114
+
115
+ def __init__(self, alias: str) -> None:
116
+ self.alias = alias
117
+ super().__init__(
118
+ f"`{alias}` is already registered. Dispose of that database before "
119
+ "registering another under the same name."
120
+ )
121
+
122
+
123
+ class DefaultAliasError(SQLAKitError, ValueError):
124
+ """Raised when registering a database as the default one."""
125
+
126
+ def __init__(self) -> None:
127
+ super().__init__(
128
+ f"`{DEFAULT_ALIAS}` is the registry itself. Configure it with "
129
+ "`configure()`, and register the others under their own names."
130
+ )
131
+
132
+
110
133
  class UnknownDatabaseError(SQLAKitError, KeyError):
111
134
  """Raised when asking for a database alias that was never configured."""
112
135
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes