data-syncmaster 0.1.1__py3-none-any.whl
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.
- data_syncmaster-0.1.1.dist-info/LICENSE.txt +203 -0
- data_syncmaster-0.1.1.dist-info/METADATA +115 -0
- data_syncmaster-0.1.1.dist-info/RECORD +110 -0
- data_syncmaster-0.1.1.dist-info/WHEEL +4 -0
- syncmaster/__init__.py +6 -0
- syncmaster/backend/__init__.py +2 -0
- syncmaster/backend/api/__init__.py +2 -0
- syncmaster/backend/api/deps.py +20 -0
- syncmaster/backend/api/monitoring.py +10 -0
- syncmaster/backend/api/router.py +10 -0
- syncmaster/backend/api/v1/__init__.py +2 -0
- syncmaster/backend/api/v1/auth/__init__.py +2 -0
- syncmaster/backend/api/v1/auth/router.py +32 -0
- syncmaster/backend/api/v1/auth/utils.py +26 -0
- syncmaster/backend/api/v1/connections.py +300 -0
- syncmaster/backend/api/v1/groups.py +225 -0
- syncmaster/backend/api/v1/queue.py +148 -0
- syncmaster/backend/api/v1/router.py +18 -0
- syncmaster/backend/api/v1/transfers/__init__.py +2 -0
- syncmaster/backend/api/v1/transfers/router.py +469 -0
- syncmaster/backend/api/v1/transfers/utils.py +17 -0
- syncmaster/backend/api/v1/users.py +75 -0
- syncmaster/backend/export_openapi_schema.py +26 -0
- syncmaster/backend/handler.py +203 -0
- syncmaster/backend/logger.py +2 -0
- syncmaster/backend/main.py +63 -0
- syncmaster/backend/pre_start.py +94 -0
- syncmaster/backend/services/__init__.py +4 -0
- syncmaster/backend/services/auth.py +58 -0
- syncmaster/backend/services/unit_of_work.py +44 -0
- syncmaster/config.py +110 -0
- syncmaster/db/__init__.py +2 -0
- syncmaster/db/alembic.ini +41 -0
- syncmaster/db/base.py +28 -0
- syncmaster/db/factory.py +37 -0
- syncmaster/db/migrations/README +1 -0
- syncmaster/db/migrations/__init__.py +2 -0
- syncmaster/db/migrations/env.py +87 -0
- syncmaster/db/migrations/script.py.mako +24 -0
- syncmaster/db/migrations/versions/2023-11-23_478240cdad4b_init.py +242 -0
- syncmaster/db/migrations/versions/__init__.py +2 -0
- syncmaster/db/mixins.py +33 -0
- syncmaster/db/models.py +194 -0
- syncmaster/db/repositories/__init__.py +22 -0
- syncmaster/db/repositories/base.py +109 -0
- syncmaster/db/repositories/connection.py +138 -0
- syncmaster/db/repositories/credentials_repository.py +87 -0
- syncmaster/db/repositories/group.py +264 -0
- syncmaster/db/repositories/queue.py +195 -0
- syncmaster/db/repositories/repository_with_owner.py +115 -0
- syncmaster/db/repositories/run.py +78 -0
- syncmaster/db/repositories/transfer.py +202 -0
- syncmaster/db/repositories/user.py +72 -0
- syncmaster/db/repositories/utils.py +25 -0
- syncmaster/db/utils.py +31 -0
- syncmaster/dto/__init__.py +2 -0
- syncmaster/dto/connections.py +60 -0
- syncmaster/dto/transfers.py +46 -0
- syncmaster/exceptions/__init__.py +13 -0
- syncmaster/exceptions/base.py +12 -0
- syncmaster/exceptions/connection.py +28 -0
- syncmaster/exceptions/credentials.py +8 -0
- syncmaster/exceptions/group.py +27 -0
- syncmaster/exceptions/queue.py +16 -0
- syncmaster/exceptions/run.py +19 -0
- syncmaster/exceptions/transfer.py +39 -0
- syncmaster/exceptions/user.py +11 -0
- syncmaster/schemas/__init__.py +2 -0
- syncmaster/schemas/v1/__init__.py +54 -0
- syncmaster/schemas/v1/auth.py +12 -0
- syncmaster/schemas/v1/connection_types.py +9 -0
- syncmaster/schemas/v1/connections/__init__.py +2 -0
- syncmaster/schemas/v1/connections/connection.py +146 -0
- syncmaster/schemas/v1/connections/hdfs.py +40 -0
- syncmaster/schemas/v1/connections/hive.py +40 -0
- syncmaster/schemas/v1/connections/oracle.py +58 -0
- syncmaster/schemas/v1/connections/postgres.py +48 -0
- syncmaster/schemas/v1/connections/s3.py +66 -0
- syncmaster/schemas/v1/file_formats.py +7 -0
- syncmaster/schemas/v1/groups.py +39 -0
- syncmaster/schemas/v1/page.py +40 -0
- syncmaster/schemas/v1/queue.py +32 -0
- syncmaster/schemas/v1/status.py +16 -0
- syncmaster/schemas/v1/transfer_types.py +6 -0
- syncmaster/schemas/v1/transfers/__init__.py +172 -0
- syncmaster/schemas/v1/transfers/db.py +23 -0
- syncmaster/schemas/v1/transfers/file/__init__.py +2 -0
- syncmaster/schemas/v1/transfers/file/base.py +47 -0
- syncmaster/schemas/v1/transfers/file/hdfs.py +27 -0
- syncmaster/schemas/v1/transfers/file/s3.py +27 -0
- syncmaster/schemas/v1/transfers/file_format.py +29 -0
- syncmaster/schemas/v1/transfers/run.py +37 -0
- syncmaster/schemas/v1/transfers/strategy.py +15 -0
- syncmaster/schemas/v1/types.py +5 -0
- syncmaster/schemas/v1/users.py +83 -0
- syncmaster/worker/__init__.py +2 -0
- syncmaster/worker/base.py +14 -0
- syncmaster/worker/config.py +18 -0
- syncmaster/worker/controller.py +127 -0
- syncmaster/worker/handlers/__init__.py +2 -0
- syncmaster/worker/handlers/base.py +49 -0
- syncmaster/worker/handlers/file/__init__.py +2 -0
- syncmaster/worker/handlers/file/base.py +56 -0
- syncmaster/worker/handlers/file/hdfs.py +14 -0
- syncmaster/worker/handlers/file/s3.py +20 -0
- syncmaster/worker/handlers/hive.py +41 -0
- syncmaster/worker/handlers/oracle.py +48 -0
- syncmaster/worker/handlers/postgres.py +47 -0
- syncmaster/worker/spark.py +93 -0
- syncmaster/worker/transfer.py +85 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2023-2024 MTS (Mobile Telesystems)
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
from typing import NoReturn
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import ScalarResult, insert, select
|
|
6
|
+
from sqlalchemy.exc import DBAPIError, IntegrityError, NoResultFound
|
|
7
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
8
|
+
from sqlalchemy.orm import selectinload
|
|
9
|
+
|
|
10
|
+
from syncmaster.db.models import Group, GroupMemberRole, Queue, User, UserGroup
|
|
11
|
+
from syncmaster.db.repositories.repository_with_owner import RepositoryWithOwner
|
|
12
|
+
from syncmaster.db.utils import Permission
|
|
13
|
+
from syncmaster.exceptions import EntityNotFoundError, SyncmasterError
|
|
14
|
+
from syncmaster.exceptions.group import GroupNotFoundError
|
|
15
|
+
from syncmaster.exceptions.queue import QueueNotFoundError
|
|
16
|
+
|
|
17
|
+
# TODO: remove HTTP response schemes from repositories, these are different layers
|
|
18
|
+
from syncmaster.schemas.v1.queue import UpdateQueueSchema
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class QueueRepository(RepositoryWithOwner[Queue]):
|
|
22
|
+
def __init__(self, session: AsyncSession):
|
|
23
|
+
super().__init__(model=Queue, session=session)
|
|
24
|
+
|
|
25
|
+
async def create(self, queue_data: dict) -> Queue:
|
|
26
|
+
query = insert(Queue).values(**queue_data).returning(Queue)
|
|
27
|
+
try:
|
|
28
|
+
result: ScalarResult[Queue] = await self._session.scalars(query)
|
|
29
|
+
except IntegrityError as e:
|
|
30
|
+
self._raise_error(e)
|
|
31
|
+
else:
|
|
32
|
+
await self._session.flush()
|
|
33
|
+
return result.one()
|
|
34
|
+
|
|
35
|
+
async def read_by_id(
|
|
36
|
+
self,
|
|
37
|
+
queue_id: int,
|
|
38
|
+
) -> Queue:
|
|
39
|
+
stmt = (
|
|
40
|
+
select(Queue)
|
|
41
|
+
.where(Queue.id == queue_id, Queue.is_deleted.is_(False))
|
|
42
|
+
.options(selectinload(Queue.transfers))
|
|
43
|
+
.options(selectinload(Queue.group))
|
|
44
|
+
)
|
|
45
|
+
try:
|
|
46
|
+
result: ScalarResult[Queue] = await self._session.scalars(stmt)
|
|
47
|
+
return result.one()
|
|
48
|
+
except NoResultFound as e:
|
|
49
|
+
raise QueueNotFoundError from e
|
|
50
|
+
|
|
51
|
+
async def paginate(
|
|
52
|
+
self,
|
|
53
|
+
page: int,
|
|
54
|
+
page_size: int,
|
|
55
|
+
group_id: int,
|
|
56
|
+
):
|
|
57
|
+
stmt = select(Queue).where(
|
|
58
|
+
Queue.is_deleted.is_(False),
|
|
59
|
+
Queue.group_id == group_id,
|
|
60
|
+
)
|
|
61
|
+
return await self._paginate_scalar_result(
|
|
62
|
+
query=stmt.order_by(Queue.id),
|
|
63
|
+
page=page,
|
|
64
|
+
page_size=page_size,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
async def update(
|
|
68
|
+
self,
|
|
69
|
+
queue_id: int,
|
|
70
|
+
queue_data: UpdateQueueSchema,
|
|
71
|
+
) -> Queue:
|
|
72
|
+
try:
|
|
73
|
+
queue = await self.read_by_id(queue_id=queue_id)
|
|
74
|
+
return await self._update(
|
|
75
|
+
Queue.id == queue_id,
|
|
76
|
+
Queue.is_deleted.is_(False),
|
|
77
|
+
description=queue_data.description or queue.description,
|
|
78
|
+
)
|
|
79
|
+
except IntegrityError as e:
|
|
80
|
+
self._raise_error(e)
|
|
81
|
+
|
|
82
|
+
async def delete(
|
|
83
|
+
self,
|
|
84
|
+
queue_id: int,
|
|
85
|
+
) -> None:
|
|
86
|
+
try:
|
|
87
|
+
await self._delete(queue_id)
|
|
88
|
+
except (NoResultFound, EntityNotFoundError) as e:
|
|
89
|
+
raise QueueNotFoundError from e
|
|
90
|
+
|
|
91
|
+
async def get_group_permission(self, user: User, group_id: int) -> Permission:
|
|
92
|
+
"""
|
|
93
|
+
Method for determining CRUD permissions in the specified group
|
|
94
|
+
'DEVELOPER' does not have WRITE permission in the QUEUE repository
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
owner_query = (
|
|
98
|
+
(
|
|
99
|
+
select(Group).where(
|
|
100
|
+
Group.owner_id == user.id,
|
|
101
|
+
Group.id == group_id,
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
.exists()
|
|
105
|
+
.select()
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
is_owner = await self._session.scalar(owner_query)
|
|
109
|
+
|
|
110
|
+
if is_owner:
|
|
111
|
+
return Permission.DELETE
|
|
112
|
+
|
|
113
|
+
group_role_query = select(UserGroup).where(
|
|
114
|
+
UserGroup.group_id == group_id,
|
|
115
|
+
UserGroup.user_id == user.id,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
user_group = await self._session.scalar(group_role_query)
|
|
119
|
+
|
|
120
|
+
if not user_group:
|
|
121
|
+
# Check: group exists
|
|
122
|
+
if not await self._session.get(Group, group_id):
|
|
123
|
+
raise GroupNotFoundError
|
|
124
|
+
|
|
125
|
+
# If the user is not in the group, then he is either a superuser or does not have any rights
|
|
126
|
+
if not user.is_superuser:
|
|
127
|
+
return Permission.NONE
|
|
128
|
+
else:
|
|
129
|
+
return Permission.DELETE
|
|
130
|
+
|
|
131
|
+
group_role = user_group.role
|
|
132
|
+
|
|
133
|
+
if group_role in (GroupMemberRole.Guest, GroupMemberRole.Developer):
|
|
134
|
+
return Permission.READ
|
|
135
|
+
|
|
136
|
+
return Permission.DELETE
|
|
137
|
+
|
|
138
|
+
async def get_resource_permission(self, user: User, resource_id: int) -> Permission:
|
|
139
|
+
"""
|
|
140
|
+
Method for determining CRUD rights in a repository (self.model) for a resource
|
|
141
|
+
'DEVELOPER' does not have WRITE permission in the QUEUE repository
|
|
142
|
+
"""
|
|
143
|
+
is_exists = await self._session.get(self._model, resource_id)
|
|
144
|
+
|
|
145
|
+
if not is_exists:
|
|
146
|
+
return Permission.NONE
|
|
147
|
+
|
|
148
|
+
if user.is_superuser:
|
|
149
|
+
return Permission.DELETE
|
|
150
|
+
|
|
151
|
+
owner_query = (
|
|
152
|
+
(
|
|
153
|
+
select(self._model)
|
|
154
|
+
.join(
|
|
155
|
+
Group,
|
|
156
|
+
Group.id == self._model.group_id,
|
|
157
|
+
)
|
|
158
|
+
.where(
|
|
159
|
+
Group.owner_id == user.id,
|
|
160
|
+
self._model.id == resource_id,
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
.exists()
|
|
164
|
+
.select()
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
is_owner = await self._session.scalar(owner_query)
|
|
168
|
+
|
|
169
|
+
if is_owner:
|
|
170
|
+
return Permission.DELETE
|
|
171
|
+
|
|
172
|
+
group_role_query = (
|
|
173
|
+
select(UserGroup)
|
|
174
|
+
.join(
|
|
175
|
+
self._model,
|
|
176
|
+
UserGroup.group_id == self._model.group_id,
|
|
177
|
+
)
|
|
178
|
+
.where(self._model.id == resource_id, UserGroup.user_id == user.id)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
user_group = await self._session.scalar(group_role_query)
|
|
182
|
+
|
|
183
|
+
if not user_group:
|
|
184
|
+
return Permission.NONE
|
|
185
|
+
|
|
186
|
+
group_role = user_group.role
|
|
187
|
+
|
|
188
|
+
if group_role in (GroupMemberRole.Guest, GroupMemberRole.Developer):
|
|
189
|
+
return Permission.READ
|
|
190
|
+
|
|
191
|
+
return Permission.DELETE
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def _raise_error(err: DBAPIError) -> NoReturn:
|
|
195
|
+
raise SyncmasterError from err
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2023-2024 MTS (Mobile Telesystems)
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
from typing import Generic, TypeVar
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import select
|
|
6
|
+
|
|
7
|
+
from syncmaster.db.models import Base, Group, GroupMemberRole, User, UserGroup
|
|
8
|
+
from syncmaster.db.repositories.base import Repository
|
|
9
|
+
from syncmaster.db.utils import Permission
|
|
10
|
+
from syncmaster.exceptions.group import GroupNotFoundError
|
|
11
|
+
|
|
12
|
+
Model = TypeVar("Model", bound=Base)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RepositoryWithOwner(Repository, Generic[Model]):
|
|
16
|
+
async def get_resource_permission(self, user: User, resource_id: int) -> Permission:
|
|
17
|
+
"""Method for determining CRUD rights in a repository (self.model) for a resource"""
|
|
18
|
+
is_exists = await self._session.get(self._model, resource_id)
|
|
19
|
+
|
|
20
|
+
if not is_exists:
|
|
21
|
+
return Permission.NONE
|
|
22
|
+
|
|
23
|
+
if user.is_superuser:
|
|
24
|
+
return Permission.DELETE
|
|
25
|
+
|
|
26
|
+
owner_query = (
|
|
27
|
+
(
|
|
28
|
+
select(self._model)
|
|
29
|
+
.join(
|
|
30
|
+
Group,
|
|
31
|
+
Group.id == self._model.group_id,
|
|
32
|
+
)
|
|
33
|
+
.where(
|
|
34
|
+
Group.owner_id == user.id,
|
|
35
|
+
self._model.id == resource_id,
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
.exists()
|
|
39
|
+
.select()
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
is_owner = await self._session.scalar(owner_query)
|
|
43
|
+
|
|
44
|
+
if is_owner:
|
|
45
|
+
return Permission.DELETE
|
|
46
|
+
|
|
47
|
+
group_role_query = (
|
|
48
|
+
select(UserGroup)
|
|
49
|
+
.join(
|
|
50
|
+
self._model,
|
|
51
|
+
UserGroup.group_id == self._model.group_id,
|
|
52
|
+
)
|
|
53
|
+
.where(self._model.id == resource_id, UserGroup.user_id == user.id)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
user_group = await self._session.scalar(group_role_query)
|
|
57
|
+
|
|
58
|
+
if not user_group:
|
|
59
|
+
return Permission.NONE
|
|
60
|
+
|
|
61
|
+
group_role = user_group.role
|
|
62
|
+
|
|
63
|
+
if group_role == GroupMemberRole.Guest:
|
|
64
|
+
return Permission.READ
|
|
65
|
+
|
|
66
|
+
if group_role == GroupMemberRole.Developer:
|
|
67
|
+
return Permission.WRITE
|
|
68
|
+
|
|
69
|
+
return Permission.DELETE # Maintainer
|
|
70
|
+
|
|
71
|
+
async def get_group_permission(self, user: User, group_id: int) -> Permission:
|
|
72
|
+
"""Method for determining CRUD permissions in the specified group"""
|
|
73
|
+
owner_query = (
|
|
74
|
+
(
|
|
75
|
+
select(Group).where(
|
|
76
|
+
Group.owner_id == user.id,
|
|
77
|
+
Group.id == group_id,
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
.exists()
|
|
81
|
+
.select()
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
is_owner = await self._session.scalar(owner_query)
|
|
85
|
+
|
|
86
|
+
if is_owner:
|
|
87
|
+
return Permission.DELETE
|
|
88
|
+
|
|
89
|
+
group_role_query = select(UserGroup).where(
|
|
90
|
+
UserGroup.group_id == group_id,
|
|
91
|
+
UserGroup.user_id == user.id,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
user_group = await self._session.scalar(group_role_query)
|
|
95
|
+
|
|
96
|
+
if not user_group:
|
|
97
|
+
# Check: group exists
|
|
98
|
+
if not await self._session.get(Group, group_id):
|
|
99
|
+
raise GroupNotFoundError
|
|
100
|
+
|
|
101
|
+
# If the user is not in the group, then he is either a superuser or does not have any rights
|
|
102
|
+
if not user.is_superuser:
|
|
103
|
+
return Permission.NONE
|
|
104
|
+
else:
|
|
105
|
+
return Permission.DELETE
|
|
106
|
+
|
|
107
|
+
group_role = user_group.role
|
|
108
|
+
|
|
109
|
+
if group_role == GroupMemberRole.Guest:
|
|
110
|
+
return Permission.READ
|
|
111
|
+
|
|
112
|
+
if group_role == GroupMemberRole.Developer:
|
|
113
|
+
return Permission.WRITE
|
|
114
|
+
|
|
115
|
+
return Permission.DELETE # Maintainer
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2023-2024 MTS (Mobile Telesystems)
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
from typing import Any, NoReturn
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import desc, select
|
|
6
|
+
from sqlalchemy.exc import DBAPIError, IntegrityError
|
|
7
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
8
|
+
from sqlalchemy.orm import selectinload
|
|
9
|
+
|
|
10
|
+
from syncmaster.db.models import Run, Status, Transfer
|
|
11
|
+
from syncmaster.db.repositories.base import Repository
|
|
12
|
+
from syncmaster.db.utils import Pagination
|
|
13
|
+
from syncmaster.exceptions import SyncmasterError
|
|
14
|
+
from syncmaster.exceptions.run import CannotStopRunError, RunNotFoundError
|
|
15
|
+
from syncmaster.exceptions.transfer import TransferNotFoundError
|
|
16
|
+
from syncmaster.schemas.v1.transfers import ReadFullTransferSchema
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RunRepository(Repository[Run]):
|
|
20
|
+
def __init__(self, session: AsyncSession) -> None:
|
|
21
|
+
super().__init__(model=Run, session=session)
|
|
22
|
+
|
|
23
|
+
async def paginate(
|
|
24
|
+
self,
|
|
25
|
+
transfer_id: int,
|
|
26
|
+
page: int,
|
|
27
|
+
page_size: int,
|
|
28
|
+
) -> Pagination:
|
|
29
|
+
query = select(Run).where(Run.transfer_id == transfer_id).order_by(desc(Run.created_at))
|
|
30
|
+
return await self._paginate_scalar_result(query=query, page=page, page_size=page_size)
|
|
31
|
+
|
|
32
|
+
async def read_by_id(self, run_id: int) -> Run:
|
|
33
|
+
run = await self._session.get(Run, run_id)
|
|
34
|
+
if run is None:
|
|
35
|
+
raise RunNotFoundError
|
|
36
|
+
return run
|
|
37
|
+
|
|
38
|
+
async def create(self, transfer_id: int) -> Run:
|
|
39
|
+
run = Run()
|
|
40
|
+
run.transfer_id = transfer_id
|
|
41
|
+
run.transfer_dump = await self.read_full_serialized_transfer(transfer_id)
|
|
42
|
+
try:
|
|
43
|
+
self._session.add(run)
|
|
44
|
+
await self._session.flush()
|
|
45
|
+
return run
|
|
46
|
+
except IntegrityError as e:
|
|
47
|
+
self._raise_error(e)
|
|
48
|
+
|
|
49
|
+
async def update(self, run_id: int, **kwargs: Any) -> Run:
|
|
50
|
+
try:
|
|
51
|
+
return await self._update(Run.id == run_id, **kwargs)
|
|
52
|
+
except IntegrityError as e:
|
|
53
|
+
self._raise_error(e)
|
|
54
|
+
|
|
55
|
+
async def stop(self, run_id: int) -> Run:
|
|
56
|
+
run = await self.read_by_id(run_id=run_id)
|
|
57
|
+
if run.status not in [Status.CREATED, Status.STARTED]:
|
|
58
|
+
raise CannotStopRunError(run_id=run_id, current_status=run.status)
|
|
59
|
+
run.status = Status.SEND_STOP_SIGNAL
|
|
60
|
+
await self._session.flush()
|
|
61
|
+
return run
|
|
62
|
+
|
|
63
|
+
async def read_full_serialized_transfer(self, transfer_id: int) -> dict[str, Any]:
|
|
64
|
+
transfer = await self._session.scalars(
|
|
65
|
+
select(Transfer)
|
|
66
|
+
.where(Transfer.id == transfer_id)
|
|
67
|
+
.options(
|
|
68
|
+
selectinload(Transfer.source_connection),
|
|
69
|
+
selectinload(Transfer.target_connection),
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
return ReadFullTransferSchema.from_orm(transfer.one()).dict()
|
|
73
|
+
|
|
74
|
+
def _raise_error(self, e: DBAPIError) -> NoReturn:
|
|
75
|
+
constraint = e.__cause__.__cause__.constraint_name
|
|
76
|
+
if constraint == "fk__run__transfer_id__transfer":
|
|
77
|
+
raise TransferNotFoundError from e
|
|
78
|
+
raise SyncmasterError from e
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2023-2024 MTS (Mobile Telesystems)
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import Any, NoReturn
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import ScalarResult, insert, or_, select
|
|
7
|
+
from sqlalchemy.exc import DBAPIError, IntegrityError, NoResultFound
|
|
8
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
9
|
+
from sqlalchemy.orm import selectinload
|
|
10
|
+
|
|
11
|
+
from syncmaster.db.models import Transfer
|
|
12
|
+
from syncmaster.db.repositories.repository_with_owner import RepositoryWithOwner
|
|
13
|
+
from syncmaster.db.utils import Pagination
|
|
14
|
+
from syncmaster.exceptions import EntityNotFoundError, SyncmasterError
|
|
15
|
+
from syncmaster.exceptions.connection import ConnectionNotFoundError
|
|
16
|
+
from syncmaster.exceptions.group import GroupNotFoundError
|
|
17
|
+
from syncmaster.exceptions.queue import QueueNotFoundError
|
|
18
|
+
from syncmaster.exceptions.transfer import (
|
|
19
|
+
DuplicatedTransferNameError,
|
|
20
|
+
TransferNotFoundError,
|
|
21
|
+
TransferOwnerError,
|
|
22
|
+
)
|
|
23
|
+
from syncmaster.exceptions.user import UserNotFoundError
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TransferRepository(RepositoryWithOwner[Transfer]):
|
|
27
|
+
def __init__(self, session: AsyncSession):
|
|
28
|
+
super().__init__(model=Transfer, session=session)
|
|
29
|
+
|
|
30
|
+
async def paginate(
|
|
31
|
+
self,
|
|
32
|
+
page: int,
|
|
33
|
+
page_size: int,
|
|
34
|
+
group_id: int | None = None,
|
|
35
|
+
) -> Pagination:
|
|
36
|
+
stmt = select(Transfer).where(Transfer.is_deleted.is_(False))
|
|
37
|
+
|
|
38
|
+
return await self._paginate_scalar_result(
|
|
39
|
+
query=stmt.where(Transfer.group_id == group_id).order_by(Transfer.name),
|
|
40
|
+
page=page,
|
|
41
|
+
page_size=page_size,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
async def read_by_id(
|
|
45
|
+
self,
|
|
46
|
+
transfer_id: int,
|
|
47
|
+
) -> Transfer:
|
|
48
|
+
stmt = (
|
|
49
|
+
select(Transfer)
|
|
50
|
+
.where(
|
|
51
|
+
Transfer.id == transfer_id,
|
|
52
|
+
Transfer.is_deleted.is_(False),
|
|
53
|
+
)
|
|
54
|
+
.options(selectinload(Transfer.queue))
|
|
55
|
+
)
|
|
56
|
+
try:
|
|
57
|
+
result: ScalarResult[Transfer] = await self._session.scalars(stmt)
|
|
58
|
+
return result.one()
|
|
59
|
+
except NoResultFound as e:
|
|
60
|
+
raise TransferNotFoundError from e
|
|
61
|
+
|
|
62
|
+
async def create(
|
|
63
|
+
self,
|
|
64
|
+
group_id: int,
|
|
65
|
+
source_connection_id: int,
|
|
66
|
+
target_connection_id: int,
|
|
67
|
+
name: str,
|
|
68
|
+
description: str,
|
|
69
|
+
source_params: dict[str, Any],
|
|
70
|
+
target_params: dict[str, Any],
|
|
71
|
+
strategy_params: dict[str, Any],
|
|
72
|
+
queue_id: int,
|
|
73
|
+
) -> Transfer:
|
|
74
|
+
query = (
|
|
75
|
+
insert(Transfer)
|
|
76
|
+
.values(
|
|
77
|
+
group_id=group_id,
|
|
78
|
+
source_connection_id=source_connection_id,
|
|
79
|
+
target_connection_id=target_connection_id,
|
|
80
|
+
name=name,
|
|
81
|
+
description=description,
|
|
82
|
+
source_params=source_params,
|
|
83
|
+
target_params=target_params,
|
|
84
|
+
strategy_params=strategy_params,
|
|
85
|
+
queue_id=queue_id,
|
|
86
|
+
)
|
|
87
|
+
.returning(Transfer)
|
|
88
|
+
)
|
|
89
|
+
try:
|
|
90
|
+
result: ScalarResult[Transfer] = await self._session.scalars(query)
|
|
91
|
+
except IntegrityError as e:
|
|
92
|
+
self._raise_error(e)
|
|
93
|
+
else:
|
|
94
|
+
await self._session.flush()
|
|
95
|
+
return result.one()
|
|
96
|
+
|
|
97
|
+
async def update(
|
|
98
|
+
self,
|
|
99
|
+
transfer: Transfer,
|
|
100
|
+
name: str | None,
|
|
101
|
+
description: str | None,
|
|
102
|
+
source_connection_id: int | None,
|
|
103
|
+
target_connection_id: int | None,
|
|
104
|
+
source_params: dict[str, Any],
|
|
105
|
+
target_params: dict[str, Any],
|
|
106
|
+
strategy_params: dict[str, Any],
|
|
107
|
+
is_scheduled: bool | None,
|
|
108
|
+
schedule: str | None,
|
|
109
|
+
new_queue_id: int | None,
|
|
110
|
+
) -> Transfer:
|
|
111
|
+
try:
|
|
112
|
+
for key in transfer.source_params:
|
|
113
|
+
if key not in source_params or source_params[key] is None:
|
|
114
|
+
source_params[key] = transfer.source_params[key]
|
|
115
|
+
for key in transfer.target_params:
|
|
116
|
+
if key not in target_params or target_params[key] is None:
|
|
117
|
+
target_params[key] = transfer.target_params[key]
|
|
118
|
+
for key in transfer.strategy_params:
|
|
119
|
+
if key not in strategy_params or strategy_params[key] is None:
|
|
120
|
+
strategy_params[key] = transfer.strategy_params[key]
|
|
121
|
+
return await self._update(
|
|
122
|
+
Transfer.id == transfer.id,
|
|
123
|
+
Transfer.is_deleted.is_(False),
|
|
124
|
+
name=name or transfer.name,
|
|
125
|
+
description=description or transfer.description,
|
|
126
|
+
strategy_params=strategy_params,
|
|
127
|
+
is_scheduled=is_scheduled or transfer.is_scheduled,
|
|
128
|
+
schedule=schedule or transfer.schedule,
|
|
129
|
+
source_connection_id=source_connection_id or transfer.source_connection_id,
|
|
130
|
+
target_connection_id=target_connection_id or transfer.target_connection_id,
|
|
131
|
+
source_params=source_params,
|
|
132
|
+
target_params=target_params,
|
|
133
|
+
queue_id=new_queue_id or transfer.queue_id,
|
|
134
|
+
)
|
|
135
|
+
except IntegrityError as e:
|
|
136
|
+
self._raise_error(e)
|
|
137
|
+
|
|
138
|
+
async def delete(
|
|
139
|
+
self,
|
|
140
|
+
transfer_id: int,
|
|
141
|
+
) -> None:
|
|
142
|
+
try:
|
|
143
|
+
await self._delete(transfer_id)
|
|
144
|
+
except (NoResultFound, EntityNotFoundError) as e:
|
|
145
|
+
raise TransferNotFoundError from e
|
|
146
|
+
|
|
147
|
+
async def copy(
|
|
148
|
+
self,
|
|
149
|
+
transfer_id: int,
|
|
150
|
+
new_queue_id: int,
|
|
151
|
+
new_group_id: int | None,
|
|
152
|
+
new_source_connection: int | None,
|
|
153
|
+
new_target_connection: int | None,
|
|
154
|
+
new_name: str | None,
|
|
155
|
+
) -> Transfer:
|
|
156
|
+
try:
|
|
157
|
+
kwargs = dict(
|
|
158
|
+
group_id=new_group_id,
|
|
159
|
+
source_connection_id=new_source_connection,
|
|
160
|
+
target_connection_id=new_target_connection,
|
|
161
|
+
queue_id=new_queue_id,
|
|
162
|
+
name=new_name,
|
|
163
|
+
)
|
|
164
|
+
new_transfer = await self._copy(Transfer.id == transfer_id, **kwargs)
|
|
165
|
+
|
|
166
|
+
return new_transfer
|
|
167
|
+
except IntegrityError as integrity_error:
|
|
168
|
+
self._raise_error(integrity_error)
|
|
169
|
+
|
|
170
|
+
async def list_by_connection_id(self, conn_id: int) -> Sequence[Transfer]:
|
|
171
|
+
query = select(Transfer).where(
|
|
172
|
+
or_(
|
|
173
|
+
Transfer.source_connection_id == conn_id,
|
|
174
|
+
Transfer.target_connection_id == conn_id,
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
result = await self._session.scalars(query)
|
|
178
|
+
return result.fetchall()
|
|
179
|
+
|
|
180
|
+
def _raise_error(self, err: DBAPIError) -> NoReturn:
|
|
181
|
+
constraint = err.__cause__.__cause__.constraint_name
|
|
182
|
+
if constraint == "fk__transfer__group_id__group":
|
|
183
|
+
raise GroupNotFoundError from err
|
|
184
|
+
if constraint == "fk__transfer__user_id__user":
|
|
185
|
+
raise UserNotFoundError from err
|
|
186
|
+
|
|
187
|
+
if constraint in [
|
|
188
|
+
"fk__transfer__source_connection_id__connection",
|
|
189
|
+
"fk__transfer__target_connection_id__connection",
|
|
190
|
+
]:
|
|
191
|
+
raise ConnectionNotFoundError from err
|
|
192
|
+
|
|
193
|
+
if constraint == "ck__transfer__owner_constraint":
|
|
194
|
+
raise TransferOwnerError from err
|
|
195
|
+
|
|
196
|
+
if constraint == "fk__transfer__queue_id__queue":
|
|
197
|
+
raise QueueNotFoundError from err
|
|
198
|
+
|
|
199
|
+
if constraint == "uq__transfer__name_group_id":
|
|
200
|
+
raise DuplicatedTransferNameError
|
|
201
|
+
|
|
202
|
+
raise SyncmasterError from err
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2023-2024 MTS (Mobile Telesystems)
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
from typing import Any, NoReturn
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import ScalarResult, insert, select
|
|
6
|
+
from sqlalchemy.exc import DBAPIError, IntegrityError, NoResultFound
|
|
7
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
8
|
+
|
|
9
|
+
from syncmaster.db.models import User
|
|
10
|
+
from syncmaster.db.repositories.base import Repository
|
|
11
|
+
from syncmaster.db.utils import Pagination
|
|
12
|
+
from syncmaster.exceptions import EntityNotFoundError, SyncmasterError
|
|
13
|
+
from syncmaster.exceptions.user import UsernameAlreadyExistsError, UserNotFoundError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UserRepository(Repository[User]):
|
|
17
|
+
def __init__(self, session: AsyncSession) -> None:
|
|
18
|
+
super().__init__(model=User, session=session)
|
|
19
|
+
|
|
20
|
+
async def paginate(self, page: int, page_size: int, is_superuser: bool) -> Pagination:
|
|
21
|
+
stmt = select(User).where(User.is_deleted.is_(False))
|
|
22
|
+
|
|
23
|
+
if not is_superuser:
|
|
24
|
+
stmt = stmt.where(User.is_active.is_(True))
|
|
25
|
+
return await self._paginate_scalar_result(query=stmt.order_by(User.username), page=page, page_size=page_size)
|
|
26
|
+
|
|
27
|
+
async def read_by_id(self, user_id: int, **kwargs: Any) -> User:
|
|
28
|
+
try:
|
|
29
|
+
return await self._read_by_id(id=user_id, **kwargs)
|
|
30
|
+
except EntityNotFoundError as e:
|
|
31
|
+
raise UserNotFoundError from e
|
|
32
|
+
|
|
33
|
+
async def read_by_username(self, username: str) -> User:
|
|
34
|
+
try:
|
|
35
|
+
result: ScalarResult[User] = await self._session.scalars(select(User).where(User.username == username))
|
|
36
|
+
return result.one()
|
|
37
|
+
except NoResultFound as e:
|
|
38
|
+
raise EntityNotFoundError from e
|
|
39
|
+
|
|
40
|
+
async def update(self, user_id: int, data: dict) -> User:
|
|
41
|
+
try:
|
|
42
|
+
return await self._update(User.id == user_id, User.is_deleted.is_(False), **data)
|
|
43
|
+
except EntityNotFoundError as e:
|
|
44
|
+
raise UserNotFoundError from e
|
|
45
|
+
except IntegrityError as e:
|
|
46
|
+
self._raise_error(e)
|
|
47
|
+
|
|
48
|
+
async def create(self, username: str, is_active: bool, is_superuser: bool = False) -> User:
|
|
49
|
+
query = (
|
|
50
|
+
insert(User)
|
|
51
|
+
.values(
|
|
52
|
+
username=username,
|
|
53
|
+
is_active=is_active,
|
|
54
|
+
is_superuser=is_superuser,
|
|
55
|
+
)
|
|
56
|
+
.returning(User)
|
|
57
|
+
)
|
|
58
|
+
try:
|
|
59
|
+
result: ScalarResult[User] = await self._session.scalars(query)
|
|
60
|
+
await self._session.flush()
|
|
61
|
+
except IntegrityError as err:
|
|
62
|
+
self._raise_error(err)
|
|
63
|
+
else:
|
|
64
|
+
return result.one()
|
|
65
|
+
|
|
66
|
+
def _raise_error(self, err: DBAPIError) -> NoReturn:
|
|
67
|
+
constraint = err.__cause__.__cause__.constraint_name
|
|
68
|
+
|
|
69
|
+
if constraint == "ix__user__username":
|
|
70
|
+
raise UsernameAlreadyExistsError from err
|
|
71
|
+
|
|
72
|
+
raise SyncmasterError from err
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2023-2024 MTS (Mobile Telesystems)
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from cryptography.fernet import Fernet
|
|
6
|
+
|
|
7
|
+
from syncmaster.config import Settings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def decrypt_auth_data(
|
|
11
|
+
value: str,
|
|
12
|
+
settings: Settings,
|
|
13
|
+
) -> dict:
|
|
14
|
+
f = Fernet(settings.CRYPTO_KEY)
|
|
15
|
+
return json.loads(f.decrypt(value))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def encrypt_auth_data(
|
|
19
|
+
value: dict,
|
|
20
|
+
settings: Settings,
|
|
21
|
+
) -> str:
|
|
22
|
+
key = str.encode(settings.CRYPTO_KEY)
|
|
23
|
+
f = Fernet(key)
|
|
24
|
+
token = f.encrypt(str.encode(json.dumps(value)))
|
|
25
|
+
return token.decode(encoding="utf-8")
|