artemis-model 0.1.66__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.
- artemis_model-0.1.66/PKG-INFO +113 -0
- artemis_model-0.1.66/README.md +96 -0
- artemis_model-0.1.66/artemis_model/__init__.py +17 -0
- artemis_model-0.1.66/artemis_model/album.py +73 -0
- artemis_model-0.1.66/artemis_model/artist.py +51 -0
- artemis_model-0.1.66/artemis_model/auth.py +119 -0
- artemis_model-0.1.66/artemis_model/base.py +222 -0
- artemis_model-0.1.66/artemis_model/category.py +60 -0
- artemis_model-0.1.66/artemis_model/dj_set.py +120 -0
- artemis_model-0.1.66/artemis_model/genre.py +52 -0
- artemis_model-0.1.66/artemis_model/location.py +154 -0
- artemis_model-0.1.66/artemis_model/message.py +97 -0
- artemis_model-0.1.66/artemis_model/organization.py +78 -0
- artemis_model-0.1.66/artemis_model/otp.py +34 -0
- artemis_model-0.1.66/artemis_model/permission.py +53 -0
- artemis_model-0.1.66/artemis_model/playlist.py +191 -0
- artemis_model-0.1.66/artemis_model/schedule.py +35 -0
- artemis_model-0.1.66/artemis_model/setting.py +45 -0
- artemis_model-0.1.66/artemis_model/track.py +123 -0
- artemis_model-0.1.66/artemis_model/user.py +73 -0
- artemis_model-0.1.66/artemis_model/zone.py +69 -0
- artemis_model-0.1.66/pyproject.toml +98 -0
@@ -0,0 +1,113 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: artemis-model
|
3
|
+
Version: 0.1.66
|
4
|
+
Summary:
|
5
|
+
Author: Jukeboxy
|
6
|
+
Requires-Python: >=3.10.6,<4.0.0
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
10
|
+
Requires-Dist: alembic (>=1.13.1,<2.0.0)
|
11
|
+
Requires-Dist: asyncpg (>=0.29.0,<0.30.0)
|
12
|
+
Requires-Dist: greenlet (>=3.0.2,<4.0.0)
|
13
|
+
Requires-Dist: python-slugify (>=8.0.4,<9.0.0)
|
14
|
+
Requires-Dist: sqlalchemy (>=2.0.23,<3.0.0)
|
15
|
+
Description-Content-Type: text/markdown
|
16
|
+
|
17
|
+
# artemis-model
|
18
|
+
|
19
|
+
Welcome to `artemis-model`, the backbone repository that contains all the essential models used in both the `artemis-api` and the `prophet` project. This project includes asynchronous models used by `artemis-api`, such as `Artist`, and synchronous models like `ArtistSync` for other implementations.
|
20
|
+
|
21
|
+
## Getting Started
|
22
|
+
|
23
|
+
To set up your development environment for `artemis-model`, follow these initial setup steps:
|
24
|
+
|
25
|
+
1. **Environment Setup**
|
26
|
+
|
27
|
+
```shell
|
28
|
+
cp ./alembic/.env.example ./alembic/.env
|
29
|
+
```
|
30
|
+
|
31
|
+
After copying the example environment file, make sure to fill in the `.env` file with your specific configurations.
|
32
|
+
|
33
|
+
2. **Install Dependencies**
|
34
|
+
|
35
|
+
```shell
|
36
|
+
poetry install --all-extras
|
37
|
+
```
|
38
|
+
|
39
|
+
This will install all necessary dependencies to get you started with `artemis-models` and `alembic`.
|
40
|
+
|
41
|
+
## Creating a New Model
|
42
|
+
|
43
|
+
To introduce a new model in the project, you should start by creating a mixin and then define two different model classes that inherit from this mixin.
|
44
|
+
|
45
|
+
### Example: Adding a `LoginHistory` Model
|
46
|
+
|
47
|
+
1. **Define the Mixin**
|
48
|
+
This mixin will include all the common attributes of your model.
|
49
|
+
|
50
|
+
```python
|
51
|
+
class LoginHistoryMixin:
|
52
|
+
"""
|
53
|
+
Stores the login history of users.
|
54
|
+
"""
|
55
|
+
|
56
|
+
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True, nullable=False)
|
57
|
+
account_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("user_account.id"), nullable=False, index=True)
|
58
|
+
ip_address: Mapped[str] = mapped_column(nullable=True)
|
59
|
+
created_at = mapped_column(DateTime, default=datetime.utcnow)
|
60
|
+
|
61
|
+
@declared_attr
|
62
|
+
def account(cls) -> Mapped["UserAccount"]:
|
63
|
+
return relationship("UserAccount", back_populates="login_histories")
|
64
|
+
```
|
65
|
+
|
66
|
+
2. **Inherit from Base Classes**
|
67
|
+
Create two classes that inherit from CustomBase and CustomSyncBase respectively, using the mixin for shared attributes.
|
68
|
+
|
69
|
+
```python
|
70
|
+
class LoginHistorySync(CustomSyncBase, LoginHistoryMixin):
|
71
|
+
pass
|
72
|
+
|
73
|
+
class LoginHistory(CustomBase, LoginHistoryMixin):
|
74
|
+
pass
|
75
|
+
```
|
76
|
+
|
77
|
+
## Version Management and Builds
|
78
|
+
|
79
|
+
1. **Update the Project Version**
|
80
|
+
|
81
|
+
Open pyproject.toml and increment the minor version number.
|
82
|
+
|
83
|
+
2. **Build the Project**
|
84
|
+
|
85
|
+
```shell
|
86
|
+
poetry build
|
87
|
+
```
|
88
|
+
|
89
|
+
3. **Update Dependency in artemis-api/prophet**
|
90
|
+
|
91
|
+
If the build succeeds, remember to also bump the version number in the pyproject.toml of artemis-api and prophet to match the new version of artemis-model.
|
92
|
+
|
93
|
+
## Using Alembic for Model Changes
|
94
|
+
|
95
|
+
If modifications are necessary for any model:
|
96
|
+
|
97
|
+
1. **Modify the Model**
|
98
|
+
2. **Create an Alembic Revision**
|
99
|
+
|
100
|
+
```shell
|
101
|
+
alembic revision --autogenerate -m "Description of changes"
|
102
|
+
```
|
103
|
+
|
104
|
+
3. **Upgrade Database Schema**
|
105
|
+
|
106
|
+
```shell
|
107
|
+
alembic upgrade head
|
108
|
+
```
|
109
|
+
|
110
|
+
Ensure that the new Alembic script in the versions directory is committed to your git repository.
|
111
|
+
Repeat the build and version update steps as necessary after making changes.
|
112
|
+
|
113
|
+
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# artemis-model
|
2
|
+
|
3
|
+
Welcome to `artemis-model`, the backbone repository that contains all the essential models used in both the `artemis-api` and the `prophet` project. This project includes asynchronous models used by `artemis-api`, such as `Artist`, and synchronous models like `ArtistSync` for other implementations.
|
4
|
+
|
5
|
+
## Getting Started
|
6
|
+
|
7
|
+
To set up your development environment for `artemis-model`, follow these initial setup steps:
|
8
|
+
|
9
|
+
1. **Environment Setup**
|
10
|
+
|
11
|
+
```shell
|
12
|
+
cp ./alembic/.env.example ./alembic/.env
|
13
|
+
```
|
14
|
+
|
15
|
+
After copying the example environment file, make sure to fill in the `.env` file with your specific configurations.
|
16
|
+
|
17
|
+
2. **Install Dependencies**
|
18
|
+
|
19
|
+
```shell
|
20
|
+
poetry install --all-extras
|
21
|
+
```
|
22
|
+
|
23
|
+
This will install all necessary dependencies to get you started with `artemis-models` and `alembic`.
|
24
|
+
|
25
|
+
## Creating a New Model
|
26
|
+
|
27
|
+
To introduce a new model in the project, you should start by creating a mixin and then define two different model classes that inherit from this mixin.
|
28
|
+
|
29
|
+
### Example: Adding a `LoginHistory` Model
|
30
|
+
|
31
|
+
1. **Define the Mixin**
|
32
|
+
This mixin will include all the common attributes of your model.
|
33
|
+
|
34
|
+
```python
|
35
|
+
class LoginHistoryMixin:
|
36
|
+
"""
|
37
|
+
Stores the login history of users.
|
38
|
+
"""
|
39
|
+
|
40
|
+
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True, nullable=False)
|
41
|
+
account_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("user_account.id"), nullable=False, index=True)
|
42
|
+
ip_address: Mapped[str] = mapped_column(nullable=True)
|
43
|
+
created_at = mapped_column(DateTime, default=datetime.utcnow)
|
44
|
+
|
45
|
+
@declared_attr
|
46
|
+
def account(cls) -> Mapped["UserAccount"]:
|
47
|
+
return relationship("UserAccount", back_populates="login_histories")
|
48
|
+
```
|
49
|
+
|
50
|
+
2. **Inherit from Base Classes**
|
51
|
+
Create two classes that inherit from CustomBase and CustomSyncBase respectively, using the mixin for shared attributes.
|
52
|
+
|
53
|
+
```python
|
54
|
+
class LoginHistorySync(CustomSyncBase, LoginHistoryMixin):
|
55
|
+
pass
|
56
|
+
|
57
|
+
class LoginHistory(CustomBase, LoginHistoryMixin):
|
58
|
+
pass
|
59
|
+
```
|
60
|
+
|
61
|
+
## Version Management and Builds
|
62
|
+
|
63
|
+
1. **Update the Project Version**
|
64
|
+
|
65
|
+
Open pyproject.toml and increment the minor version number.
|
66
|
+
|
67
|
+
2. **Build the Project**
|
68
|
+
|
69
|
+
```shell
|
70
|
+
poetry build
|
71
|
+
```
|
72
|
+
|
73
|
+
3. **Update Dependency in artemis-api/prophet**
|
74
|
+
|
75
|
+
If the build succeeds, remember to also bump the version number in the pyproject.toml of artemis-api and prophet to match the new version of artemis-model.
|
76
|
+
|
77
|
+
## Using Alembic for Model Changes
|
78
|
+
|
79
|
+
If modifications are necessary for any model:
|
80
|
+
|
81
|
+
1. **Modify the Model**
|
82
|
+
2. **Create an Alembic Revision**
|
83
|
+
|
84
|
+
```shell
|
85
|
+
alembic revision --autogenerate -m "Description of changes"
|
86
|
+
```
|
87
|
+
|
88
|
+
3. **Upgrade Database Schema**
|
89
|
+
|
90
|
+
```shell
|
91
|
+
alembic upgrade head
|
92
|
+
```
|
93
|
+
|
94
|
+
Ensure that the new Alembic script in the versions directory is committed to your git repository.
|
95
|
+
Repeat the build and version update steps as necessary after making changes.
|
96
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from artemis_model.album import * # noqa
|
2
|
+
from artemis_model.artist import * # noqa
|
3
|
+
from artemis_model.auth import * # noqa
|
4
|
+
from artemis_model.category import * # noqa
|
5
|
+
from artemis_model.dj_set import * # noqa
|
6
|
+
from artemis_model.genre import * # noqa
|
7
|
+
from artemis_model.location import * # noqa
|
8
|
+
from artemis_model.message import * # noqa
|
9
|
+
from artemis_model.organization import * # noqa
|
10
|
+
from artemis_model.playlist import * # noqa
|
11
|
+
from artemis_model.schedule import * # noqa
|
12
|
+
from artemis_model.setting import * # noqa
|
13
|
+
from artemis_model.track import * # noqa
|
14
|
+
from artemis_model.user import * # noqa
|
15
|
+
from artemis_model.zone import * # noqa
|
16
|
+
from artemis_model.otp import * # noqa
|
17
|
+
|
@@ -0,0 +1,73 @@
|
|
1
|
+
"""Album models"""
|
2
|
+
|
3
|
+
from datetime import datetime
|
4
|
+
|
5
|
+
from sqlalchemy import Computed, ForeignKey, func, literal, text
|
6
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
7
|
+
|
8
|
+
from artemis_model.base import CustomSyncBase, CustomBase, TSVector, TimeStampMixin
|
9
|
+
|
10
|
+
from sqlalchemy.ext.declarative import declared_attr
|
11
|
+
|
12
|
+
|
13
|
+
def to_tsvector_ix(*columns):
|
14
|
+
s = " || ' ' || ".join(columns)
|
15
|
+
return func.to_tsvector(literal("english"), text(s))
|
16
|
+
|
17
|
+
|
18
|
+
class AlbumMixin(TimeStampMixin):
|
19
|
+
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
20
|
+
name: Mapped[str] = mapped_column(nullable=False)
|
21
|
+
description: Mapped[str | None] = mapped_column(nullable=True)
|
22
|
+
entry_date: Mapped[datetime] = mapped_column(
|
23
|
+
nullable=False, default=datetime.utcnow
|
24
|
+
)
|
25
|
+
disabled: Mapped[bool] = mapped_column(nullable=False, default=False)
|
26
|
+
legacy_id: Mapped[str] = mapped_column(nullable=False)
|
27
|
+
is_internal: Mapped[bool] = mapped_column(nullable=False, default=False)
|
28
|
+
|
29
|
+
name_tsv = mapped_column(
|
30
|
+
TSVector(),
|
31
|
+
Computed("to_tsvector('english', name)", persisted=True),
|
32
|
+
)
|
33
|
+
|
34
|
+
# __table_args__ = (
|
35
|
+
# Index("fts_ix_album_name_tsv", to_tsvector_ix("name"), postgresql_using="gin"),
|
36
|
+
# )
|
37
|
+
|
38
|
+
@declared_attr
|
39
|
+
def artists(cls) -> Mapped[list["Artist"]]:
|
40
|
+
return relationship(secondary="album_artist_assoc", back_populates="albums")
|
41
|
+
|
42
|
+
@declared_attr
|
43
|
+
def tracks(cls) -> Mapped[list["Track"]]:
|
44
|
+
return relationship(
|
45
|
+
"Track",
|
46
|
+
back_populates="album",
|
47
|
+
cascade="all, delete-orphan",
|
48
|
+
)
|
49
|
+
|
50
|
+
|
51
|
+
class AlbumSync(CustomSyncBase, AlbumMixin):
|
52
|
+
pass
|
53
|
+
|
54
|
+
|
55
|
+
class Album(CustomBase, AlbumMixin):
|
56
|
+
pass
|
57
|
+
|
58
|
+
|
59
|
+
class AlbumArtistAssocMixin():
|
60
|
+
album_id: Mapped[int] = mapped_column(
|
61
|
+
ForeignKey("album.id"), primary_key=True, nullable=False
|
62
|
+
)
|
63
|
+
artist_id: Mapped[int] = mapped_column(
|
64
|
+
ForeignKey("artist.id"), primary_key=True, nullable=False
|
65
|
+
)
|
66
|
+
|
67
|
+
|
68
|
+
class AlbumArtistAssocSync(CustomSyncBase, AlbumArtistAssocMixin):
|
69
|
+
pass
|
70
|
+
|
71
|
+
|
72
|
+
class AlbumArtistAssoc(CustomBase, AlbumArtistAssocMixin):
|
73
|
+
pass
|
@@ -0,0 +1,51 @@
|
|
1
|
+
"""Artist models"""
|
2
|
+
|
3
|
+
from datetime import datetime
|
4
|
+
|
5
|
+
from sqlalchemy import Computed, func, literal, text
|
6
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
7
|
+
|
8
|
+
from artemis_model.base import CustomBase, CustomSyncBase, TSVector, TimeStampMixin
|
9
|
+
|
10
|
+
from sqlalchemy.ext.declarative import declared_attr
|
11
|
+
|
12
|
+
|
13
|
+
def to_tsvector_ix(*columns):
|
14
|
+
s = " || ' ' || ".join(columns)
|
15
|
+
return func.to_tsvector(literal("english"), text(s))
|
16
|
+
|
17
|
+
|
18
|
+
class ArtistMixin(TimeStampMixin):
|
19
|
+
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
20
|
+
name: Mapped[str] = mapped_column(nullable=False)
|
21
|
+
entry_date: Mapped[datetime] = mapped_column(
|
22
|
+
nullable=False, default=datetime.utcnow
|
23
|
+
)
|
24
|
+
disabled: Mapped[bool] = mapped_column(nullable=False, default=False)
|
25
|
+
is_internal: Mapped[bool] = mapped_column(nullable=False, default=False)
|
26
|
+
legacy_id: Mapped[str] = mapped_column(nullable=False)
|
27
|
+
|
28
|
+
name_tsv = mapped_column(
|
29
|
+
TSVector(),
|
30
|
+
Computed("to_tsvector('english', name)", persisted=True),
|
31
|
+
)
|
32
|
+
|
33
|
+
# __table_args__ = (
|
34
|
+
# Index("fts_ix_artist_name_tsv", to_tsvector_ix("name"), postgresql_using="gin"),
|
35
|
+
# )
|
36
|
+
|
37
|
+
@declared_attr
|
38
|
+
def albums(cls) -> Mapped[list["Album"]]:
|
39
|
+
return relationship(secondary="album_artist_assoc", back_populates="artists")
|
40
|
+
|
41
|
+
@declared_attr
|
42
|
+
def tracks(cls) -> Mapped[list["Track"]]:
|
43
|
+
return relationship("Track", back_populates="artist", cascade="all, delete-orphan")
|
44
|
+
|
45
|
+
|
46
|
+
class ArtistSync(CustomSyncBase, ArtistMixin):
|
47
|
+
pass
|
48
|
+
|
49
|
+
|
50
|
+
class Artist(CustomBase, ArtistMixin):
|
51
|
+
pass
|
@@ -0,0 +1,119 @@
|
|
1
|
+
"""Auth models."""
|
2
|
+
import uuid
|
3
|
+
from datetime import datetime
|
4
|
+
from typing import Optional
|
5
|
+
|
6
|
+
from sqlalchemy import UUID, DateTime, ForeignKey, LargeBinary
|
7
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
8
|
+
from sqlalchemy.ext.declarative import declared_attr
|
9
|
+
|
10
|
+
from artemis_model.base import CustomSyncBase, TimeStampMixin, CustomBase
|
11
|
+
|
12
|
+
|
13
|
+
class UserUnverifiedAccountMixin(TimeStampMixin):
|
14
|
+
"""
|
15
|
+
This table is used to store the account info of users who have requested an account but have not yet
|
16
|
+
been verified. Once the user has been verified, the account will be moved to the UserAccount table.
|
17
|
+
"""
|
18
|
+
|
19
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
20
|
+
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
21
|
+
)
|
22
|
+
email: Mapped[str] = mapped_column(index=True)
|
23
|
+
name: Mapped[str] = mapped_column(nullable=False)
|
24
|
+
mobile: Mapped[str] = mapped_column(nullable=True, index=True)
|
25
|
+
is_root_user: Mapped[bool] = mapped_column(default=True)
|
26
|
+
is_email_verified: Mapped[bool] = mapped_column(default=False)
|
27
|
+
is_mobile_verified: Mapped[bool] = mapped_column(default=False)
|
28
|
+
is_onboarded: Mapped[bool] = mapped_column(default=False)
|
29
|
+
password = mapped_column(LargeBinary, nullable=False)
|
30
|
+
provider: Mapped[str] = mapped_column(default="internal")
|
31
|
+
|
32
|
+
|
33
|
+
class UserUnverifiedAccountSync(CustomSyncBase, UserUnverifiedAccountMixin):
|
34
|
+
pass
|
35
|
+
|
36
|
+
|
37
|
+
class UserUnverifiedAccount(CustomBase, UserUnverifiedAccountMixin):
|
38
|
+
pass
|
39
|
+
|
40
|
+
|
41
|
+
class UserAccountMixin(TimeStampMixin):
|
42
|
+
"""
|
43
|
+
This table is used to store the account info of users who have been verified.
|
44
|
+
"""
|
45
|
+
|
46
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
47
|
+
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
48
|
+
)
|
49
|
+
email: Mapped[str] = mapped_column(index=True, unique=True)
|
50
|
+
name: Mapped[str] = mapped_column(nullable=False)
|
51
|
+
mobile: Mapped[Optional[str]] = mapped_column(
|
52
|
+
nullable=True, unique=True, index=True
|
53
|
+
)
|
54
|
+
password = mapped_column(LargeBinary, nullable=True)
|
55
|
+
provider: Mapped[str] = mapped_column(nullable=False)
|
56
|
+
oauth_id: Mapped[Optional[str]] = mapped_column(nullable=True, index=True)
|
57
|
+
image_url: Mapped[Optional[str]] = mapped_column(nullable=True)
|
58
|
+
is_email_verified: Mapped[bool] = mapped_column(default=False)
|
59
|
+
is_mobile_verified: Mapped[bool] = mapped_column(default=False)
|
60
|
+
is_root_user: Mapped[bool] = mapped_column(default=True)
|
61
|
+
is_super_admin: Mapped[bool] = mapped_column(default=False)
|
62
|
+
disabled: Mapped[bool] = mapped_column(default=False)
|
63
|
+
disabled_reason: Mapped[Optional[str]] = mapped_column(nullable=True)
|
64
|
+
is_onboarded: Mapped[bool] = mapped_column(default=False)
|
65
|
+
|
66
|
+
@declared_attr
|
67
|
+
def login_histories(cls) -> Mapped["LoginHistory"]:
|
68
|
+
return relationship("LoginHistory", back_populates="account")
|
69
|
+
|
70
|
+
@declared_attr
|
71
|
+
def user(cls) -> Mapped["User"]:
|
72
|
+
return relationship("User", back_populates="account")
|
73
|
+
|
74
|
+
|
75
|
+
class UserAccountSync(CustomSyncBase, UserAccountMixin):
|
76
|
+
pass
|
77
|
+
|
78
|
+
|
79
|
+
class UserAccount(CustomBase, UserAccountMixin):
|
80
|
+
pass
|
81
|
+
|
82
|
+
|
83
|
+
class LoginHistoryMixin:
|
84
|
+
"""
|
85
|
+
This table is used to store the login history of users.
|
86
|
+
"""
|
87
|
+
id: Mapped[int] = mapped_column(
|
88
|
+
primary_key=True, autoincrement=True, nullable=False
|
89
|
+
)
|
90
|
+
account_id: Mapped[uuid.UUID] = mapped_column(
|
91
|
+
ForeignKey("user_account.id"), nullable=False, index=True
|
92
|
+
)
|
93
|
+
ip_address: Mapped[str] = mapped_column(nullable=True)
|
94
|
+
created_at = mapped_column(DateTime, default=datetime.utcnow)
|
95
|
+
|
96
|
+
@declared_attr
|
97
|
+
def account(cls) -> Mapped["UserAccount"]:
|
98
|
+
return relationship("UserAccount", back_populates="login_histories")
|
99
|
+
|
100
|
+
|
101
|
+
class LoginHistorySync(CustomSyncBase, LoginHistoryMixin):
|
102
|
+
pass
|
103
|
+
|
104
|
+
|
105
|
+
class LoginHistory(CustomBase, LoginHistoryMixin):
|
106
|
+
pass
|
107
|
+
|
108
|
+
|
109
|
+
class OAuthCsrfStateMixin(TimeStampMixin):
|
110
|
+
id: Mapped[str] = mapped_column(primary_key=True, unique=True)
|
111
|
+
client_base_url: Mapped[str] = mapped_column(nullable=True)
|
112
|
+
|
113
|
+
|
114
|
+
class OAuthCsrfStateSync(CustomSyncBase, OAuthCsrfStateMixin):
|
115
|
+
pass
|
116
|
+
|
117
|
+
|
118
|
+
class OAuthCsrfState(CustomBase, OAuthCsrfStateMixin):
|
119
|
+
pass
|
@@ -0,0 +1,222 @@
|
|
1
|
+
import re
|
2
|
+
from datetime import datetime
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
from sqlalchemy.orm import (DeclarativeBase, Mapped, declared_attr,
|
6
|
+
mapped_column, object_session, relationship)
|
7
|
+
from sqlalchemy.ext.asyncio import AsyncAttrs
|
8
|
+
from sqlalchemy import Column, Uuid, event, inspect, TypeDecorator
|
9
|
+
from sqlalchemy.dialects.postgresql import TSVECTOR
|
10
|
+
|
11
|
+
|
12
|
+
def resolve_table_name(name: str) -> str:
|
13
|
+
"""Resolves table names to their mapped names."""
|
14
|
+
names = re.split("(?=[A-Z])", name) # noqa
|
15
|
+
print(names)
|
16
|
+
return "_".join([x.lower() for x in names if x])
|
17
|
+
|
18
|
+
|
19
|
+
class CustomBase(DeclarativeBase, AsyncAttrs):
|
20
|
+
__repr_attrs__: list[Any] = []
|
21
|
+
__repr_max_length__ = 15
|
22
|
+
|
23
|
+
@declared_attr
|
24
|
+
def __tablename__(self) -> str:
|
25
|
+
return resolve_table_name(self.__name__)
|
26
|
+
|
27
|
+
def dict(self) -> dict:
|
28
|
+
"""Returns a dict representation of a model."""
|
29
|
+
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
30
|
+
|
31
|
+
@property
|
32
|
+
def _id_str(self) -> str:
|
33
|
+
ids = inspect(self).identity
|
34
|
+
if ids:
|
35
|
+
return "-".join([str(x) for x in ids]) if len(ids) > 1 else str(ids[0])
|
36
|
+
else:
|
37
|
+
return "None"
|
38
|
+
|
39
|
+
@property
|
40
|
+
def _repr_attrs_str(self) -> str:
|
41
|
+
max_length = self.__repr_max_length__
|
42
|
+
|
43
|
+
values = []
|
44
|
+
single = len(self.__repr_attrs__) == 1
|
45
|
+
for key in self.__repr_attrs__:
|
46
|
+
if not hasattr(self, key):
|
47
|
+
raise KeyError(
|
48
|
+
"{} has incorrect attribute '{}' in "
|
49
|
+
"__repr__attrs__".format(self.__class__, key)
|
50
|
+
)
|
51
|
+
value = getattr(self, key)
|
52
|
+
wrap_in_quote = isinstance(value, str)
|
53
|
+
|
54
|
+
value = str(value)
|
55
|
+
if len(value) > max_length:
|
56
|
+
value = value[:max_length] + "..."
|
57
|
+
|
58
|
+
if wrap_in_quote:
|
59
|
+
value = "'{}'".format(value)
|
60
|
+
values.append(value if single else "{}:{}".format(key, value))
|
61
|
+
|
62
|
+
return " ".join(values)
|
63
|
+
|
64
|
+
def __repr__(self) -> str:
|
65
|
+
# get id like '#123'
|
66
|
+
id_str = ("#" + self._id_str) if self._id_str else ""
|
67
|
+
# join class name, id and repr_attrs
|
68
|
+
return "<{} {}{}>".format(
|
69
|
+
self.__class__.__name__,
|
70
|
+
id_str,
|
71
|
+
" " + self._repr_attrs_str if self._repr_attrs_str else "",
|
72
|
+
)
|
73
|
+
|
74
|
+
|
75
|
+
class AuditMixin(object):
|
76
|
+
created_by = Column(Uuid, nullable=True)
|
77
|
+
updated_by = Column(Uuid, nullable=True)
|
78
|
+
|
79
|
+
@declared_attr
|
80
|
+
def created_by_user(cls):
|
81
|
+
return relationship(
|
82
|
+
"User",
|
83
|
+
foreign_keys=[cls.created_by],
|
84
|
+
primaryjoin="User.id==%s.created_by" % cls.__name__,
|
85
|
+
)
|
86
|
+
|
87
|
+
@declared_attr
|
88
|
+
def updated_by_user(cls):
|
89
|
+
return relationship(
|
90
|
+
"User",
|
91
|
+
foreign_keys=[cls.updated_by],
|
92
|
+
primaryjoin="User.id==%s.updated_by" % cls.__name__,
|
93
|
+
)
|
94
|
+
|
95
|
+
@staticmethod
|
96
|
+
def _updated_info(mapper: Any, connection: Any, target: Any) -> None:
|
97
|
+
s = object_session(target)
|
98
|
+
target.updated_at = datetime.utcnow()
|
99
|
+
if user_id := getattr(s, "user_id", None):
|
100
|
+
target.updated_by = user_id
|
101
|
+
|
102
|
+
@staticmethod
|
103
|
+
def _created_info(mapper: Any, connection: Any, target: Any) -> None:
|
104
|
+
s = object_session(target)
|
105
|
+
if user_id := getattr(s, "user_id", None):
|
106
|
+
target.created_by = user_id
|
107
|
+
|
108
|
+
@classmethod
|
109
|
+
def get_session_user_id(cls, connection):
|
110
|
+
return connection.info.get("user_id")
|
111
|
+
|
112
|
+
@classmethod
|
113
|
+
def __declare_last__(cls) -> None:
|
114
|
+
event.listen(cls, "before_insert", cls._created_info)
|
115
|
+
event.listen(cls, "before_update", cls._updated_info)
|
116
|
+
|
117
|
+
|
118
|
+
class TimeStampMixin(object):
|
119
|
+
"""Timestamping mixin"""
|
120
|
+
|
121
|
+
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
|
122
|
+
created_at._creation_order = 9998
|
123
|
+
updated_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
|
124
|
+
updated_at._creation_order = 9998
|
125
|
+
|
126
|
+
@staticmethod
|
127
|
+
def _updated_at(mapper, connection, target):
|
128
|
+
target.updated_at = datetime.utcnow()
|
129
|
+
|
130
|
+
@classmethod
|
131
|
+
def __declare_last__(cls):
|
132
|
+
event.listen(cls, "before_update", cls._updated_at)
|
133
|
+
|
134
|
+
|
135
|
+
# class InheritanceMixin(object):
|
136
|
+
# @declared_attr
|
137
|
+
# def __mapper_args__(cls):
|
138
|
+
# return {
|
139
|
+
# 'polymorphic_identity': cls.__name__.lower(),
|
140
|
+
# }
|
141
|
+
|
142
|
+
|
143
|
+
# class CustomBase(Base, AsyncAttrs):
|
144
|
+
# pass
|
145
|
+
|
146
|
+
|
147
|
+
# class SyncInheritanceMixin(object):
|
148
|
+
# type = Column(String(50))
|
149
|
+
|
150
|
+
# @declared_attr
|
151
|
+
# def __mapper_args__(cls):
|
152
|
+
# return {
|
153
|
+
# 'polymorphic_identity': cls.__name__.lower(),
|
154
|
+
# 'polymorphic_on': 'type',
|
155
|
+
# }
|
156
|
+
|
157
|
+
|
158
|
+
class CustomSyncBase(DeclarativeBase):
|
159
|
+
__repr_attrs__: list[Any] = []
|
160
|
+
__repr_max_length__ = 15
|
161
|
+
__abstract__ = True
|
162
|
+
|
163
|
+
@declared_attr
|
164
|
+
def __tablename__(self) -> str:
|
165
|
+
return resolve_table_name(self.__name__)
|
166
|
+
|
167
|
+
""" Is this missed here or not moved intentionally?"""
|
168
|
+
|
169
|
+
# def serializable_dict(self) -> Dict[str, Any]:
|
170
|
+
# d = {col.name: getattr(self, col.name) for col in self.__table__.columns}
|
171
|
+
# return orjson.loads(orjson.dumps(jsonable_encoder(d)))
|
172
|
+
|
173
|
+
def dict(self) -> dict:
|
174
|
+
"""Returns a dict representation of a model."""
|
175
|
+
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
176
|
+
|
177
|
+
@property
|
178
|
+
def _id_str(self) -> str:
|
179
|
+
ids = inspect(self).identity
|
180
|
+
if ids:
|
181
|
+
return "-".join([str(x) for x in ids]) if len(ids) > 1 else str(ids[0])
|
182
|
+
else:
|
183
|
+
return "None"
|
184
|
+
|
185
|
+
@property
|
186
|
+
def _repr_attrs_str(self) -> str:
|
187
|
+
max_length = self.__repr_max_length__
|
188
|
+
|
189
|
+
values = []
|
190
|
+
single = len(self.__repr_attrs__) == 1
|
191
|
+
for key in self.__repr_attrs__:
|
192
|
+
if not hasattr(self, key):
|
193
|
+
raise KeyError(
|
194
|
+
"{} has incorrect attribute '{}' in "
|
195
|
+
"__repr__attrs__".format(self.__class__, key)
|
196
|
+
)
|
197
|
+
value = getattr(self, key)
|
198
|
+
wrap_in_quote = isinstance(value, str)
|
199
|
+
|
200
|
+
value = str(value)
|
201
|
+
if len(value) > max_length:
|
202
|
+
value = value[:max_length] + "..."
|
203
|
+
|
204
|
+
if wrap_in_quote:
|
205
|
+
value = "'{}'".format(value)
|
206
|
+
values.append(value if single else "{}:{}".format(key, value))
|
207
|
+
|
208
|
+
return " ".join(values)
|
209
|
+
|
210
|
+
def __repr__(self) -> str:
|
211
|
+
# get id like '#123'
|
212
|
+
id_str = ("#" + self._id_str) if self._id_str else ""
|
213
|
+
# join class name, id and repr_attrs
|
214
|
+
return "<{} {}{}>".format(
|
215
|
+
self.__class__.__name__,
|
216
|
+
id_str,
|
217
|
+
" " + self._repr_attrs_str if self._repr_attrs_str else "",
|
218
|
+
)
|
219
|
+
|
220
|
+
|
221
|
+
class TSVector(TypeDecorator):
|
222
|
+
impl = TSVECTOR
|