Skip to content

Commit bc04491

Browse files
committed
perf: sqlmodelx is not used by default
1 parent 2a31fb0 commit bc04491

5 files changed

Lines changed: 127 additions & 7 deletions

File tree

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
from sqlmodelx import SQLModel
2-
from sqlmodelx.enums import Choices, IntegerChoices, TextChoices
3-
41
from .fields import Field
2+
3+
try:
4+
from sqlmodelx import SQLModel
5+
except ImportError:
6+
from sqlmodel import SQLModel
7+
8+
try:
9+
from sqlmodelx.enums import Choices, IntegerChoices, TextChoices
10+
from sqlmodelx.sqltypes import ChoiceType
11+
except ImportError:
12+
from ._enums import Choices, IntegerChoices, TextChoices
13+
from ._sqltypes import ChoiceType
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import enum
2+
3+
__all__ = ["Choices", "IntegerChoices", "TextChoices"]
4+
5+
6+
class ChoicesMeta(enum.EnumMeta):
7+
"""A metaclass for creating a enum choices."""
8+
9+
def __new__(metacls, classname, bases, classdict, **kwds):
10+
labels = []
11+
for key in classdict._member_names:
12+
value = classdict[key]
13+
if isinstance(value, (list, tuple)) and len(value) > 1 and isinstance(value[-1], str):
14+
*value, label = value
15+
value = tuple(value)
16+
else:
17+
label = key.replace("_", " ").title()
18+
labels.append(label)
19+
# Use dict.__setitem__() to suppress defenses against double
20+
# assignment in enum's classdict.
21+
dict.__setitem__(classdict, key, value)
22+
cls = super().__new__(metacls, classname, bases, classdict, **kwds)
23+
cls._value2label_map_ = dict(zip(cls._value2member_map_, labels))
24+
# Add a label property to instances of enum which uses the enum member
25+
# that is passed in as "self" as the value to use when looking up the
26+
# label in the choices.
27+
cls.label = property(lambda self: cls._value2label_map_.get(self.value))
28+
cls.do_not_call_in_templates = True
29+
return enum.unique(cls)
30+
31+
def __contains__(cls, member):
32+
if not isinstance(member, enum.Enum):
33+
# Allow non-enums to match against member values.
34+
return any(x.value == member for x in cls)
35+
return super().__contains__(member)
36+
37+
@property
38+
def names(cls):
39+
empty = ["__empty__"] if hasattr(cls, "__empty__") else []
40+
return empty + [member.name for member in cls]
41+
42+
@property
43+
def choices(cls):
44+
empty = [(None, cls.__empty__)] if hasattr(cls, "__empty__") else []
45+
return empty + [(member.value, member.label) for member in cls]
46+
47+
@property
48+
def labels(cls):
49+
return [label for _, label in cls.choices]
50+
51+
@property
52+
def values(cls):
53+
return [value for value, _ in cls.choices]
54+
55+
56+
class Choices(enum.Enum, metaclass=ChoicesMeta):
57+
"""Class for creating enumerated choices."""
58+
59+
def __str__(self):
60+
"""
61+
Use value when cast to str, so that Choices set as model instance
62+
attributes are rendered as expected in templates and similar contexts.
63+
"""
64+
return str(self.value)
65+
66+
67+
class IntegerChoices(enum.IntEnum, Choices):
68+
"""Class for creating enumerated integer choices."""
69+
70+
pass
71+
72+
73+
class TextChoices(str, Choices):
74+
"""Class for creating enumerated string choices."""
75+
76+
def _generate_next_value_(name, start, count, last_values):
77+
return name
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from typing import Type
2+
3+
from sqlalchemy import types
4+
from sqlalchemy.engine import Dialect
5+
6+
from ._enums import Choices
7+
8+
9+
class ChoiceType(types.TypeDecorator):
10+
impl = types.CHAR
11+
cache_ok = True
12+
13+
def __init__(self, choices: Type[Choices], impl=None, *args, **kwargs):
14+
super().__init__(*args, **kwargs)
15+
self.choices = choices
16+
17+
@property
18+
def python_type(self):
19+
return self.impl.python_type
20+
21+
def load_dialect_impl(self, dialect: Dialect):
22+
return dialect.type_descriptor(types.CHAR(20))
23+
24+
def process_bind_param(self, value, dialect):
25+
if value and isinstance(value, Choices):
26+
return value.value
27+
return value
28+
29+
def process_result_value(self, value, dialect):
30+
if value is not None:
31+
if issubclass(self.choices, int):
32+
value = int(value)
33+
return self.choices(value)
34+
return value

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ classifiers = [
3737
]
3838
dependencies = [
3939
"fastapi>=0.103.2",
40-
"sqlmodelx>=0.0.7,<0.1.0",
40+
"sqlmodel>=0.0.14,<0.1.0",
4141
"python-multipart>=0.0.5",
4242
"sqlalchemy-database>=0.1.1,<0.2.0",
4343
"aiofiles>=0.17.0",

tests/models/sqlm.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
from typing import List, Optional
33

44
from sqlalchemy import JSON, Column, String, Text
5-
from sqlmodel import Field, Relationship
5+
from sqlmodel import Field, Relationship, SQLModel
66

7-
from fastapi_amis_admin.models import SQLModel
7+
from fastapi_amis_admin.models import ChoiceType
88
from tests.models.schemas import ArticleStatusChoices
99

1010
Base = SQLModel
@@ -63,7 +63,7 @@ class ArticleContent(PkModelMixin, table=True):
6363
class Article(PkModelMixin, CreateTimeModelMixin, table=True):
6464
title: str = Field(title="ArticleTitle", max_length=200)
6565
description: str = Field(default="", title="ArticleDescription", sa_column=Column(Text))
66-
status: ArticleStatusChoices = Field(ArticleStatusChoices.PENDING, title="status")
66+
status: ArticleStatusChoices = Field(ArticleStatusChoices.PENDING, title="status", sa_type=ChoiceType(ArticleStatusChoices))
6767

6868
category_id: Optional[int] = Field(default=None, foreign_key="category.id", title="CategoryId")
6969
category: Optional[Category] = Relationship(back_populates="articles")

0 commit comments

Comments
 (0)