Skip to content

Commit d323b73

Browse files
committed
fixed model registries and model export
1 parent a50ea41 commit d323b73

File tree

18 files changed

+229
-84
lines changed

18 files changed

+229
-84
lines changed

docs/migrations/env.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ class MyCustomMigrationEnv(AlembicEnvMigrationBase):
138138
139139
"""
140140
key, engine = self.db_service.engines.popitem()
141-
metadata = get_metadata(key, certain=True)
141+
metadata = get_metadata(key, certain=True).metadata
142142

143143
conf_args = {}
144144
conf_args.setdefault(

docs/testing/index.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
3+
4+
## Testing Fixtures
5+
6+
## Alembic Migration with Test Fixture
7+
8+
## Testing a model
9+
10+
## Factory Boy

ellar_sql/exceptions.py

Lines changed: 0 additions & 6 deletions
This file was deleted.

ellar_sql/migrations/multiple.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ def run_migrations_offline(
104104
logger.info("Migrating database %s" % key)
105105

106106
url = str(engine.url).replace("%", "%%")
107-
metadata = get_metadata(key, certain=True)
107+
metadata = get_metadata(key, certain=True).metadata
108108

109109
file_ = "%s.sql" % key
110110
logger.info("Writing output to %s" % file_)
@@ -170,7 +170,7 @@ async def _compute_engine_info(self) -> t.List[DatabaseInfo]:
170170
res = []
171171

172172
for key, engine in self.db_service.engines.items():
173-
metadata = get_metadata(key, certain=True)
173+
metadata = get_metadata(key, certain=True).metadata
174174

175175
if engine.dialect.is_async:
176176
async_engine = AsyncEngine(engine)

ellar_sql/migrations/single.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def run_migrations_offline(
5353
"""
5454

5555
key, engine = self.db_service.engines.popitem()
56-
metadata = get_metadata(key, certain=True)
56+
metadata = get_metadata(key, certain=True).metadata
5757

5858
conf_args = self.get_user_context_configurations()
5959

@@ -95,7 +95,7 @@ async def run_migrations_online(self, context: "EnvironmentContext") -> None:
9595
"""
9696

9797
key, engine = self.db_service.engines.popitem()
98-
metadata = get_metadata(key, certain=True)
98+
metadata = get_metadata(key, certain=True).metadata
9999

100100
migration_action_partial = functools.partial(
101101
self._migration_action, metadata=metadata, context=context

ellar_sql/model/base.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ def _update_metadata(namespace: t.Dict[str, t.Any]) -> None:
2626
if not has_metadata(database_key):
2727
# verify the key exist and store the metadata
2828
metadata.info[DATABASE_BIND_KEY] = database_key
29-
update_database_metadata(database_key, metadata)
29+
update_database_metadata(
30+
database_key, metadata, sa_orm.registry(metadata=metadata)
31+
)
3032
# if we have saved metadata then, its save to remove it and allow
3133
# DatabaseBindKeyMixin to set it back when the class if fully created.
3234
namespace.pop("metadata")
@@ -107,29 +109,30 @@ def __new__(
107109
lambda ns: ns.update(namespace),
108110
)
109111

112+
# model = t.cast(t.Type[sa_orm.DeclarativeBase], model)
113+
110114
if not has_metadata(DEFAULT_KEY):
111115
# Use the model's metadata as the default metadata.
112116
model.metadata.info[DATABASE_BIND_KEY] = DEFAULT_KEY
113-
update_database_metadata(DEFAULT_KEY, model.metadata)
117+
update_database_metadata(DEFAULT_KEY, model.metadata, model.registry)
114118
elif not has_metadata(model.metadata.info.get(DATABASE_BIND_KEY)):
115119
# Use the passed in default metadata as the model's metadata.
116120
model.metadata = get_metadata(DEFAULT_KEY, certain=True)
117121

118122
return model
119123

120124
# _update_metadata(namespace)
125+
__base_config__ = ModelBaseConfig(use_bases=options.use_bases, as_base=True)
121126

122127
base = ModelMeta(
123128
"ModelBase",
124129
bases,
125-
{
126-
"__base_config__": ModelBaseConfig(
127-
use_bases=options.use_bases, as_base=True
128-
)
129-
},
130+
{"__base_config__": __base_config__},
130131
)
131132

132-
return types.new_class(name, (base,), {}, lambda ns: ns.update(namespace))
133+
return types.new_class(
134+
name, (base,), {"options": options}, lambda ns: ns.update(namespace)
135+
)
133136

134137

135138
class ModelBase(ModelDataExportMixin):
@@ -157,9 +160,6 @@ class ModelBase(ModelDataExportMixin):
157160
def __init__(self, **kwargs: t.Any) -> None:
158161
...
159162

160-
def dict(self, exclude: t.Optional[t.Set[str]] = None) -> t.Dict[str, t.Any]:
161-
...
162-
163163
def _sa_inspect_type(self) -> sa.Mapper["ModelBase"]:
164164
...
165165

ellar_sql/model/database_binds.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,40 @@
11
import typing as t
22

33
import sqlalchemy as sa
4-
5-
__model_database_metadata__: t.Dict[str, sa.MetaData] = {}
4+
import sqlalchemy.orm as sa_orm
65

76
from ellar_sql.constant import DEFAULT_KEY
87

98

10-
def update_database_metadata(database_key: str, value: sa.MetaData) -> None:
9+
class DatabaseMetadata(t.NamedTuple):
10+
metadata: sa.MetaData
11+
registry: sa_orm.registry
12+
13+
14+
__model_database_metadata__: t.Dict[str, DatabaseMetadata] = {}
15+
16+
17+
def update_database_metadata(
18+
database_key: str, value: sa.MetaData, registry: sa_orm.registry
19+
) -> None:
1120
"""
1221
Update a metadata based on a database key
1322
"""
14-
__model_database_metadata__[database_key] = value
23+
__model_database_metadata__[database_key] = DatabaseMetadata(
24+
metadata=value, registry=registry
25+
)
1526

1627

17-
def get_all_metadata() -> t.Dict[str, sa.MetaData]:
28+
def get_all_metadata() -> t.Dict[str, DatabaseMetadata]:
1829
"""
1930
Get all metadata available in your application
2031
"""
2132
return __model_database_metadata__.copy()
2233

2334

24-
def get_metadata(database_key: str = DEFAULT_KEY, certain: bool = False) -> sa.MetaData:
35+
def get_metadata(
36+
database_key: str = DEFAULT_KEY, certain: bool = False
37+
) -> DatabaseMetadata:
2538
"""
2639
Gets Metadata associated with a database key
2740
"""

ellar_sql/model/mixins.py

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
import typing as t
22

33
import sqlalchemy as sa
4+
import sqlalchemy.orm as sa_orm
5+
from pydantic.v1 import BaseModel
46

57
from ellar_sql.constant import ABSTRACT_KEY, DATABASE_KEY, DEFAULT_KEY, TABLE_KEY
8+
from ellar_sql.model.utils import (
9+
camel_to_snake_case,
10+
make_metadata,
11+
should_set_table_name,
12+
)
13+
from ellar_sql.schemas import ModelBaseConfig, ModelMetaStore
614

7-
from .utils import camel_to_snake_case, make_metadata, should_set_table_name
15+
16+
class asss(BaseModel):
17+
sd: str
18+
19+
20+
IncEx = t.Union[t.Set[int], t.Set[str], t.Dict[int, t.Any], t.Dict[str, t.Any]]
821

922
if t.TYPE_CHECKING:
1023
from .base import ModelBase
@@ -30,33 +43,48 @@ def __init_subclass__(cls, **kwargs: t.Dict[str, t.Any]) -> None:
3043

3144
class DatabaseBindKeyMixin:
3245
metadata: sa.MetaData
33-
__dnd__ = "Ellar"
3446

3547
def __init_subclass__(cls, **kwargs: t.Dict[str, t.Any]) -> None:
3648
if not ("metadata" in cls.__dict__ or TABLE_KEY in cls.__dict__) and hasattr(
3749
cls, DATABASE_KEY
3850
):
3951
database_bind_key = getattr(cls, DATABASE_KEY, DEFAULT_KEY)
4052
parent_metadata = getattr(cls, "metadata", None)
41-
metadata = make_metadata(database_bind_key)
53+
db_metadata = make_metadata(database_bind_key)
4254

43-
if metadata is not parent_metadata:
44-
cls.metadata = metadata
55+
if db_metadata.metadata is not parent_metadata:
56+
cls.metadata = db_metadata.metadata
57+
cls.registry = db_metadata.registry # type:ignore[attr-defined]
4558

4659
super().__init_subclass__(**kwargs)
4760

4861

4962
class ModelTrackMixin:
5063
metadata: sa.MetaData
64+
__mms__: ModelMetaStore
65+
__table__: sa.Table
5166

5267
def __init_subclass__(cls, **kwargs: t.Dict[str, t.Any]) -> None:
68+
options: ModelBaseConfig = kwargs.pop( # type:ignore[assignment]
69+
"options",
70+
ModelBaseConfig(as_base=False, use_bases=[sa_orm.DeclarativeBase]),
71+
)
72+
5373
super().__init_subclass__(**kwargs)
5474

5575
if TABLE_KEY in cls.__dict__ and ABSTRACT_KEY not in cls.__dict__:
5676
__ellar_sqlalchemy_models__[str(cls)] = cls # type:ignore[assignment]
5777

78+
cls.__mms__ = ModelMetaStore(
79+
base_config=options,
80+
pk_column=None,
81+
columns=list(cls.__table__.columns), # type:ignore[arg-type]
82+
)
83+
5884

5985
class ModelDataExportMixin:
86+
__mms__: t.Optional[ModelMetaStore] = None
87+
6088
def __repr__(self) -> str:
6189
state = sa.inspect(self)
6290
assert state is not None
@@ -70,13 +98,46 @@ def __repr__(self) -> str:
7098

7199
return f"<{type(self).__name__} {pk}>"
72100

73-
def dict(self, exclude: t.Optional[t.Set[str]] = None) -> t.Dict[str, t.Any]:
101+
def _calculate_keys(
102+
self,
103+
include: t.Optional[t.Set[str]],
104+
exclude: t.Optional[t.Set[str]],
105+
) -> t.Set[str]:
106+
keys: t.Set[str] = {k for k in self.__dict__.keys() if not k.startswith("_sa")}
107+
108+
if include is None and exclude is None:
109+
return keys
110+
111+
if include is not None:
112+
keys &= include
113+
114+
if exclude:
115+
keys -= exclude
116+
117+
return keys
118+
119+
def _iter(
120+
self,
121+
include: t.Optional[t.Set[str]],
122+
exclude: t.Optional[t.Set[str]],
123+
exclude_none: bool = False,
124+
) -> t.Generator[t.Tuple[str, t.Any], None, None]:
125+
allowed_keys = self._calculate_keys(include=include, exclude=exclude)
126+
127+
for field_key, v in self.__dict__.items():
128+
if (allowed_keys is not None and field_key not in allowed_keys) or (
129+
exclude_none and v is None
130+
):
131+
continue
132+
yield field_key, v
133+
134+
def dict(
135+
self,
136+
include: t.Optional[t.Set[str]] = None,
137+
exclude: t.Optional[t.Set[str]] = None,
138+
exclude_none: bool = False,
139+
) -> t.Dict[str, t.Any]:
74140
# TODO: implement advance exclude and include that goes deep into relationships too
75-
_exclude: t.Set[str] = set() if not exclude else exclude
76-
77-
tuple_generator = (
78-
(k, v)
79-
for k, v in self.__dict__.items()
80-
if k not in _exclude and not k.startswith("_sa")
141+
return dict(
142+
self._iter(include=include, exclude_none=exclude_none, exclude=exclude)
81143
)
82-
return dict(tuple_generator)

ellar_sql/model/table.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,7 @@ def __new__(
5959
if not args or (len(args) >= 2 and isinstance(args[1], sa.MetaData)):
6060
return super().__new__(cls, *args, **kwargs)
6161

62-
metadata = make_metadata(__database__ or DEFAULT_KEY)
63-
return super().__new__(cls, *[args[0], metadata, *args[1:]], **kwargs)
62+
db_metadata = make_metadata(__database__ or DEFAULT_KEY)
63+
return super().__new__(
64+
cls, *[args[0], db_metadata.metadata, *args[1:]], **kwargs
65+
)

ellar_sql/model/utils.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,32 @@
55

66
from ellar_sql.constant import DATABASE_BIND_KEY, DEFAULT_KEY, NAMING_CONVERSION
77

8-
from .database_binds import get_metadata, has_metadata, update_database_metadata
8+
from .database_binds import (
9+
DatabaseMetadata,
10+
get_metadata,
11+
has_metadata,
12+
update_database_metadata,
13+
)
914

1015

11-
def make_metadata(database_key: str) -> sa.MetaData:
16+
def make_metadata(database_key: str) -> DatabaseMetadata:
1217
if has_metadata(database_key):
1318
return get_metadata(database_key, certain=True)
1419

1520
if database_key != DEFAULT_KEY:
1621
# Copy the naming convention from the default metadata.
17-
naming_convention = make_metadata(DEFAULT_KEY).naming_convention
22+
naming_convention = make_metadata(DEFAULT_KEY).metadata.naming_convention
1823
else:
1924
naming_convention = NAMING_CONVERSION
2025

2126
# Set the bind key in info to be used by session.get_bind.
2227
metadata = sa.MetaData(
2328
naming_convention=naming_convention, info={DATABASE_BIND_KEY: database_key}
2429
)
25-
update_database_metadata(database_key, metadata)
26-
return metadata
30+
update_database_metadata(
31+
database_key, metadata, registry=sa_orm.registry(metadata=metadata)
32+
)
33+
return get_metadata(database_key, certain=True)
2734

2835

2936
def camel_to_snake_case(name: str) -> str:

0 commit comments

Comments
 (0)