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.
Files changed (110) hide show
  1. data_syncmaster-0.1.1.dist-info/LICENSE.txt +203 -0
  2. data_syncmaster-0.1.1.dist-info/METADATA +115 -0
  3. data_syncmaster-0.1.1.dist-info/RECORD +110 -0
  4. data_syncmaster-0.1.1.dist-info/WHEEL +4 -0
  5. syncmaster/__init__.py +6 -0
  6. syncmaster/backend/__init__.py +2 -0
  7. syncmaster/backend/api/__init__.py +2 -0
  8. syncmaster/backend/api/deps.py +20 -0
  9. syncmaster/backend/api/monitoring.py +10 -0
  10. syncmaster/backend/api/router.py +10 -0
  11. syncmaster/backend/api/v1/__init__.py +2 -0
  12. syncmaster/backend/api/v1/auth/__init__.py +2 -0
  13. syncmaster/backend/api/v1/auth/router.py +32 -0
  14. syncmaster/backend/api/v1/auth/utils.py +26 -0
  15. syncmaster/backend/api/v1/connections.py +300 -0
  16. syncmaster/backend/api/v1/groups.py +225 -0
  17. syncmaster/backend/api/v1/queue.py +148 -0
  18. syncmaster/backend/api/v1/router.py +18 -0
  19. syncmaster/backend/api/v1/transfers/__init__.py +2 -0
  20. syncmaster/backend/api/v1/transfers/router.py +469 -0
  21. syncmaster/backend/api/v1/transfers/utils.py +17 -0
  22. syncmaster/backend/api/v1/users.py +75 -0
  23. syncmaster/backend/export_openapi_schema.py +26 -0
  24. syncmaster/backend/handler.py +203 -0
  25. syncmaster/backend/logger.py +2 -0
  26. syncmaster/backend/main.py +63 -0
  27. syncmaster/backend/pre_start.py +94 -0
  28. syncmaster/backend/services/__init__.py +4 -0
  29. syncmaster/backend/services/auth.py +58 -0
  30. syncmaster/backend/services/unit_of_work.py +44 -0
  31. syncmaster/config.py +110 -0
  32. syncmaster/db/__init__.py +2 -0
  33. syncmaster/db/alembic.ini +41 -0
  34. syncmaster/db/base.py +28 -0
  35. syncmaster/db/factory.py +37 -0
  36. syncmaster/db/migrations/README +1 -0
  37. syncmaster/db/migrations/__init__.py +2 -0
  38. syncmaster/db/migrations/env.py +87 -0
  39. syncmaster/db/migrations/script.py.mako +24 -0
  40. syncmaster/db/migrations/versions/2023-11-23_478240cdad4b_init.py +242 -0
  41. syncmaster/db/migrations/versions/__init__.py +2 -0
  42. syncmaster/db/mixins.py +33 -0
  43. syncmaster/db/models.py +194 -0
  44. syncmaster/db/repositories/__init__.py +22 -0
  45. syncmaster/db/repositories/base.py +109 -0
  46. syncmaster/db/repositories/connection.py +138 -0
  47. syncmaster/db/repositories/credentials_repository.py +87 -0
  48. syncmaster/db/repositories/group.py +264 -0
  49. syncmaster/db/repositories/queue.py +195 -0
  50. syncmaster/db/repositories/repository_with_owner.py +115 -0
  51. syncmaster/db/repositories/run.py +78 -0
  52. syncmaster/db/repositories/transfer.py +202 -0
  53. syncmaster/db/repositories/user.py +72 -0
  54. syncmaster/db/repositories/utils.py +25 -0
  55. syncmaster/db/utils.py +31 -0
  56. syncmaster/dto/__init__.py +2 -0
  57. syncmaster/dto/connections.py +60 -0
  58. syncmaster/dto/transfers.py +46 -0
  59. syncmaster/exceptions/__init__.py +13 -0
  60. syncmaster/exceptions/base.py +12 -0
  61. syncmaster/exceptions/connection.py +28 -0
  62. syncmaster/exceptions/credentials.py +8 -0
  63. syncmaster/exceptions/group.py +27 -0
  64. syncmaster/exceptions/queue.py +16 -0
  65. syncmaster/exceptions/run.py +19 -0
  66. syncmaster/exceptions/transfer.py +39 -0
  67. syncmaster/exceptions/user.py +11 -0
  68. syncmaster/schemas/__init__.py +2 -0
  69. syncmaster/schemas/v1/__init__.py +54 -0
  70. syncmaster/schemas/v1/auth.py +12 -0
  71. syncmaster/schemas/v1/connection_types.py +9 -0
  72. syncmaster/schemas/v1/connections/__init__.py +2 -0
  73. syncmaster/schemas/v1/connections/connection.py +146 -0
  74. syncmaster/schemas/v1/connections/hdfs.py +40 -0
  75. syncmaster/schemas/v1/connections/hive.py +40 -0
  76. syncmaster/schemas/v1/connections/oracle.py +58 -0
  77. syncmaster/schemas/v1/connections/postgres.py +48 -0
  78. syncmaster/schemas/v1/connections/s3.py +66 -0
  79. syncmaster/schemas/v1/file_formats.py +7 -0
  80. syncmaster/schemas/v1/groups.py +39 -0
  81. syncmaster/schemas/v1/page.py +40 -0
  82. syncmaster/schemas/v1/queue.py +32 -0
  83. syncmaster/schemas/v1/status.py +16 -0
  84. syncmaster/schemas/v1/transfer_types.py +6 -0
  85. syncmaster/schemas/v1/transfers/__init__.py +172 -0
  86. syncmaster/schemas/v1/transfers/db.py +23 -0
  87. syncmaster/schemas/v1/transfers/file/__init__.py +2 -0
  88. syncmaster/schemas/v1/transfers/file/base.py +47 -0
  89. syncmaster/schemas/v1/transfers/file/hdfs.py +27 -0
  90. syncmaster/schemas/v1/transfers/file/s3.py +27 -0
  91. syncmaster/schemas/v1/transfers/file_format.py +29 -0
  92. syncmaster/schemas/v1/transfers/run.py +37 -0
  93. syncmaster/schemas/v1/transfers/strategy.py +15 -0
  94. syncmaster/schemas/v1/types.py +5 -0
  95. syncmaster/schemas/v1/users.py +83 -0
  96. syncmaster/worker/__init__.py +2 -0
  97. syncmaster/worker/base.py +14 -0
  98. syncmaster/worker/config.py +18 -0
  99. syncmaster/worker/controller.py +127 -0
  100. syncmaster/worker/handlers/__init__.py +2 -0
  101. syncmaster/worker/handlers/base.py +49 -0
  102. syncmaster/worker/handlers/file/__init__.py +2 -0
  103. syncmaster/worker/handlers/file/base.py +56 -0
  104. syncmaster/worker/handlers/file/hdfs.py +14 -0
  105. syncmaster/worker/handlers/file/s3.py +20 -0
  106. syncmaster/worker/handlers/hive.py +41 -0
  107. syncmaster/worker/handlers/oracle.py +48 -0
  108. syncmaster/worker/handlers/postgres.py +47 -0
  109. syncmaster/worker/spark.py +93 -0
  110. 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")