Skip to content

Commit 55a0bba

Browse files
committed
Added table, image and file fields and some code refactoring
1 parent 0e01c85 commit 55a0bba

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+482
-832
lines changed

README.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,8 @@
1010

1111
## Project Status
1212
- 70% done
13-
- SQLAlchemy Table support with ModelSession
14-
- Migration custom revision directives
13+
- tests
1514
- Documentation
16-
- File Field
17-
- Image Field
1815

1916
## Introduction
2017
Ellar SQLAlchemy Module simplifies the integration of SQLAlchemy and Alembic migration tooling into your ellar application.

ellar_sqlalchemy/constant.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@
66
TABLE_KEY = "__table__"
77
ABSTRACT_KEY = "__abstract__"
88

9+
NAMING_CONVERSION = {
10+
"ix": "ix_%(column_0_label)s",
11+
"uq": "uq_%(table_name)s_%(column_0_name)s",
12+
"ck": "ck_%(table_name)s_%(constraint_name)s",
13+
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
14+
"pk": "pk_%(table_name)s",
15+
}
16+
917

1018
class DeclarativeBasePlaceHolder(sa_orm.DeclarativeBase):
1119
pass

ellar_sqlalchemy/migrations/base.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ def __init__(self, db_service: EllarSQLAlchemyService) -> None:
1818
self.db_service = db_service
1919
self.use_two_phase = db_service.migration_options.use_two_phase
2020

21+
def get_user_context_configurations(self) -> t.Dict[str, t.Any]:
22+
conf_args = dict(self.db_service.migration_options.context_configure)
23+
24+
# detecting type changes
25+
conf_args.setdefault("compare_type", True)
26+
conf_args.setdefault("render_as_batch", True)
27+
# If you want to ignore things like these, set the following as a class attribute
28+
# __table_args__ = {"info": {"skip_autogen": True}}
29+
conf_args.setdefault("include_object", self.include_object)
30+
conf_args.setdefault("dialect_opts", {"paramstyle": "named"})
31+
return conf_args
32+
2133
def include_object(
2234
self,
2335
obj: SchemaItem,

ellar_sqlalchemy/migrations/multiple.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ def run_migrations_offline(self, context: "EnvironmentContext") -> None:
9696
# for --sql use case, run migrations for each URL into
9797
# individual files.
9898

99+
conf_args = self.get_user_context_configurations()
100+
99101
for key, engine in self.db_service.engines.items():
100102
logger.info("Migrating database %s" % key)
101103

@@ -104,18 +106,14 @@ def run_migrations_offline(self, context: "EnvironmentContext") -> None:
104106

105107
file_ = "%s.sql" % key
106108
logger.info("Writing output to %s" % file_)
109+
107110
with open(file_, "w") as buffer:
108111
context.configure(
109112
url=url,
110113
output_buffer=buffer,
111114
target_metadata=metadata,
112115
literal_binds=True,
113-
dialect_opts={"paramstyle": "named"},
114-
# If you want to ignore things like these, set the following as a class attribute
115-
# __table_args__ = {"info": {"skip_autogen": True}}
116-
include_object=self.include_object,
117-
# detecting type changes
118-
# compare_type=True,
116+
**conf_args,
119117
)
120118
with context.begin_transaction():
121119
context.run_migrations(engine_name=key)
@@ -126,12 +124,11 @@ def _migration_action(
126124
# this callback is used to prevent an auto-migration from being generated
127125
# when there are no changes to the schema
128126
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
129-
conf_args = {
130-
"process_revision_directives": self.default_process_revision_directives
131-
}
132-
# conf_args = current_app.extensions['migrate'].configure_args
133-
# if conf_args.get("process_revision_directives") is None:
134-
# conf_args["process_revision_directives"] = process_revision_directives
127+
128+
conf_args = self.get_user_context_configurations()
129+
conf_args.setdefault(
130+
"process_revision_directives", self.default_process_revision_directives
131+
)
135132

136133
try:
137134
for db_info in db_infos:

ellar_sqlalchemy/migrations/single.py

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,13 @@ def run_migrations_offline(self, context: "EnvironmentContext") -> None:
5353
key, engine = self.db_service.engines.popitem()
5454
metadata = get_database_bind(key, certain=True)
5555

56+
conf_args = self.get_user_context_configurations()
57+
5658
context.configure(
5759
url=str(engine.url).replace("%", "%%"),
5860
target_metadata=metadata,
5961
literal_binds=True,
60-
dialect_opts={"paramstyle": "named"},
61-
# If you want to ignore things like these, set the following as a class attribute
62-
# __table_args__ = {"info": {"skip_autogen": True}}
63-
include_object=self.include_object,
64-
# detecting type changes
65-
# compare_type=True,
62+
**conf_args,
6663
)
6764

6865
with context.begin_transaction():
@@ -77,12 +74,10 @@ def _migration_action(
7774
# this callback is used to prevent an auto-migration from being generated
7875
# when there are no changes to the schema
7976
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
80-
conf_args = {
81-
"process_revision_directives": self.default_process_revision_directives
82-
}
83-
# conf_args = current_app.extensions['migrate'].configure_args
84-
# if conf_args.get("process_revision_directives") is None:
85-
# conf_args["process_revision_directives"] = process_revision_directives
77+
conf_args = self.get_user_context_configurations()
78+
conf_args.setdefault(
79+
"process_revision_directives", self.default_process_revision_directives
80+
)
8681

8782
context.configure(connection=connection, target_metadata=metadata, **conf_args)
8883

ellar_sqlalchemy/model/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from .base import Model
2+
from .table import Table
23
from .typeDecorator import GUID, GenericIP
34
from .utils import make_metadata
45

56
__all__ = [
67
"Model",
8+
"Table",
79
"make_metadata",
810
"GUID",
911
"GenericIP",

ellar_sqlalchemy/model/base.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import types
22
import typing as t
33

4+
import sqlalchemy as sa
45
import sqlalchemy.orm as sa_orm
56
from sqlalchemy.ext.asyncio import AsyncSession
6-
from sqlalchemy.orm import DeclarativeBase
77

88
from ellar_sqlalchemy.constant import (
99
DATABASE_BIND_KEY,
1010
DEFAULT_KEY,
11+
NAMING_CONVERSION,
1112
)
1213

1314
from .database_binds import get_database_bind, has_database_bind, update_database_binds
@@ -70,7 +71,7 @@ def get_session(cls: t.Type[Model]) -> None:
7071
return SQLAlchemyDefaultBase
7172

7273

73-
class ModelMeta(type(DeclarativeBase)): # type:ignore[misc]
74+
class ModelMeta(type(sa_orm.DeclarativeBase)): # type:ignore[misc]
7475
def __new__(
7576
mcs,
7677
name: str,
@@ -92,11 +93,20 @@ def __new__(
9293

9394
if not skip_default_base_check:
9495
if SQLAlchemyDefaultBase is None:
95-
raise Exception(
96-
"EllarSQLAlchemy Default Declarative Base has not been configured."
97-
"\nPlease call `configure_model_declarative_base` before ORM Model construction"
98-
" or Use EllarSQLAlchemy Service"
96+
# raise Exception(
97+
# "EllarSQLAlchemy Default Declarative Base has not been configured."
98+
# "\nPlease call `configure_model_declarative_base` before ORM Model construction"
99+
# " or Use EllarSQLAlchemy Service"
100+
# )
101+
_model_as_base(
102+
"SQLAlchemyDefaultBase",
103+
(),
104+
{
105+
"skip_default_base_check": True,
106+
"metadata": sa.MetaData(naming_convention=NAMING_CONVERSION),
107+
},
99108
)
109+
_bases = [SQLAlchemyDefaultBase, *_bases]
100110
elif SQLAlchemyDefaultBase and SQLAlchemyDefaultBase not in _bases:
101111
_bases = [SQLAlchemyDefaultBase, *_bases]
102112

ellar_sqlalchemy/model/table.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import typing as t
2+
3+
import sqlalchemy as sa
4+
import sqlalchemy.sql.schema as sa_sql_schema
5+
6+
from ellar_sqlalchemy.constant import DEFAULT_KEY
7+
from ellar_sqlalchemy.model import make_metadata
8+
9+
10+
class Table(sa.Table):
11+
"""
12+
Custom SQLAlchemy Table class that supports database-binding
13+
E.g.:
14+
15+
user_book_m2m = Table(
16+
"user_book",
17+
sa.Column("user_id", sa.ForeignKey(User.id), primary_key=True),
18+
sa.Column("book_id", sa.ForeignKey(Book.id), primary_key=True),
19+
__database__='default'
20+
)
21+
"""
22+
23+
@t.overload
24+
def __init__(
25+
self,
26+
name: str,
27+
*args: sa_sql_schema.SchemaItem,
28+
__database__: t.Optional[str] = None,
29+
**kwargs: t.Any,
30+
) -> None:
31+
...
32+
33+
@t.overload
34+
def __init__(
35+
self,
36+
name: str,
37+
metadata: sa.MetaData,
38+
*args: sa_sql_schema.SchemaItem,
39+
**kwargs: t.Any,
40+
) -> None:
41+
...
42+
43+
@t.overload
44+
def __init__(
45+
self, name: str, *args: sa_sql_schema.SchemaItem, **kwargs: t.Any
46+
) -> None:
47+
...
48+
49+
def __init__(
50+
self, name: str, *args: sa_sql_schema.SchemaItem, **kwargs: t.Any
51+
) -> None:
52+
super().__init__(name, *args, **kwargs) # type: ignore[arg-type]
53+
54+
def __new__(
55+
cls, *args: t.Any, __database__: t.Optional[str] = None, **kwargs: t.Any
56+
) -> "Table":
57+
# If a metadata arg is passed, go directly to the base Table. Also do
58+
# this for no args so the correct error is shown.
59+
if not args or (len(args) >= 2 and isinstance(args[1], sa.MetaData)):
60+
return super().__new__(cls, *args, **kwargs)
61+
62+
metadata = make_metadata(__database__ or DEFAULT_KEY)
63+
return super().__new__(cls, *[args[0], metadata, *args[1:]], **kwargs)
Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
# from .file import FileField
1+
from .file import FileField, FileFieldBase, FileObject
22
from .guid import GUID
3+
from .image import CroppingDetails, ImageFileField
34
from .ipaddress import GenericIP
45

5-
# from .image import CroppingDetails, ImageFileField
6-
76
__all__ = [
87
"GUID",
98
"GenericIP",
10-
# "CroppingDetails",
11-
# "FileField",
12-
# "ImageFileField",
9+
"CroppingDetails",
10+
"FileField",
11+
"ImageFileField",
12+
"FileObject",
13+
"FileFieldBase",
1314
]

ellar_sqlalchemy/model/typeDecorator/exceptions.py.ellar renamed to ellar_sqlalchemy/model/typeDecorator/exceptions.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
class ContentTypeValidationError(Exception):
2-
def __init__(self, content_type=None, valid_content_types=None):
1+
import typing as t
2+
33

4+
class ContentTypeValidationError(Exception):
5+
def __init__(
6+
self,
7+
content_type: t.Optional[str] = None,
8+
valid_content_types: t.Optional[t.List[str]] = None,
9+
) -> None:
410
if content_type is None:
511
message = "Content type is not provided. "
612
else:
@@ -21,5 +27,5 @@ class InvalidImageOperationError(Exception):
2127

2228

2329
class MaximumAllowedFileLengthError(Exception):
24-
def __init__(self, max_length: int):
30+
def __init__(self, max_length: int) -> None:
2531
super().__init__("Cannot store files larger than: %d bytes" % max_length)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy