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,203 @@
1
+ # SPDX-FileCopyrightText: 2023-2024 MTS (Mobile Telesystems)
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ import logging
4
+
5
+ from fastapi import HTTPException, Request, status
6
+ from fastapi.responses import JSONResponse
7
+
8
+ from syncmaster.exceptions import ActionNotAllowedError, SyncmasterError
9
+ from syncmaster.exceptions.connection import (
10
+ ConnectionDeleteError,
11
+ ConnectionNotFoundError,
12
+ ConnectionOwnerError,
13
+ DuplicatedConnectionNameError,
14
+ )
15
+ from syncmaster.exceptions.credentials import AuthDataNotFoundError
16
+ from syncmaster.exceptions.group import (
17
+ AlreadyIsGroupMemberError,
18
+ AlreadyIsNotGroupMemberError,
19
+ GroupAdminNotFoundError,
20
+ GroupAlreadyExistsError,
21
+ GroupNotFoundError,
22
+ )
23
+ from syncmaster.exceptions.queue import (
24
+ DifferentTransferAndQueueGroupError,
25
+ QueueDeleteError,
26
+ QueueNotFoundError,
27
+ )
28
+ from syncmaster.exceptions.run import (
29
+ CannotConnectToTaskQueueError,
30
+ CannotStopRunError,
31
+ RunNotFoundError,
32
+ )
33
+ from syncmaster.exceptions.transfer import (
34
+ DifferentTransferAndConnectionsGroupsError,
35
+ DifferentTypeConnectionsAndParamsError,
36
+ DuplicatedTransferNameError,
37
+ TransferNotFoundError,
38
+ TransferOwnerError,
39
+ )
40
+ from syncmaster.exceptions.user import UsernameAlreadyExistsError, UserNotFoundError
41
+ from syncmaster.schemas.v1.status import StatusResponseSchema
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+
46
+ async def http_exception_handler(request: Request, exc: HTTPException):
47
+ return exception_json_response(status_code=exc.status_code, detail=exc.detail)
48
+
49
+
50
+ async def syncmsater_exception_handler(request: Request, exc: SyncmasterError):
51
+ if isinstance(exc, AuthDataNotFoundError):
52
+ return exception_json_response(
53
+ status_code=status.HTTP_404_NOT_FOUND,
54
+ detail=f"Credentials not found. {exc.message}",
55
+ )
56
+
57
+ if isinstance(exc, ConnectionDeleteError):
58
+ return exception_json_response(status_code=status.HTTP_409_CONFLICT, detail=exc.message)
59
+
60
+ if isinstance(exc, ActionNotAllowedError):
61
+ return exception_json_response(status_code=status.HTTP_403_FORBIDDEN, detail="You have no power here")
62
+
63
+ if isinstance(exc, GroupNotFoundError):
64
+ return exception_json_response(
65
+ status_code=status.HTTP_404_NOT_FOUND,
66
+ detail="Group not found",
67
+ )
68
+
69
+ if isinstance(exc, RunNotFoundError):
70
+ return exception_json_response(
71
+ status_code=status.HTTP_404_NOT_FOUND,
72
+ detail="Run not found",
73
+ )
74
+
75
+ if isinstance(exc, QueueNotFoundError):
76
+ return exception_json_response(
77
+ status_code=status.HTTP_404_NOT_FOUND,
78
+ detail="Queue not found",
79
+ )
80
+
81
+ if isinstance(exc, GroupAdminNotFoundError):
82
+ return exception_json_response(
83
+ status_code=status.HTTP_400_BAD_REQUEST,
84
+ detail="Admin not found",
85
+ )
86
+ if isinstance(exc, GroupAlreadyExistsError):
87
+ return exception_json_response(
88
+ status_code=status.HTTP_400_BAD_REQUEST,
89
+ detail="Group name already taken",
90
+ )
91
+
92
+ if isinstance(exc, AlreadyIsNotGroupMemberError):
93
+ return exception_json_response(
94
+ status_code=status.HTTP_400_BAD_REQUEST,
95
+ detail="User already is not group member",
96
+ )
97
+
98
+ if isinstance(exc, AlreadyIsGroupMemberError):
99
+ return exception_json_response(
100
+ status_code=status.HTTP_400_BAD_REQUEST,
101
+ detail="User already is group member",
102
+ )
103
+
104
+ if isinstance(exc, UserNotFoundError):
105
+ return exception_json_response(
106
+ status_code=status.HTTP_404_NOT_FOUND,
107
+ detail="User not found",
108
+ )
109
+
110
+ if isinstance(exc, UsernameAlreadyExistsError):
111
+ return exception_json_response(
112
+ status_code=status.HTTP_400_BAD_REQUEST,
113
+ detail="Username is already taken",
114
+ )
115
+
116
+ if isinstance(exc, ConnectionNotFoundError):
117
+ return exception_json_response(
118
+ status_code=status.HTTP_404_NOT_FOUND,
119
+ detail="Connection not found",
120
+ )
121
+
122
+ if isinstance(exc, ConnectionOwnerError):
123
+ return exception_json_response(
124
+ status_code=status.HTTP_400_BAD_REQUEST,
125
+ detail="Cannot create connection with that user_id and group_id values",
126
+ )
127
+
128
+ if isinstance(exc, TransferNotFoundError):
129
+ return exception_json_response(
130
+ status_code=status.HTTP_404_NOT_FOUND,
131
+ detail="Transfer not found",
132
+ )
133
+
134
+ if isinstance(exc, TransferOwnerError):
135
+ return exception_json_response(
136
+ status_code=status.HTTP_400_BAD_REQUEST,
137
+ detail="Cannot create transfer with that group_id value",
138
+ )
139
+
140
+ if isinstance(exc, DifferentTransferAndConnectionsGroupsError):
141
+ return exception_json_response(
142
+ status_code=status.HTTP_400_BAD_REQUEST,
143
+ detail="Connections should belong to the transfer group",
144
+ )
145
+
146
+ if isinstance(exc, DifferentTransferAndQueueGroupError):
147
+ return exception_json_response(
148
+ status_code=status.HTTP_400_BAD_REQUEST,
149
+ detail="Queue should belong to the transfer group",
150
+ )
151
+
152
+ if isinstance(exc, DifferentTypeConnectionsAndParamsError):
153
+ return exception_json_response(
154
+ status_code=status.HTTP_400_BAD_REQUEST,
155
+ detail=exc.message,
156
+ )
157
+
158
+ if isinstance(exc, QueueDeleteError):
159
+ return exception_json_response(
160
+ status_code=status.HTTP_409_CONFLICT,
161
+ detail=exc.message,
162
+ )
163
+
164
+ if isinstance(exc, DuplicatedConnectionNameError):
165
+ return exception_json_response(
166
+ status_code=status.HTTP_409_CONFLICT,
167
+ detail="The connection name already exists in the target group, please specify a new one",
168
+ )
169
+
170
+ if isinstance(exc, DuplicatedTransferNameError):
171
+ return exception_json_response(
172
+ status_code=status.HTTP_409_CONFLICT,
173
+ detail="The transfer name already exists in the target group, please specify a new one",
174
+ )
175
+
176
+ if isinstance(exc, CannotConnectToTaskQueueError):
177
+ return exception_json_response(
178
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
179
+ detail=f"Syncmaster not connected to task queue. Run {exc.run_id} was failed",
180
+ )
181
+
182
+ if isinstance(exc, CannotStopRunError):
183
+ return exception_json_response(
184
+ status_code=status.HTTP_400_BAD_REQUEST,
185
+ detail=f"Cannot stop run {exc.run_id}. Current status is {exc.current_status}",
186
+ )
187
+
188
+ logger.exception("Got unhandled error")
189
+ return exception_json_response(
190
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
191
+ detail="Got unhandled exception. See logs",
192
+ )
193
+
194
+
195
+ def exception_json_response(status_code: int, detail: str) -> JSONResponse:
196
+ return JSONResponse(
197
+ status_code=status_code,
198
+ content=StatusResponseSchema(
199
+ ok=False,
200
+ status_code=status_code,
201
+ message=detail,
202
+ ).dict(),
203
+ )
@@ -0,0 +1,2 @@
1
+ # SPDX-FileCopyrightText: 2023-2024 MTS (Mobile Telesystems)
2
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,63 @@
1
+ # SPDX-FileCopyrightText: 2023-2024 MTS (Mobile Telesystems)
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ import uvicorn
4
+ from fastapi import FastAPI, HTTPException
5
+ from starlette.middleware.cors import CORSMiddleware
6
+
7
+ from syncmaster.backend.api.deps import (
8
+ AuthMarker,
9
+ DatabaseEngineMarker,
10
+ DatabaseSessionMarker,
11
+ SettingsMarker,
12
+ UnitOfWorkMarker,
13
+ )
14
+ from syncmaster.backend.api.router import api_router
15
+ from syncmaster.backend.handler import (
16
+ http_exception_handler,
17
+ syncmsater_exception_handler,
18
+ )
19
+ from syncmaster.backend.services import get_auth_scheme
20
+ from syncmaster.config import Settings
21
+ from syncmaster.db.factory import create_engine, create_session_factory, get_uow
22
+ from syncmaster.exceptions import SyncmasterError
23
+
24
+
25
+ def get_application(settings: Settings) -> FastAPI:
26
+ application = FastAPI(
27
+ title=settings.PROJECT_NAME,
28
+ debug=settings.DEBUG,
29
+ )
30
+ application.add_middleware(
31
+ CORSMiddleware,
32
+ allow_origins=["*"],
33
+ allow_credentials=True,
34
+ allow_methods=["*"],
35
+ allow_headers=["*"],
36
+ )
37
+
38
+ application.include_router(api_router)
39
+ application.exception_handler(HTTPException)(http_exception_handler)
40
+ application.exception_handler(SyncmasterError)(syncmsater_exception_handler)
41
+
42
+ engine = create_engine(connection_uri=settings.build_db_connection_uri())
43
+ session_factory = create_session_factory(engine=engine)
44
+
45
+ auth_scheme = get_auth_scheme(settings)
46
+
47
+ application.dependency_overrides.update(
48
+ {
49
+ SettingsMarker: lambda: settings,
50
+ DatabaseEngineMarker: lambda: engine,
51
+ DatabaseSessionMarker: lambda: session_factory,
52
+ UnitOfWorkMarker: get_uow(session_factory, settings),
53
+ AuthMarker: auth_scheme,
54
+ }
55
+ )
56
+
57
+ return application
58
+
59
+
60
+ if __name__ == "__main__":
61
+ settings = Settings()
62
+ app = get_application(settings=settings)
63
+ uvicorn.run(app, host="0.0.0.0", port=8000)
@@ -0,0 +1,94 @@
1
+ # SPDX-FileCopyrightText: 2023-2024 MTS (Mobile Telesystems)
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ import logging
4
+ from time import sleep
5
+
6
+ from syncmaster.config import Settings, TestSettings
7
+ from tests.test_integration.test_run_transfer.conftest import get_spark_session
8
+
9
+ TIMEOUT = 5
10
+ COUNT = 12
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def check_test_postgres(settings: Settings, test_settings: TestSettings) -> None:
16
+ from onetl.connection import Postgres
17
+
18
+ spark_session = get_spark_session(settings)
19
+
20
+ count = COUNT
21
+ connection = Postgres(
22
+ host=test_settings.TEST_POSTGRES_HOST,
23
+ port=test_settings.TEST_POSTGRES_PORT,
24
+ user=test_settings.TEST_POSTGRES_USER,
25
+ password=test_settings.TEST_POSTGRES_PASSWORD,
26
+ database=test_settings.TEST_POSTGRES_DB,
27
+ spark=spark_session,
28
+ )
29
+ exception = None
30
+ while count > 0:
31
+ try:
32
+ connection.check()
33
+ return
34
+ except Exception:
35
+ count -= 1
36
+ logger.info("Got exception on postgres. Will try after %d sec", TIMEOUT)
37
+ sleep(TIMEOUT)
38
+ if exception:
39
+ raise exception
40
+
41
+
42
+ def check_test_oracle(settings: Settings, test_settings: TestSettings) -> None:
43
+ from onetl.connection import Oracle
44
+
45
+ spark_session = get_spark_session(settings)
46
+
47
+ count = COUNT
48
+ connection = Oracle(
49
+ host=test_settings.TEST_ORACLE_HOST,
50
+ port=test_settings.TEST_ORACLE_PORT,
51
+ user=test_settings.TEST_ORACLE_USER,
52
+ password=test_settings.TEST_ORACLE_PASSWORD,
53
+ service_name=test_settings.TEST_ORACLE_SERVICE_NAME,
54
+ sid=test_settings.TEST_ORACLE_SID,
55
+ spark=spark_session,
56
+ )
57
+ exception = None
58
+ while count > 0:
59
+ try:
60
+ connection.check()
61
+ return
62
+ except Exception:
63
+ count -= 1
64
+ logger.info("Got exception on oracle. Will try after %d sec", TIMEOUT)
65
+ sleep(TIMEOUT)
66
+ if exception:
67
+ raise exception
68
+
69
+
70
+ def check_test_hive(settings: Settings, test_settings: TestSettings) -> None:
71
+ from onetl.connection import Hive
72
+
73
+ spark_session = get_spark_session(settings)
74
+
75
+ count = COUNT
76
+ connection = Hive(
77
+ cluster=test_settings.TEST_HIVE_CLUSTER,
78
+ spark=spark_session,
79
+ )
80
+ exception = None
81
+ while count > 0:
82
+ try:
83
+ connection.check()
84
+ connection.execute("SHOW DATABASES")
85
+ return
86
+ except Exception:
87
+ count -= 1
88
+ logger.info("Got exception on hive. will try after %d sec", TIMEOUT)
89
+ sleep(TIMEOUT)
90
+ if exception:
91
+ raise exception
92
+
93
+
94
+ settings = Settings()
@@ -0,0 +1,4 @@
1
+ # SPDX-FileCopyrightText: 2023-2024 MTS (Mobile Telesystems)
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from syncmaster.backend.services.auth import get_auth_scheme, get_user
4
+ from syncmaster.backend.services.unit_of_work import UnitOfWork
@@ -0,0 +1,58 @@
1
+ # SPDX-FileCopyrightText: 2023-2024 MTS (Mobile Telesystems)
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from collections.abc import Awaitable, Callable
4
+
5
+ from fastapi import Depends, Request, status
6
+ from fastapi.exceptions import HTTPException
7
+ from fastapi.security import OAuth2PasswordBearer
8
+
9
+ from syncmaster.backend.api.deps import AuthMarker, SettingsMarker, UnitOfWorkMarker
10
+ from syncmaster.backend.api.v1.auth.utils import decode_jwt
11
+ from syncmaster.backend.services.unit_of_work import UnitOfWork
12
+ from syncmaster.config import Settings
13
+ from syncmaster.db.models import User
14
+
15
+
16
+ def get_user(
17
+ is_active: bool = False,
18
+ is_superuser: bool = False,
19
+ ) -> Callable[[str, Settings, UnitOfWork], Awaitable[User]]:
20
+ async def wrapper(
21
+ token: str = Depends(AuthMarker),
22
+ settings: Settings = Depends(SettingsMarker),
23
+ unit_of_work: UnitOfWork = Depends(UnitOfWorkMarker),
24
+ ) -> User:
25
+ async with unit_of_work:
26
+ token_data = decode_jwt(token, settings=settings)
27
+ if token_data is None:
28
+ raise HTTPException(
29
+ status_code=status.HTTP_401_UNAUTHORIZED,
30
+ detail="You are not authorized",
31
+ )
32
+ user = await unit_of_work.user.read_by_id(user_id=token_data.user_id)
33
+ if user is None:
34
+ raise HTTPException(
35
+ status_code=status.HTTP_404_NOT_FOUND,
36
+ detail="User not found",
37
+ )
38
+ if is_active and not user.is_active:
39
+ raise HTTPException(
40
+ status_code=status.HTTP_403_FORBIDDEN,
41
+ detail="Inactive user",
42
+ )
43
+ if is_superuser and not user.is_superuser:
44
+ raise HTTPException(
45
+ status_code=status.HTTP_403_FORBIDDEN,
46
+ detail="You have no power here",
47
+ )
48
+ return user
49
+
50
+ return wrapper
51
+
52
+
53
+ def get_auth_scheme(
54
+ settings: Settings,
55
+ ) -> Callable[[Request], str] | OAuth2PasswordBearer:
56
+ if settings.DEBUG:
57
+ return OAuth2PasswordBearer(tokenUrl=settings.AUTH_TOKEN_URL)
58
+ return lambda request: "Here will be keycloak"
@@ -0,0 +1,44 @@
1
+ # SPDX-FileCopyrightText: 2023-2024 MTS (Mobile Telesystems)
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from sqlalchemy.ext.asyncio import AsyncSession
4
+
5
+ from syncmaster.config import Settings
6
+ from syncmaster.db.models import AuthData
7
+ from syncmaster.db.repositories import (
8
+ ConnectionRepository,
9
+ CredentialsRepository,
10
+ GroupRepository,
11
+ QueueRepository,
12
+ RunRepository,
13
+ TransferRepository,
14
+ UserRepository,
15
+ )
16
+
17
+
18
+ class UnitOfWork:
19
+ def __init__(
20
+ self,
21
+ session: AsyncSession,
22
+ settings: Settings,
23
+ ):
24
+ self._session = session
25
+ self.user = UserRepository(session=session)
26
+ self.group = GroupRepository(session=session)
27
+ self.connection = ConnectionRepository(session=session)
28
+ self.transfer = TransferRepository(session=session)
29
+ self.run = RunRepository(session=session)
30
+ self.credentials = CredentialsRepository(
31
+ session=session,
32
+ settings=settings,
33
+ model=AuthData,
34
+ )
35
+ self.queue = QueueRepository(session=session)
36
+
37
+ async def __aenter__(self):
38
+ return self
39
+
40
+ async def __aexit__(self, exc_type, exc, tb):
41
+ if exc_type:
42
+ await self._session.rollback()
43
+ else:
44
+ await self._session.commit()
syncmaster/config.py ADDED
@@ -0,0 +1,110 @@
1
+ # SPDX-FileCopyrightText: 2023-2024 MTS (Mobile Telesystems)
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from enum import StrEnum
4
+ from typing import TYPE_CHECKING
5
+
6
+ from pydantic import model_validator
7
+ from pydantic.types import ImportString
8
+ from pydantic_settings import BaseSettings
9
+
10
+ if TYPE_CHECKING:
11
+ pass
12
+
13
+
14
+ class EnvTypes(StrEnum):
15
+ LOCAL = "LOCAL"
16
+
17
+
18
+ class Settings(BaseSettings):
19
+ ENV: EnvTypes = EnvTypes.LOCAL
20
+ DEBUG: bool = True
21
+
22
+ PROJECT_NAME: str = "SyncMaster"
23
+
24
+ SECRET_KEY: str = "secret"
25
+ SECURITY_ALGORITHM: str = "HS256"
26
+ AUTH_TOKEN_URL: str = "v1/auth/token"
27
+ CRYPTO_KEY: str = "UBgPTioFrtH2unlC4XFDiGf5sYfzbdSf_VgiUSaQc94="
28
+
29
+ POSTGRES_HOST: str
30
+ POSTGRES_PORT: int
31
+ POSTGRES_DB: str
32
+ POSTGRES_USER: str
33
+ POSTGRES_PASSWORD: str
34
+
35
+ RABBITMQ_HOST: str
36
+ RABBITMQ_PORT: int
37
+ RABBITMQ_USER: str
38
+ RABBITMQ_PASSWORD: str
39
+
40
+ def build_db_connection_uri(
41
+ self,
42
+ *,
43
+ driver: str | None = None,
44
+ host: str | None = None,
45
+ port: int | None = None,
46
+ user: str | None = None,
47
+ password: str | None = None,
48
+ database: str | None = None,
49
+ ) -> str:
50
+ return "postgresql+{}://{}:{}@{}:{}/{}".format(
51
+ driver or "asyncpg",
52
+ user or self.POSTGRES_USER,
53
+ password or self.POSTGRES_PASSWORD,
54
+ host or self.POSTGRES_HOST,
55
+ port or self.POSTGRES_PORT,
56
+ database or self.POSTGRES_DB,
57
+ )
58
+
59
+ def build_rabbit_connection_uri(
60
+ self,
61
+ *,
62
+ host: str | None = None,
63
+ port: int | None = None,
64
+ user: str | None = None,
65
+ password: str | None = None,
66
+ ) -> str:
67
+ return "amqp://{}:{}@{}:{}//".format(
68
+ user or self.RABBITMQ_USER,
69
+ password or self.RABBITMQ_PASSWORD,
70
+ host or self.RABBITMQ_HOST,
71
+ port or self.RABBITMQ_PORT,
72
+ )
73
+
74
+ TOKEN_EXPIRED_TIME: int = 60 * 60 * 10 # 10 hours
75
+ CREATE_SPARK_SESSION_FUNCTION: ImportString = "syncmaster.worker.spark.get_worker_spark_session"
76
+
77
+
78
+ class TestSettings(BaseSettings):
79
+ TEST_POSTGRES_HOST: str
80
+ TEST_POSTGRES_PORT: int
81
+ TEST_POSTGRES_DB: str
82
+ TEST_POSTGRES_USER: str
83
+ TEST_POSTGRES_PASSWORD: str
84
+
85
+ TEST_ORACLE_HOST: str
86
+ TEST_ORACLE_PORT: int
87
+ TEST_ORACLE_USER: str
88
+ TEST_ORACLE_PASSWORD: str
89
+ TEST_ORACLE_SID: str | None = None
90
+ TEST_ORACLE_SERVICE_NAME: str | None = None
91
+
92
+ TEST_HIVE_CLUSTER: str
93
+ TEST_HIVE_USER: str
94
+ TEST_HIVE_PASSWORD: str
95
+
96
+ TEST_S3_HOST: str
97
+ TEST_S3_PORT: int
98
+ TEST_S3_BUCKET: str
99
+ TEST_S3_ACCESS_KEY: str
100
+ TEST_S3_SECRET_KEY: str
101
+ TEST_S3_PROTOCOL: str = "http"
102
+ TEST_S3_ADDITIONAL_PARAMS: dict = {}
103
+
104
+ @model_validator(mode="before")
105
+ def check_sid_and_service_name(cls, values):
106
+ sid = values.get("TEST_ORACLE_SID")
107
+ service_name = values.get("TEST_ORACLE_SERVICE_NAME")
108
+ if (sid is None) == (service_name is None):
109
+ raise ValueError("Connection must have one param: sid or service name")
110
+ return values
@@ -0,0 +1,2 @@
1
+ # SPDX-FileCopyrightText: 2023-2024 MTS (Mobile Telesystems)
2
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,41 @@
1
+ [alembic]
2
+ script_location = %(here)s/migrations
3
+ file_template = %%(year)d-%%(month).2d-%%(day).2d_%%(rev)s_%%(slug)s
4
+ prepend_sys_path = .
5
+ version_path_separator = os
6
+
7
+ [post_write_hooks]
8
+
9
+ [loggers]
10
+ keys = root,sqlalchemy,alembic
11
+
12
+ [handlers]
13
+ keys = console
14
+
15
+ [formatters]
16
+ keys = generic
17
+
18
+ [logger_root]
19
+ level = WARN
20
+ handlers = console
21
+ qualname =
22
+
23
+ [logger_sqlalchemy]
24
+ level = WARN
25
+ handlers =
26
+ qualname = sqlalchemy.engine
27
+
28
+ [logger_alembic]
29
+ level = INFO
30
+ handlers =
31
+ qualname = alembic
32
+
33
+ [handler_console]
34
+ class = StreamHandler
35
+ args = (sys.stderr,)
36
+ level = NOTSET
37
+ formatter = generic
38
+
39
+ [formatter_generic]
40
+ format = %(levelname)-5.5s [%(name)s] %(message)s
41
+ datefmt = %H:%M:%S
syncmaster/db/base.py ADDED
@@ -0,0 +1,28 @@
1
+ # SPDX-FileCopyrightText: 2023-2024 MTS (Mobile Telesystems)
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ import re
4
+
5
+ from sqlalchemy import MetaData
6
+ from sqlalchemy.orm import DeclarativeBase, declared_attr
7
+
8
+ convention = {
9
+ "all_column_names": lambda constraint, table: "_".join([column.name for column in constraint.columns.values()]),
10
+ "ix": "ix__%(table_name)s__%(all_column_names)s",
11
+ "uq": "uq__%(table_name)s__%(all_column_names)s",
12
+ "ck": "ck__%(table_name)s__%(constraint_name)s",
13
+ "fk": ("fk__%(table_name)s__%(all_column_names)s__%(referred_table_name)s"),
14
+ "pk": "pk__%(table_name)s",
15
+ }
16
+
17
+ model_metadata = MetaData(naming_convention=convention)
18
+
19
+
20
+ # as_declarative decorator causes mypy errors.
21
+ # Use inheritance from DeclarativeBase.
22
+ class Base(DeclarativeBase):
23
+ metadata = model_metadata
24
+
25
+ @declared_attr
26
+ def __tablename__(cls) -> str:
27
+ name_list = re.findall(r"[A-Z][a-z\d]*", cls.__name__)
28
+ return "_".join(name_list).lower()
@@ -0,0 +1,37 @@
1
+ # SPDX-FileCopyrightText: 2023-2024 MTS (Mobile Telesystems)
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from collections.abc import AsyncGenerator, Callable
4
+ from typing import Any
5
+
6
+ from sqlalchemy.ext.asyncio import (
7
+ AsyncEngine,
8
+ AsyncSession,
9
+ async_sessionmaker,
10
+ create_async_engine,
11
+ )
12
+
13
+ from syncmaster.backend.services import UnitOfWork
14
+ from syncmaster.config import Settings
15
+
16
+
17
+ def create_engine(connection_uri: str, **engine_kwargs: Any) -> AsyncEngine:
18
+ return create_async_engine(url=connection_uri, **engine_kwargs)
19
+
20
+
21
+ def create_session_factory(engine: AsyncEngine) -> async_sessionmaker[AsyncSession]:
22
+ return async_sessionmaker(
23
+ bind=engine,
24
+ class_=AsyncSession,
25
+ expire_on_commit=False,
26
+ )
27
+
28
+
29
+ def get_uow(
30
+ session_factory: async_sessionmaker[AsyncSession],
31
+ settings: Settings,
32
+ ) -> Callable[[], AsyncGenerator[UnitOfWork, None]]:
33
+ async def wrapper():
34
+ async with session_factory() as session:
35
+ yield UnitOfWork(session=session, settings=settings)
36
+
37
+ return wrapper
@@ -0,0 +1 @@
1
+ Generic single-database configuration with an async dbapi.