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,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,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,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,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()
|
syncmaster/db/factory.py
ADDED
|
@@ -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.
|