Skip to content

Commit 7ab9812

Browse files
authored
Merge pull request #6 from python-ellar/table_image_file
Added table, image and file fields and some code refactoring
2 parents aeaacc1 + 5f9d653 commit 7ab9812

Some content is hidden

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

44 files changed

+481
-832
lines changed

README.md

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

1111
## Project Status
1212
- 70% done
13-
- SQLAlchemy Table support with ModelSession
14-
- Migration custom revision directives
15-
- Documentation
16-
- File Field
17-
- Image Field
1813
- Tests
14+
- Documentation
1915

2016
## Introduction
2117
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)