amplify-excel-migrator 1.1.5__py3-none-any.whl → 1.2.15__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.
- amplify_excel_migrator/__init__.py +17 -0
- amplify_excel_migrator/auth/__init__.py +6 -0
- amplify_excel_migrator/auth/cognito_auth.py +306 -0
- amplify_excel_migrator/auth/provider.py +42 -0
- amplify_excel_migrator/cli/__init__.py +5 -0
- amplify_excel_migrator/cli/commands.py +165 -0
- amplify_excel_migrator/client.py +47 -0
- amplify_excel_migrator/core/__init__.py +5 -0
- amplify_excel_migrator/core/config.py +98 -0
- amplify_excel_migrator/data/__init__.py +7 -0
- amplify_excel_migrator/data/excel_reader.py +23 -0
- amplify_excel_migrator/data/transformer.py +119 -0
- amplify_excel_migrator/data/validator.py +48 -0
- amplify_excel_migrator/graphql/__init__.py +8 -0
- amplify_excel_migrator/graphql/client.py +137 -0
- amplify_excel_migrator/graphql/executor.py +405 -0
- amplify_excel_migrator/graphql/mutation_builder.py +80 -0
- amplify_excel_migrator/graphql/query_builder.py +194 -0
- amplify_excel_migrator/migration/__init__.py +8 -0
- amplify_excel_migrator/migration/batch_uploader.py +23 -0
- amplify_excel_migrator/migration/failure_tracker.py +92 -0
- amplify_excel_migrator/migration/orchestrator.py +143 -0
- amplify_excel_migrator/migration/progress_reporter.py +57 -0
- amplify_excel_migrator/schema/__init__.py +6 -0
- model_field_parser.py → amplify_excel_migrator/schema/field_parser.py +100 -22
- amplify_excel_migrator/schema/introspector.py +95 -0
- {amplify_excel_migrator-1.1.5.dist-info → amplify_excel_migrator-1.2.15.dist-info}/METADATA +121 -26
- amplify_excel_migrator-1.2.15.dist-info/RECORD +40 -0
- amplify_excel_migrator-1.2.15.dist-info/entry_points.txt +2 -0
- amplify_excel_migrator-1.2.15.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/test_cli_commands.py +292 -0
- tests/test_client.py +187 -0
- tests/test_cognito_auth.py +363 -0
- tests/test_config_manager.py +347 -0
- tests/test_field_parser.py +615 -0
- tests/test_mutation_builder.py +391 -0
- tests/test_query_builder.py +384 -0
- amplify_client.py +0 -941
- amplify_excel_migrator-1.1.5.dist-info/RECORD +0 -9
- amplify_excel_migrator-1.1.5.dist-info/entry_points.txt +0 -2
- amplify_excel_migrator-1.1.5.dist-info/top_level.txt +0 -3
- migrator.py +0 -437
- {amplify_excel_migrator-1.1.5.dist-info → amplify_excel_migrator-1.2.15.dist-info}/WHEEL +0 -0
- {amplify_excel_migrator-1.1.5.dist-info → amplify_excel_migrator-1.2.15.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Amplify Excel Migrator - Migrate Excel data to AWS Amplify GraphQL API."""
|
|
2
|
+
|
|
3
|
+
__version__ = "1.2.5"
|
|
4
|
+
|
|
5
|
+
from amplify_excel_migrator.client import AmplifyClient
|
|
6
|
+
from amplify_excel_migrator.migration import MigrationOrchestrator
|
|
7
|
+
from amplify_excel_migrator.auth import AuthenticationProvider, CognitoAuthProvider
|
|
8
|
+
from amplify_excel_migrator.core import ConfigManager
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"AmplifyClient",
|
|
12
|
+
"MigrationOrchestrator",
|
|
13
|
+
"AuthenticationProvider",
|
|
14
|
+
"CognitoAuthProvider",
|
|
15
|
+
"ConfigManager",
|
|
16
|
+
"__version__",
|
|
17
|
+
]
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""AWS Cognito authentication provider implementation."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from getpass import getpass
|
|
6
|
+
|
|
7
|
+
import boto3
|
|
8
|
+
import jwt
|
|
9
|
+
from botocore.exceptions import NoCredentialsError, ProfileNotFound, NoRegionError, ClientError
|
|
10
|
+
from pycognito import Cognito, MFAChallengeException
|
|
11
|
+
from pycognito.exceptions import ForceChangePasswordException
|
|
12
|
+
|
|
13
|
+
from .provider import AuthenticationProvider
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CognitoAuthProvider(AuthenticationProvider):
|
|
19
|
+
"""AWS Cognito authentication provider."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
user_pool_id: str,
|
|
24
|
+
client_id: str,
|
|
25
|
+
region: str,
|
|
26
|
+
admin_group_name: str = "ADMINS",
|
|
27
|
+
):
|
|
28
|
+
"""
|
|
29
|
+
Initialize the Cognito authentication provider.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
user_pool_id: Cognito User Pool ID
|
|
33
|
+
client_id: Cognito App Client ID
|
|
34
|
+
region: AWS region
|
|
35
|
+
admin_group_name: Name of the admin group to check membership
|
|
36
|
+
"""
|
|
37
|
+
self.user_pool_id = user_pool_id
|
|
38
|
+
self.client_id = client_id
|
|
39
|
+
self.region = region
|
|
40
|
+
self.admin_group_name = admin_group_name
|
|
41
|
+
|
|
42
|
+
self.cognito_client: Optional[Cognito] = None
|
|
43
|
+
self.boto_cognito_admin_client: Optional[any] = None
|
|
44
|
+
self._id_token: Optional[str] = None
|
|
45
|
+
self._mfa_tokens: Optional[dict] = None
|
|
46
|
+
|
|
47
|
+
def authenticate(self, username: str, password: str, mfa_code: Optional[str] = None) -> bool:
|
|
48
|
+
"""
|
|
49
|
+
Authenticate using standard Cognito user authentication.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
username: User's username or email
|
|
53
|
+
password: User's password
|
|
54
|
+
mfa_code: Optional MFA code if MFA is enabled
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
True if authentication successful, False otherwise
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
if not self.cognito_client:
|
|
61
|
+
self._init_cognito_client(username)
|
|
62
|
+
|
|
63
|
+
if mfa_code and self._mfa_tokens:
|
|
64
|
+
if not self._complete_mfa_challenge(mfa_code):
|
|
65
|
+
return False
|
|
66
|
+
else:
|
|
67
|
+
self.cognito_client.authenticate(password=password)
|
|
68
|
+
|
|
69
|
+
self._id_token = self.cognito_client.id_token
|
|
70
|
+
|
|
71
|
+
self._check_user_in_admins_group(self._id_token)
|
|
72
|
+
|
|
73
|
+
logger.info("✅ Authentication successful")
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
except MFAChallengeException as e:
|
|
77
|
+
logger.warning("MFA required")
|
|
78
|
+
if hasattr(e, "get_tokens"):
|
|
79
|
+
self._mfa_tokens = e.get_tokens()
|
|
80
|
+
|
|
81
|
+
mfa_code = input("Enter MFA code: ").strip()
|
|
82
|
+
if mfa_code:
|
|
83
|
+
return self.authenticate(username, password, mfa_code)
|
|
84
|
+
else:
|
|
85
|
+
logger.error("MFA code required but not provided")
|
|
86
|
+
return False
|
|
87
|
+
else:
|
|
88
|
+
logger.error("MFA challenge received but no session tokens available")
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
except ForceChangePasswordException:
|
|
92
|
+
logger.warning("Password change required")
|
|
93
|
+
new_password = input("Your password has expired. Enter new password: ").strip()
|
|
94
|
+
confirm_password = input("Confirm new password: ").strip()
|
|
95
|
+
if new_password != confirm_password:
|
|
96
|
+
logger.error("Passwords do not match")
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
self.cognito_client.new_password_challenge(password, new_password)
|
|
101
|
+
return self.authenticate(username, new_password)
|
|
102
|
+
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.error(f"Failed to change password: {e}")
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.error(f"Authentication failed: {e}")
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
def authenticate_admin(self, username: str, password: str, aws_profile: Optional[str] = None) -> bool:
|
|
112
|
+
"""
|
|
113
|
+
Authenticate using AWS admin credentials (ADMIN_USER_PASSWORD_AUTH flow).
|
|
114
|
+
|
|
115
|
+
Requires AWS credentials with cognito-idp:ListUserPoolClients permission.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
username: User's username or email
|
|
119
|
+
password: User's password
|
|
120
|
+
aws_profile: Optional AWS profile name
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
True if authentication successful, False otherwise
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
if not self.boto_cognito_admin_client:
|
|
127
|
+
self._init_boto_admin_client(aws_profile)
|
|
128
|
+
|
|
129
|
+
logger.info(f"Authenticating {username} using ADMIN_USER_PASSWORD_AUTH flow...")
|
|
130
|
+
|
|
131
|
+
response = self.boto_cognito_admin_client.admin_initiate_auth(
|
|
132
|
+
UserPoolId=self.user_pool_id,
|
|
133
|
+
ClientId=self.client_id,
|
|
134
|
+
AuthFlow="ADMIN_USER_PASSWORD_AUTH",
|
|
135
|
+
AuthParameters={"USERNAME": username, "PASSWORD": password},
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
self._check_for_mfa_challenges(response, username)
|
|
139
|
+
|
|
140
|
+
if "AuthenticationResult" in response:
|
|
141
|
+
self._id_token = response["AuthenticationResult"]["IdToken"]
|
|
142
|
+
else:
|
|
143
|
+
logger.error("❌ Authentication failed: No AuthenticationResult in response")
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
self._check_user_in_admins_group(self._id_token)
|
|
147
|
+
|
|
148
|
+
logger.info("✅ Authentication successful")
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
except Exception as e:
|
|
152
|
+
if hasattr(e, "response"):
|
|
153
|
+
error_code = e.response.get("Error", {}).get("Code", "Unknown")
|
|
154
|
+
if error_code == "NotAuthorizedException":
|
|
155
|
+
logger.error(f"❌ Authentication failed: {e}")
|
|
156
|
+
elif error_code == "UserNotFoundException":
|
|
157
|
+
logger.error(f"❌ User not found: {username}")
|
|
158
|
+
else:
|
|
159
|
+
logger.error(f"❌ Error during authentication: {e}")
|
|
160
|
+
else:
|
|
161
|
+
logger.error(f"❌ Error during authentication: {e}")
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
def get_id_token(self) -> Optional[str]:
|
|
165
|
+
"""
|
|
166
|
+
Get the ID token from the last successful authentication.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
ID token string if authenticated, None otherwise
|
|
170
|
+
"""
|
|
171
|
+
return self._id_token
|
|
172
|
+
|
|
173
|
+
def is_authenticated(self) -> bool:
|
|
174
|
+
"""
|
|
175
|
+
Check if the provider is currently authenticated.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
True if authenticated, False otherwise
|
|
179
|
+
"""
|
|
180
|
+
return self._id_token is not None
|
|
181
|
+
|
|
182
|
+
def _init_cognito_client(self, username: str) -> None:
|
|
183
|
+
"""Initialize the standard Cognito client."""
|
|
184
|
+
try:
|
|
185
|
+
self.cognito_client = Cognito(
|
|
186
|
+
user_pool_id=self.user_pool_id,
|
|
187
|
+
client_id=self.client_id,
|
|
188
|
+
user_pool_region=self.region,
|
|
189
|
+
username=username,
|
|
190
|
+
)
|
|
191
|
+
except ValueError as e:
|
|
192
|
+
logger.error(f"Invalid parameter: {e}")
|
|
193
|
+
raise
|
|
194
|
+
|
|
195
|
+
def _init_boto_admin_client(self, aws_profile: Optional[str] = None) -> None:
|
|
196
|
+
"""Initialize the boto3 Cognito admin client."""
|
|
197
|
+
try:
|
|
198
|
+
if aws_profile:
|
|
199
|
+
session = boto3.Session(profile_name=aws_profile)
|
|
200
|
+
self.boto_cognito_admin_client = session.client("cognito-idp", region_name=self.region)
|
|
201
|
+
else:
|
|
202
|
+
# Use default AWS credentials (from ~/.aws/credentials, env vars, or IAM role)
|
|
203
|
+
self.boto_cognito_admin_client = boto3.client("cognito-idp", region_name=self.region)
|
|
204
|
+
|
|
205
|
+
except NoCredentialsError:
|
|
206
|
+
logger.error("AWS credentials not found. Please configure AWS credentials.")
|
|
207
|
+
logger.error("Options: 1) AWS CLI: 'aws configure', 2) Environment variables, 3) Pass credentials directly")
|
|
208
|
+
raise RuntimeError(
|
|
209
|
+
"Failed to initialize client: No AWS credentials found. "
|
|
210
|
+
"Run 'aws configure' or set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables."
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
except ProfileNotFound:
|
|
214
|
+
logger.error(f"AWS profile '{aws_profile}' not found")
|
|
215
|
+
raise RuntimeError(
|
|
216
|
+
f"Failed to initialize client: AWS profile '{aws_profile}' not found. "
|
|
217
|
+
f"Available profiles can be found in ~/.aws/credentials"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
except NoRegionError:
|
|
221
|
+
logger.error("No AWS region specified")
|
|
222
|
+
raise RuntimeError(
|
|
223
|
+
"Failed to initialize client: No AWS region specified. "
|
|
224
|
+
"Provide region parameter or set AWS_DEFAULT_REGION environment variable."
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
except ClientError as e:
|
|
228
|
+
error_code = e.response.get("Error", {}).get("Code", "Unknown")
|
|
229
|
+
error_msg = e.response.get("Error", {}).get("Message", str(e))
|
|
230
|
+
logger.error(f"AWS Client Error [{error_code}]: {error_msg}")
|
|
231
|
+
raise RuntimeError(f"Failed to initialize client: AWS error [{error_code}]: {error_msg}")
|
|
232
|
+
|
|
233
|
+
except Exception as e:
|
|
234
|
+
logger.error(f"Error during client initialization: {e}")
|
|
235
|
+
raise RuntimeError(f"Failed to initialize client: {e}")
|
|
236
|
+
|
|
237
|
+
def _complete_mfa_challenge(self, mfa_code: str) -> bool:
|
|
238
|
+
"""Complete MFA challenge."""
|
|
239
|
+
try:
|
|
240
|
+
if not self._mfa_tokens:
|
|
241
|
+
logger.error("No MFA session tokens available")
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
challenge_name = self._mfa_tokens.get("ChallengeName", "SMS_MFA")
|
|
245
|
+
|
|
246
|
+
if "SOFTWARE_TOKEN" in challenge_name:
|
|
247
|
+
self.cognito_client.respond_to_software_token_mfa_challenge(code=mfa_code, mfa_tokens=self._mfa_tokens)
|
|
248
|
+
else:
|
|
249
|
+
self.cognito_client.respond_to_sms_mfa_challenge(code=mfa_code, mfa_tokens=self._mfa_tokens)
|
|
250
|
+
|
|
251
|
+
logger.info("✅ MFA challenge successful")
|
|
252
|
+
return True
|
|
253
|
+
|
|
254
|
+
except Exception as e:
|
|
255
|
+
logger.error(f"MFA challenge failed: {e}")
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
def _check_for_mfa_challenges(self, response: dict, username: str) -> bool:
|
|
259
|
+
"""Check and handle MFA challenges in admin auth response."""
|
|
260
|
+
if "ChallengeName" in response:
|
|
261
|
+
challenge = response["ChallengeName"]
|
|
262
|
+
|
|
263
|
+
if challenge == "MFA_SETUP":
|
|
264
|
+
logger.error("MFA setup required")
|
|
265
|
+
return False
|
|
266
|
+
|
|
267
|
+
elif challenge == "SMS_MFA" or challenge == "SOFTWARE_TOKEN_MFA":
|
|
268
|
+
mfa_code = input("Enter MFA code: ")
|
|
269
|
+
_ = self.boto_cognito_admin_client.admin_respond_to_auth_challenge(
|
|
270
|
+
UserPoolId=self.user_pool_id,
|
|
271
|
+
ClientId=self.client_id,
|
|
272
|
+
ChallengeName=challenge,
|
|
273
|
+
Session=response["Session"],
|
|
274
|
+
ChallengeResponses={
|
|
275
|
+
"USERNAME": username,
|
|
276
|
+
"SMS_MFA_CODE" if challenge == "SMS_MFA" else "SOFTWARE_TOKEN_MFA_CODE": mfa_code,
|
|
277
|
+
},
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
elif challenge == "NEW_PASSWORD_REQUIRED":
|
|
281
|
+
new_password = getpass("Enter new password: ")
|
|
282
|
+
_ = self.boto_cognito_admin_client.admin_respond_to_auth_challenge(
|
|
283
|
+
UserPoolId=self.user_pool_id,
|
|
284
|
+
ClientId=self.client_id,
|
|
285
|
+
ChallengeName=challenge,
|
|
286
|
+
Session=response["Session"],
|
|
287
|
+
ChallengeResponses={"USERNAME": username, "NEW_PASSWORD": new_password},
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
return False
|
|
291
|
+
|
|
292
|
+
def _check_user_in_admins_group(self, id_token: str) -> None:
|
|
293
|
+
"""
|
|
294
|
+
Check if user belongs to the admin group.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
id_token: JWT ID token
|
|
298
|
+
|
|
299
|
+
Raises:
|
|
300
|
+
PermissionError: If user is not in admin group
|
|
301
|
+
"""
|
|
302
|
+
claims = jwt.decode(id_token, options={"verify_signature": False})
|
|
303
|
+
groups = claims.get("cognito:groups", [])
|
|
304
|
+
|
|
305
|
+
if self.admin_group_name not in groups:
|
|
306
|
+
raise PermissionError(f"User is not in {self.admin_group_name} group")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Abstract authentication provider interface."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AuthenticationProvider(ABC):
|
|
8
|
+
"""Abstract base class for authentication providers."""
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def authenticate(self, username: str, password: str) -> bool:
|
|
12
|
+
"""
|
|
13
|
+
Authenticate a user with username and password.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
username: User's username or email
|
|
17
|
+
password: User's password
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
True if authentication successful, False otherwise
|
|
21
|
+
"""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def get_id_token(self) -> Optional[str]:
|
|
26
|
+
"""
|
|
27
|
+
Get the ID token from the last successful authentication.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
ID token string if authenticated, None otherwise
|
|
31
|
+
"""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def is_authenticated(self) -> bool:
|
|
36
|
+
"""
|
|
37
|
+
Check if the provider is currently authenticated.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
True if authenticated, False otherwise
|
|
41
|
+
"""
|
|
42
|
+
pass
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""CLI command handlers for Amplify Excel Migrator."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from amplify_excel_migrator.client import AmplifyClient
|
|
7
|
+
from amplify_excel_migrator.core import ConfigManager
|
|
8
|
+
from amplify_excel_migrator.schema import FieldParser
|
|
9
|
+
from amplify_excel_migrator.data import ExcelReader, DataTransformer
|
|
10
|
+
from amplify_excel_migrator.migration import (
|
|
11
|
+
FailureTracker,
|
|
12
|
+
ProgressReporter,
|
|
13
|
+
BatchUploader,
|
|
14
|
+
MigrationOrchestrator,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def cmd_show(args=None):
|
|
19
|
+
print(
|
|
20
|
+
"""
|
|
21
|
+
╔════════════════════════════════════════════════════╗
|
|
22
|
+
║ Amplify Migrator - Current Configuration ║
|
|
23
|
+
╚════════════════════════════════════════════════════╝
|
|
24
|
+
"""
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
config_manager = ConfigManager()
|
|
28
|
+
cached_config = config_manager.load()
|
|
29
|
+
|
|
30
|
+
if not cached_config:
|
|
31
|
+
print("\n❌ No configuration found!")
|
|
32
|
+
print("💡 Run 'amplify-migrator config' first to set up your configuration.")
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
print("\n📋 Cached Configuration:")
|
|
36
|
+
print("-" * 54)
|
|
37
|
+
print(f"Excel file path: {cached_config.get('excel_path', 'N/A')}")
|
|
38
|
+
print(f"API endpoint: {cached_config.get('api_endpoint', 'N/A')}")
|
|
39
|
+
print(f"AWS Region: {cached_config.get('region', 'N/A')}")
|
|
40
|
+
print(f"User Pool ID: {cached_config.get('user_pool_id', 'N/A')}")
|
|
41
|
+
print(f"Client ID: {cached_config.get('client_id', 'N/A')}")
|
|
42
|
+
print(f"Admin Username: {cached_config.get('username', 'N/A')}")
|
|
43
|
+
print("-" * 54)
|
|
44
|
+
print(f"\n📍 Config location: {config_manager.config_path}")
|
|
45
|
+
print(f"💡 Run 'amplify-migrator config' to update configuration.")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def cmd_config(args=None):
|
|
49
|
+
print(
|
|
50
|
+
"""
|
|
51
|
+
╔════════════════════════════════════════════════════╗
|
|
52
|
+
║ Amplify Migrator - Configuration Setup ║
|
|
53
|
+
╚════════════════════════════════════════════════════╝
|
|
54
|
+
"""
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
config_manager = ConfigManager()
|
|
58
|
+
cached_config = config_manager.load()
|
|
59
|
+
|
|
60
|
+
config = {
|
|
61
|
+
"excel_path": config_manager.prompt_for_value("Excel file path", cached_config.get("excel_path", "")),
|
|
62
|
+
"api_endpoint": config_manager.prompt_for_value(
|
|
63
|
+
"AWS Amplify API endpoint", cached_config.get("api_endpoint", "")
|
|
64
|
+
),
|
|
65
|
+
"region": config_manager.prompt_for_value("AWS Region", cached_config.get("region", "")),
|
|
66
|
+
"user_pool_id": config_manager.prompt_for_value("Cognito User Pool ID", cached_config.get("user_pool_id", "")),
|
|
67
|
+
"client_id": config_manager.prompt_for_value("Cognito Client ID", cached_config.get("client_id", "")),
|
|
68
|
+
"username": config_manager.prompt_for_value("Admin Username", cached_config.get("username", "")),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
config_manager.save(config)
|
|
72
|
+
print("\n✅ Configuration saved successfully!")
|
|
73
|
+
print(f"💡 You can now run 'amplify-migrator migrate' to start the migration.")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def cmd_migrate(args=None):
|
|
77
|
+
print(
|
|
78
|
+
"""
|
|
79
|
+
╔════════════════════════════════════════════════════╗
|
|
80
|
+
║ Migrator Tool for Amplify ║
|
|
81
|
+
╠════════════════════════════════════════════════════╣
|
|
82
|
+
║ This tool requires admin privileges to execute ║
|
|
83
|
+
╚════════════════════════════════════════════════════╝
|
|
84
|
+
"""
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
config_manager = ConfigManager()
|
|
88
|
+
cached_config = config_manager.load()
|
|
89
|
+
|
|
90
|
+
if not cached_config:
|
|
91
|
+
print("\n❌ No configuration found!")
|
|
92
|
+
print("💡 Run 'amplify-migrator config' first to set up your configuration.")
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
|
|
95
|
+
excel_path = config_manager.get_or_prompt("excel_path", "Excel file path", "data.xlsx")
|
|
96
|
+
api_endpoint = config_manager.get_or_prompt("api_endpoint", "AWS Amplify API endpoint")
|
|
97
|
+
region = config_manager.get_or_prompt("region", "AWS Region", "us-east-1")
|
|
98
|
+
user_pool_id = config_manager.get_or_prompt("user_pool_id", "Cognito User Pool ID")
|
|
99
|
+
client_id = config_manager.get_or_prompt("client_id", "Cognito Client ID")
|
|
100
|
+
username = config_manager.get_or_prompt("username", "Admin Username")
|
|
101
|
+
|
|
102
|
+
print("\n🔐 Authentication:")
|
|
103
|
+
print("-" * 54)
|
|
104
|
+
password = config_manager.prompt_for_value("Admin Password", secret=True)
|
|
105
|
+
|
|
106
|
+
from amplify_excel_migrator.auth import CognitoAuthProvider
|
|
107
|
+
|
|
108
|
+
auth_provider = CognitoAuthProvider(
|
|
109
|
+
user_pool_id=user_pool_id,
|
|
110
|
+
client_id=client_id,
|
|
111
|
+
region=region,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
amplify_client = AmplifyClient(
|
|
115
|
+
api_endpoint=api_endpoint,
|
|
116
|
+
auth_provider=auth_provider,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if not auth_provider.authenticate(username, password):
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
excel_reader = ExcelReader(excel_path)
|
|
123
|
+
field_parser = FieldParser()
|
|
124
|
+
data_transformer = DataTransformer(field_parser)
|
|
125
|
+
failure_tracker = FailureTracker()
|
|
126
|
+
progress_reporter = ProgressReporter()
|
|
127
|
+
batch_uploader = BatchUploader(amplify_client)
|
|
128
|
+
|
|
129
|
+
orchestrator = MigrationOrchestrator(
|
|
130
|
+
excel_reader=excel_reader,
|
|
131
|
+
data_transformer=data_transformer,
|
|
132
|
+
amplify_client=amplify_client,
|
|
133
|
+
failure_tracker=failure_tracker,
|
|
134
|
+
progress_reporter=progress_reporter,
|
|
135
|
+
batch_uploader=batch_uploader,
|
|
136
|
+
field_parser=field_parser,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
orchestrator.run()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def main():
|
|
143
|
+
parser = argparse.ArgumentParser(
|
|
144
|
+
description="Amplify Excel Migrator - Migrate Excel data to AWS Amplify GraphQL API",
|
|
145
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
149
|
+
|
|
150
|
+
config_parser = subparsers.add_parser("config", help="Configure the migration tool")
|
|
151
|
+
config_parser.set_defaults(func=cmd_config)
|
|
152
|
+
|
|
153
|
+
show_parser = subparsers.add_parser("show", help="Show current configuration")
|
|
154
|
+
show_parser.set_defaults(func=cmd_show)
|
|
155
|
+
|
|
156
|
+
migrate_parser = subparsers.add_parser("migrate", help="Run the migration")
|
|
157
|
+
migrate_parser.set_defaults(func=cmd_migrate)
|
|
158
|
+
|
|
159
|
+
args = parser.parse_args()
|
|
160
|
+
|
|
161
|
+
if args.command is None:
|
|
162
|
+
parser.print_help()
|
|
163
|
+
sys.exit(1)
|
|
164
|
+
|
|
165
|
+
args.func(args)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Dict, Any, Optional, List
|
|
3
|
+
|
|
4
|
+
from amplify_excel_migrator.graphql import GraphQLClient, QueryExecutor
|
|
5
|
+
from amplify_excel_migrator.auth import AuthenticationProvider
|
|
6
|
+
|
|
7
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AmplifyClient:
|
|
12
|
+
"""
|
|
13
|
+
Simplified client for Amplify GraphQL API operations.
|
|
14
|
+
|
|
15
|
+
Provides a clean interface for schema introspection, foreign key lookup,
|
|
16
|
+
and batch uploading. Delegates to GraphQLClient and QueryExecutor.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, api_endpoint: str, auth_provider: Optional[AuthenticationProvider] = None):
|
|
20
|
+
self.api_endpoint = api_endpoint
|
|
21
|
+
self._auth_provider = auth_provider
|
|
22
|
+
|
|
23
|
+
self._client = GraphQLClient(api_endpoint, auth_provider)
|
|
24
|
+
self._executor = QueryExecutor(self._client, batch_size=20)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def auth_provider(self) -> Optional[AuthenticationProvider]:
|
|
28
|
+
return self._auth_provider
|
|
29
|
+
|
|
30
|
+
@auth_provider.setter
|
|
31
|
+
def auth_provider(self, value: Optional[AuthenticationProvider]):
|
|
32
|
+
self._auth_provider = value
|
|
33
|
+
self._client.auth_provider = value
|
|
34
|
+
|
|
35
|
+
def get_model_structure(self, model_type: str) -> Dict[str, Any]:
|
|
36
|
+
return self._executor.get_model_structure(model_type)
|
|
37
|
+
|
|
38
|
+
def get_primary_field_name(self, model_name: str, parsed_model_structure: Dict[str, Any]) -> tuple[str, bool, str]:
|
|
39
|
+
return self._executor.get_primary_field_name(model_name, parsed_model_structure)
|
|
40
|
+
|
|
41
|
+
def build_foreign_key_lookups(self, df, parsed_model_structure: Dict[str, Any]) -> Dict[str, Dict[str, str]]:
|
|
42
|
+
return self._executor.build_foreign_key_lookups(df, parsed_model_structure)
|
|
43
|
+
|
|
44
|
+
def upload(
|
|
45
|
+
self, records: List[Dict], model_name: str, parsed_model_structure: Dict[str, Any]
|
|
46
|
+
) -> tuple[int, int, List[Dict]]:
|
|
47
|
+
return self._executor.upload(records, model_name, parsed_model_structure)
|