amplify-excel-migrator 1.0.0__py3-none-any.whl → 1.0.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.
Potentially problematic release.
This version of amplify-excel-migrator might be problematic. Click here for more details.
- amplify_client.py +126 -143
- {amplify_excel_migrator-1.0.0.dist-info → amplify_excel_migrator-1.0.1.dist-info}/METADATA +9 -1
- amplify_excel_migrator-1.0.1.dist-info/RECORD +9 -0
- migrator.py +70 -53
- model_field_parser.py +65 -52
- amplify_excel_migrator-1.0.0.dist-info/RECORD +0 -9
- {amplify_excel_migrator-1.0.0.dist-info → amplify_excel_migrator-1.0.1.dist-info}/WHEEL +0 -0
- {amplify_excel_migrator-1.0.0.dist-info → amplify_excel_migrator-1.0.1.dist-info}/entry_points.txt +0 -0
- {amplify_excel_migrator-1.0.0.dist-info → amplify_excel_migrator-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {amplify_excel_migrator-1.0.0.dist-info → amplify_excel_migrator-1.0.1.dist-info}/top_level.txt +0 -0
amplify_client.py
CHANGED
|
@@ -12,7 +12,7 @@ from botocore.exceptions import NoCredentialsError, ProfileNotFound, NoRegionErr
|
|
|
12
12
|
from pycognito import Cognito, MFAChallengeException
|
|
13
13
|
from pycognito.exceptions import ForceChangePasswordException
|
|
14
14
|
|
|
15
|
-
logging.basicConfig(level=logging.INFO, format=
|
|
15
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
|
16
16
|
logger = logging.getLogger(__name__)
|
|
17
17
|
|
|
18
18
|
|
|
@@ -21,11 +21,7 @@ class AmplifyClient:
|
|
|
21
21
|
Client for Amplify GraphQL using ADMIN_USER_PASSWORD_AUTH flow
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
def __init__(self,
|
|
25
|
-
api_endpoint: str,
|
|
26
|
-
user_pool_id: str,
|
|
27
|
-
region: str,
|
|
28
|
-
client_id: str):
|
|
24
|
+
def __init__(self, api_endpoint: str, user_pool_id: str, region: str, client_id: str):
|
|
29
25
|
"""
|
|
30
26
|
Initialize the client
|
|
31
27
|
|
|
@@ -46,7 +42,7 @@ class AmplifyClient:
|
|
|
46
42
|
self.boto_cognito_admin_client = None
|
|
47
43
|
self.id_token = None
|
|
48
44
|
self.mfa_tokens = None
|
|
49
|
-
self.admin_group_name =
|
|
45
|
+
self.admin_group_name = "ADMINS"
|
|
50
46
|
|
|
51
47
|
self.records_cache = {}
|
|
52
48
|
|
|
@@ -55,17 +51,17 @@ class AmplifyClient:
|
|
|
55
51
|
if is_aws_admin:
|
|
56
52
|
if aws_profile:
|
|
57
53
|
session = boto3.Session(profile_name=aws_profile)
|
|
58
|
-
self.boto_cognito_admin_client = session.client(
|
|
54
|
+
self.boto_cognito_admin_client = session.client("cognito-idp", region_name=self.region)
|
|
59
55
|
else:
|
|
60
56
|
# Use default AWS credentials (from ~/.aws/credentials, env vars, or IAM role)
|
|
61
|
-
self.boto_cognito_admin_client = boto3.client(
|
|
57
|
+
self.boto_cognito_admin_client = boto3.client("cognito-idp", region_name=self.region)
|
|
62
58
|
|
|
63
59
|
else:
|
|
64
60
|
self.cognito_client = Cognito(
|
|
65
61
|
user_pool_id=self.user_pool_id,
|
|
66
62
|
client_id=self.client_id,
|
|
67
63
|
user_pool_region=self.region,
|
|
68
|
-
username=username
|
|
64
|
+
username=username,
|
|
69
65
|
)
|
|
70
66
|
|
|
71
67
|
except NoCredentialsError:
|
|
@@ -95,8 +91,8 @@ class AmplifyClient:
|
|
|
95
91
|
raise
|
|
96
92
|
|
|
97
93
|
except ClientError as e:
|
|
98
|
-
error_code = e.response.get(
|
|
99
|
-
error_msg = e.response.get(
|
|
94
|
+
error_code = e.response.get("Error", {}).get("Code", "Unknown")
|
|
95
|
+
error_msg = e.response.get("Error", {}).get("Message", str(e))
|
|
100
96
|
logger.error(f"AWS Client Error [{error_code}]: {error_msg}")
|
|
101
97
|
raise RuntimeError(f"Failed to initialize client: AWS error [{error_code}]: {error_msg}")
|
|
102
98
|
|
|
@@ -124,7 +120,7 @@ class AmplifyClient:
|
|
|
124
120
|
|
|
125
121
|
except MFAChallengeException as e:
|
|
126
122
|
logger.warning("MFA required")
|
|
127
|
-
if hasattr(e,
|
|
123
|
+
if hasattr(e, "get_tokens"):
|
|
128
124
|
self.mfa_tokens = e.get_tokens()
|
|
129
125
|
|
|
130
126
|
mfa_code = input("Enter MFA code: ").strip()
|
|
@@ -170,17 +166,14 @@ class AmplifyClient:
|
|
|
170
166
|
response = self.boto_cognito_admin_client.admin_initiate_auth(
|
|
171
167
|
UserPoolId=self.user_pool_id,
|
|
172
168
|
ClientId=self.client_id,
|
|
173
|
-
AuthFlow=
|
|
174
|
-
AuthParameters={
|
|
175
|
-
'USERNAME': username,
|
|
176
|
-
'PASSWORD': password
|
|
177
|
-
}
|
|
169
|
+
AuthFlow="ADMIN_USER_PASSWORD_AUTH",
|
|
170
|
+
AuthParameters={"USERNAME": username, "PASSWORD": password},
|
|
178
171
|
)
|
|
179
172
|
|
|
180
173
|
self._check_for_mfa_challenges(response, username)
|
|
181
174
|
|
|
182
|
-
if
|
|
183
|
-
self.id_token = response[
|
|
175
|
+
if "AuthenticationResult" in response:
|
|
176
|
+
self.id_token = response["AuthenticationResult"]["IdToken"]
|
|
184
177
|
else:
|
|
185
178
|
logger.error("❌ Authentication failed: No AuthenticationResult in response")
|
|
186
179
|
return False
|
|
@@ -208,18 +201,12 @@ class AmplifyClient:
|
|
|
208
201
|
logger.error("No MFA session tokens available")
|
|
209
202
|
return False
|
|
210
203
|
|
|
211
|
-
challenge_name = self.mfa_tokens.get(
|
|
204
|
+
challenge_name = self.mfa_tokens.get("ChallengeName", "SMS_MFA")
|
|
212
205
|
|
|
213
|
-
if
|
|
214
|
-
self.cognito_client.respond_to_software_token_mfa_challenge(
|
|
215
|
-
code=mfa_code,
|
|
216
|
-
mfa_tokens=self.mfa_tokens
|
|
217
|
-
)
|
|
206
|
+
if "SOFTWARE_TOKEN" in challenge_name:
|
|
207
|
+
self.cognito_client.respond_to_software_token_mfa_challenge(code=mfa_code, mfa_tokens=self.mfa_tokens)
|
|
218
208
|
else:
|
|
219
|
-
self.cognito_client.respond_to_sms_mfa_challenge(
|
|
220
|
-
code=mfa_code,
|
|
221
|
-
mfa_tokens=self.mfa_tokens
|
|
222
|
-
)
|
|
209
|
+
self.cognito_client.respond_to_sms_mfa_challenge(code=mfa_code, mfa_tokens=self.mfa_tokens)
|
|
223
210
|
|
|
224
211
|
logger.info("✅ MFA challenge successful")
|
|
225
212
|
return True
|
|
@@ -235,13 +222,10 @@ class AmplifyClient:
|
|
|
235
222
|
try:
|
|
236
223
|
if not self.boto_cognito_admin_client:
|
|
237
224
|
self.boto_cognito_admin_client(is_aws_admin=True)
|
|
238
|
-
response = self.boto_cognito_admin_client.list_user_pool_clients(
|
|
239
|
-
UserPoolId=self.user_pool_id,
|
|
240
|
-
MaxResults=1
|
|
241
|
-
)
|
|
225
|
+
response = self.boto_cognito_admin_client.list_user_pool_clients(UserPoolId=self.user_pool_id, MaxResults=1)
|
|
242
226
|
|
|
243
|
-
if response[
|
|
244
|
-
client_id = response[
|
|
227
|
+
if response["UserPoolClients"]:
|
|
228
|
+
client_id = response["UserPoolClients"][0]["ClientId"]
|
|
245
229
|
return client_id
|
|
246
230
|
|
|
247
231
|
raise Exception("No User Pool clients found")
|
|
@@ -252,37 +236,34 @@ class AmplifyClient:
|
|
|
252
236
|
raise Exception(f"Failed to get Client ID: {e}")
|
|
253
237
|
|
|
254
238
|
def _check_for_mfa_challenges(self, response, username: str) -> bool:
|
|
255
|
-
if
|
|
256
|
-
challenge = response[
|
|
239
|
+
if "ChallengeName" in response:
|
|
240
|
+
challenge = response["ChallengeName"]
|
|
257
241
|
|
|
258
|
-
if challenge ==
|
|
242
|
+
if challenge == "MFA_SETUP":
|
|
259
243
|
logger.error("MFA setup required")
|
|
260
244
|
return False
|
|
261
245
|
|
|
262
|
-
elif challenge ==
|
|
246
|
+
elif challenge == "SMS_MFA" or challenge == "SOFTWARE_TOKEN_MFA":
|
|
263
247
|
mfa_code = input("Enter MFA code: ")
|
|
264
248
|
_ = self.cognito_client.admin_respond_to_auth_challenge(
|
|
265
249
|
UserPoolId=self.user_pool_id,
|
|
266
250
|
ClientId=self.client_id,
|
|
267
251
|
ChallengeName=challenge,
|
|
268
|
-
Session=response[
|
|
252
|
+
Session=response["Session"],
|
|
269
253
|
ChallengeResponses={
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
254
|
+
"USERNAME": username,
|
|
255
|
+
"SMS_MFA_CODE" if challenge == "SMS_MFA" else "SOFTWARE_TOKEN_MFA_CODE": mfa_code,
|
|
256
|
+
},
|
|
273
257
|
)
|
|
274
258
|
|
|
275
|
-
elif challenge ==
|
|
259
|
+
elif challenge == "NEW_PASSWORD_REQUIRED":
|
|
276
260
|
new_password = getpass("Enter new password: ")
|
|
277
261
|
_ = self.cognito_client.admin_respond_to_auth_challenge(
|
|
278
262
|
UserPoolId=self.user_pool_id,
|
|
279
263
|
ClientId=self.client_id,
|
|
280
264
|
ChallengeName=challenge,
|
|
281
|
-
Session=response[
|
|
282
|
-
ChallengeResponses={
|
|
283
|
-
'USERNAME': username,
|
|
284
|
-
'NEW_PASSWORD': new_password
|
|
285
|
-
}
|
|
265
|
+
Session=response["Session"],
|
|
266
|
+
ChallengeResponses={"USERNAME": username, "NEW_PASSWORD": new_password},
|
|
286
267
|
)
|
|
287
268
|
|
|
288
269
|
return False
|
|
@@ -310,27 +291,17 @@ class AmplifyClient:
|
|
|
310
291
|
if not self.id_token:
|
|
311
292
|
raise Exception("Not authenticated. Call authenticate() first.")
|
|
312
293
|
|
|
313
|
-
headers = {
|
|
314
|
-
'Authorization': self.id_token,
|
|
315
|
-
'Content-Type': 'application/json'
|
|
316
|
-
}
|
|
294
|
+
headers = {"Authorization": self.id_token, "Content-Type": "application/json"}
|
|
317
295
|
|
|
318
|
-
payload = {
|
|
319
|
-
'query': query,
|
|
320
|
-
'variables': variables or {}
|
|
321
|
-
}
|
|
296
|
+
payload = {"query": query, "variables": variables or {}}
|
|
322
297
|
|
|
323
298
|
try:
|
|
324
|
-
response = requests.post(
|
|
325
|
-
self.api_endpoint,
|
|
326
|
-
headers=headers,
|
|
327
|
-
json=payload
|
|
328
|
-
)
|
|
299
|
+
response = requests.post(self.api_endpoint, headers=headers, json=payload)
|
|
329
300
|
|
|
330
301
|
if response.status_code == 200:
|
|
331
302
|
result = response.json()
|
|
332
303
|
|
|
333
|
-
if
|
|
304
|
+
if "errors" in result:
|
|
334
305
|
logger.error(f"GraphQL errors: {result['errors']}")
|
|
335
306
|
return None
|
|
336
307
|
|
|
@@ -340,9 +311,10 @@ class AmplifyClient:
|
|
|
340
311
|
return None
|
|
341
312
|
|
|
342
313
|
except Exception as e:
|
|
343
|
-
if
|
|
314
|
+
if "NameResolutionError" in str(e):
|
|
344
315
|
logger.error(
|
|
345
|
-
f"Connection error: Unable to resolve hostname. Check your internet connection or the API endpoint URL."
|
|
316
|
+
f"Connection error: Unable to resolve hostname. Check your internet connection or the API endpoint URL."
|
|
317
|
+
)
|
|
346
318
|
sys.exit(1)
|
|
347
319
|
else:
|
|
348
320
|
logger.error(f"Request error: {e}")
|
|
@@ -363,22 +335,16 @@ class AmplifyClient:
|
|
|
363
335
|
if not self.id_token:
|
|
364
336
|
raise Exception("Not authenticated. Call authenticate() first.")
|
|
365
337
|
|
|
366
|
-
headers = {
|
|
367
|
-
'Authorization': self.id_token,
|
|
368
|
-
'Content-Type': 'application/json'
|
|
369
|
-
}
|
|
338
|
+
headers = {"Authorization": self.id_token, "Content-Type": "application/json"}
|
|
370
339
|
|
|
371
|
-
payload = {
|
|
372
|
-
'query': query,
|
|
373
|
-
'variables': variables or {}
|
|
374
|
-
}
|
|
340
|
+
payload = {"query": query, "variables": variables or {}}
|
|
375
341
|
|
|
376
342
|
try:
|
|
377
343
|
async with session.post(self.api_endpoint, headers=headers, json=payload) as response:
|
|
378
344
|
if response.status == 200:
|
|
379
345
|
result = await response.json()
|
|
380
346
|
|
|
381
|
-
if
|
|
347
|
+
if "errors" in result:
|
|
382
348
|
logger.error(f"GraphQL errors: {result['errors']}")
|
|
383
349
|
return None
|
|
384
350
|
|
|
@@ -391,8 +357,9 @@ class AmplifyClient:
|
|
|
391
357
|
logger.error(f"Request error: {e}")
|
|
392
358
|
return None
|
|
393
359
|
|
|
394
|
-
async def create_record_async(
|
|
395
|
-
|
|
360
|
+
async def create_record_async(
|
|
361
|
+
self, session: aiohttp.ClientSession, data: Dict, model_name: str, primary_field: str
|
|
362
|
+
) -> Dict | None:
|
|
396
363
|
mutation = f"""
|
|
397
364
|
mutation Create{model_name}($input: Create{model_name}Input!) {{
|
|
398
365
|
create{model_name}(input: $input) {{
|
|
@@ -402,19 +369,25 @@ class AmplifyClient:
|
|
|
402
369
|
}}
|
|
403
370
|
"""
|
|
404
371
|
|
|
405
|
-
result = await self._request_async(session, mutation, {
|
|
372
|
+
result = await self._request_async(session, mutation, {"input": data})
|
|
406
373
|
|
|
407
|
-
if result and
|
|
408
|
-
created = result[
|
|
374
|
+
if result and "data" in result:
|
|
375
|
+
created = result["data"].get(f"create{model_name}")
|
|
409
376
|
if created:
|
|
410
377
|
logger.info(f'Created {model_name} with {primary_field}="{data[primary_field]}" (ID: {created["id"]})')
|
|
411
378
|
return created
|
|
412
379
|
|
|
413
380
|
return None
|
|
414
381
|
|
|
415
|
-
async def check_record_exists_async(
|
|
416
|
-
|
|
417
|
-
|
|
382
|
+
async def check_record_exists_async(
|
|
383
|
+
self,
|
|
384
|
+
session: aiohttp.ClientSession,
|
|
385
|
+
model_name: str,
|
|
386
|
+
primary_field: str,
|
|
387
|
+
value: str,
|
|
388
|
+
is_secondary_index: bool,
|
|
389
|
+
record: Dict,
|
|
390
|
+
) -> Dict | None:
|
|
418
391
|
if is_secondary_index:
|
|
419
392
|
query_name = f"list{model_name}By{primary_field.capitalize()}"
|
|
420
393
|
query = f"""
|
|
@@ -427,11 +400,10 @@ class AmplifyClient:
|
|
|
427
400
|
}}
|
|
428
401
|
"""
|
|
429
402
|
result = await self._request_async(session, query, {primary_field: value})
|
|
430
|
-
if result and
|
|
431
|
-
items = result[
|
|
403
|
+
if result and "data" in result:
|
|
404
|
+
items = result["data"].get(query_name, {}).get("items", [])
|
|
432
405
|
if len(items) > 0:
|
|
433
|
-
logger.error(
|
|
434
|
-
f'Record with {primary_field}="{value}" already exists in {model_name}')
|
|
406
|
+
logger.error(f'Record with {primary_field}="{value}" already exists in {model_name}')
|
|
435
407
|
return None
|
|
436
408
|
else:
|
|
437
409
|
query_name = self._get_list_query_name(model_name)
|
|
@@ -446,21 +418,22 @@ class AmplifyClient:
|
|
|
446
418
|
"""
|
|
447
419
|
filter_input = {primary_field: {"eq": value}}
|
|
448
420
|
result = await self._request_async(session, query, {"filter": filter_input})
|
|
449
|
-
if result and
|
|
450
|
-
items = result[
|
|
421
|
+
if result and "data" in result:
|
|
422
|
+
items = result["data"].get(query_name, {}).get("items", [])
|
|
451
423
|
if len(items) > 0:
|
|
452
|
-
logger.error(
|
|
453
|
-
f'Record with {primary_field}="{value}" already exists in {model_name}')
|
|
424
|
+
logger.error(f'Record with {primary_field}="{value}" already exists in {model_name}')
|
|
454
425
|
return None
|
|
455
426
|
|
|
456
427
|
return record
|
|
457
428
|
|
|
458
|
-
async def upload_batch_async(
|
|
459
|
-
|
|
429
|
+
async def upload_batch_async(
|
|
430
|
+
self, batch: list, model_name: str, primary_field: str, is_secondary_index: bool
|
|
431
|
+
) -> tuple[int, int]:
|
|
460
432
|
async with aiohttp.ClientSession() as session:
|
|
461
433
|
duplicate_checks = [
|
|
462
|
-
self.check_record_exists_async(
|
|
463
|
-
|
|
434
|
+
self.check_record_exists_async(
|
|
435
|
+
session, model_name, primary_field, record[primary_field], is_secondary_index, record
|
|
436
|
+
)
|
|
464
437
|
for record in batch
|
|
465
438
|
]
|
|
466
439
|
check_results = await asyncio.gather(*duplicate_checks, return_exceptions=True)
|
|
@@ -476,8 +449,7 @@ class AmplifyClient:
|
|
|
476
449
|
return 0, len(batch)
|
|
477
450
|
|
|
478
451
|
create_tasks = [
|
|
479
|
-
self.create_record_async(session, record, model_name, primary_field)
|
|
480
|
-
for record in filtered_batch
|
|
452
|
+
self.create_record_async(session, record, model_name, primary_field) for record in filtered_batch
|
|
481
453
|
]
|
|
482
454
|
results = await asyncio.gather(*create_tasks, return_exceptions=True)
|
|
483
455
|
|
|
@@ -514,42 +486,41 @@ class AmplifyClient:
|
|
|
514
486
|
"""
|
|
515
487
|
|
|
516
488
|
response = self._request(query)
|
|
517
|
-
if response and
|
|
518
|
-
return response[
|
|
489
|
+
if response and "data" in response and "__type" in response["data"]:
|
|
490
|
+
return response["data"]["__type"]
|
|
519
491
|
|
|
520
492
|
return {}
|
|
521
493
|
|
|
522
|
-
def get_primary_field_name(self, model_name: str, parsed_model_structure: Dict[str, Any]) ->
|
|
523
|
-
tuple[str, bool]):
|
|
494
|
+
def get_primary_field_name(self, model_name: str, parsed_model_structure: Dict[str, Any]) -> tuple[str, bool]:
|
|
524
495
|
secondary_index = self._get_secondary_index(model_name)
|
|
525
496
|
if secondary_index:
|
|
526
497
|
return secondary_index, True
|
|
527
498
|
|
|
528
|
-
for field in parsed_model_structure[
|
|
529
|
-
if field[
|
|
530
|
-
return field[
|
|
499
|
+
for field in parsed_model_structure["fields"]:
|
|
500
|
+
if field["is_required"] and field["is_scalar"] and field["name"] != "id":
|
|
501
|
+
return field["name"], False
|
|
531
502
|
|
|
532
|
-
logger.error(
|
|
533
|
-
return
|
|
503
|
+
logger.error("No suitable primary field found (required scalar field other than id)")
|
|
504
|
+
return "", False
|
|
534
505
|
|
|
535
506
|
def _get_secondary_index(self, model_name: str) -> str:
|
|
536
507
|
query_structure = self.get_model_structure("Query")
|
|
537
508
|
if not query_structure:
|
|
538
509
|
logger.error("Query type not found in schema")
|
|
539
|
-
return
|
|
510
|
+
return ""
|
|
540
511
|
|
|
541
|
-
query_fields = query_structure[
|
|
512
|
+
query_fields = query_structure["fields"]
|
|
542
513
|
|
|
543
514
|
pattern = f"{model_name}By"
|
|
544
515
|
|
|
545
516
|
for query in query_fields:
|
|
546
|
-
query_name = query[
|
|
517
|
+
query_name = query["name"]
|
|
547
518
|
if pattern in query_name:
|
|
548
519
|
pattern_index = query_name.index(pattern)
|
|
549
|
-
field_name = query_name[pattern_index + len(pattern):]
|
|
550
|
-
return field_name[0].lower() + field_name[1:] if field_name else
|
|
520
|
+
field_name = query_name[pattern_index + len(pattern) :]
|
|
521
|
+
return field_name[0].lower() + field_name[1:] if field_name else ""
|
|
551
522
|
|
|
552
|
-
return
|
|
523
|
+
return ""
|
|
553
524
|
|
|
554
525
|
def _get_list_query_name(self, model_name: str) -> str | None:
|
|
555
526
|
"""Get the correct list query name from the schema (handles pluralization)"""
|
|
@@ -558,7 +529,7 @@ class AmplifyClient:
|
|
|
558
529
|
logger.error("Query type not found in schema")
|
|
559
530
|
return f"list{model_name}s"
|
|
560
531
|
|
|
561
|
-
query_fields = query_structure[
|
|
532
|
+
query_fields = query_structure["fields"]
|
|
562
533
|
candidates = [
|
|
563
534
|
f"list{model_name}s",
|
|
564
535
|
f"list{model_name}es",
|
|
@@ -566,8 +537,8 @@ class AmplifyClient:
|
|
|
566
537
|
]
|
|
567
538
|
|
|
568
539
|
for query in query_fields:
|
|
569
|
-
query_name = query[
|
|
570
|
-
if query_name in candidates and
|
|
540
|
+
query_name = query["name"]
|
|
541
|
+
if query_name in candidates and "By" not in query_name:
|
|
571
542
|
return query_name
|
|
572
543
|
|
|
573
544
|
logger.error(f"No list query found for model {model_name}, tried: {candidates}")
|
|
@@ -585,7 +556,7 @@ class AmplifyClient:
|
|
|
585
556
|
return 0, len(records)
|
|
586
557
|
|
|
587
558
|
for i in range(0, len(records), self.batch_size):
|
|
588
|
-
batch = records[i:i + self.batch_size]
|
|
559
|
+
batch = records[i : i + self.batch_size]
|
|
589
560
|
logger.info(f"Uploading batch {i // self.batch_size + 1} ({len(batch)} items)...")
|
|
590
561
|
|
|
591
562
|
batch_success, batch_error = asyncio.run(
|
|
@@ -595,16 +566,18 @@ class AmplifyClient:
|
|
|
595
566
|
error_count += batch_error
|
|
596
567
|
|
|
597
568
|
logger.info(
|
|
598
|
-
f"Processed batch {i // self.batch_size + 1} of model {model_name}: {success_count} success, {error_count} errors"
|
|
569
|
+
f"Processed batch {i // self.batch_size + 1} of model {model_name}: {success_count} success, {error_count} errors"
|
|
570
|
+
)
|
|
599
571
|
|
|
600
572
|
return success_count, error_count
|
|
601
573
|
|
|
602
|
-
def list_records_by_secondary_index(
|
|
603
|
-
|
|
574
|
+
def list_records_by_secondary_index(
|
|
575
|
+
self, model_name: str, secondary_index: str, value: str = None, fields: list = None
|
|
576
|
+
) -> Dict | None:
|
|
604
577
|
if fields is None:
|
|
605
|
-
fields = [
|
|
578
|
+
fields = ["id", secondary_index]
|
|
606
579
|
|
|
607
|
-
fields_str =
|
|
580
|
+
fields_str = "\n".join(fields)
|
|
608
581
|
|
|
609
582
|
if not value:
|
|
610
583
|
query_name = self._get_list_query_name(model_name)
|
|
@@ -631,17 +604,17 @@ class AmplifyClient:
|
|
|
631
604
|
"""
|
|
632
605
|
result = self._request(query, {secondary_index: value})
|
|
633
606
|
|
|
634
|
-
if result and
|
|
635
|
-
items = result[
|
|
607
|
+
if result and "data" in result:
|
|
608
|
+
items = result["data"].get(query_name, {}).get("items", [])
|
|
636
609
|
return items if items else None
|
|
637
610
|
|
|
638
611
|
return None
|
|
639
612
|
|
|
640
613
|
def get_record_by_id(self, model_name: str, record_id: str, fields: list = None) -> Dict | None:
|
|
641
614
|
if fields is None:
|
|
642
|
-
fields = [
|
|
615
|
+
fields = ["id"]
|
|
643
616
|
|
|
644
|
-
fields_str =
|
|
617
|
+
fields_str = "\n".join(fields)
|
|
645
618
|
|
|
646
619
|
query_name = f"get{model_name}"
|
|
647
620
|
query = f"""
|
|
@@ -654,17 +627,18 @@ class AmplifyClient:
|
|
|
654
627
|
|
|
655
628
|
result = self._request(query, {"id": record_id})
|
|
656
629
|
|
|
657
|
-
if result and
|
|
658
|
-
return result[
|
|
630
|
+
if result and "data" in result:
|
|
631
|
+
return result["data"].get(query_name)
|
|
659
632
|
|
|
660
633
|
return None
|
|
661
634
|
|
|
662
|
-
def get_records_by_field(
|
|
663
|
-
|
|
635
|
+
def get_records_by_field(
|
|
636
|
+
self, model_name: str, field_name: str, value: str = None, fields: list = None
|
|
637
|
+
) -> Dict | None:
|
|
664
638
|
if fields is None:
|
|
665
|
-
fields = [
|
|
639
|
+
fields = ["id", field_name]
|
|
666
640
|
|
|
667
|
-
fields_str =
|
|
641
|
+
fields_str = "\n".join(fields)
|
|
668
642
|
|
|
669
643
|
query_name = self._get_list_query_name(model_name)
|
|
670
644
|
|
|
@@ -689,21 +663,23 @@ class AmplifyClient:
|
|
|
689
663
|
}}
|
|
690
664
|
}}
|
|
691
665
|
"""
|
|
692
|
-
filter_input = {
|
|
693
|
-
field_name: {
|
|
694
|
-
"eq": value
|
|
695
|
-
}
|
|
696
|
-
}
|
|
666
|
+
filter_input = {field_name: {"eq": value}}
|
|
697
667
|
result = self._request(query, {"filter": filter_input})
|
|
698
668
|
|
|
699
|
-
if result and
|
|
700
|
-
items = result[
|
|
669
|
+
if result and "data" in result:
|
|
670
|
+
items = result["data"].get(query_name, {}).get("items", [])
|
|
701
671
|
return items if items else None
|
|
702
672
|
|
|
703
673
|
return None
|
|
704
674
|
|
|
705
|
-
def get_records(
|
|
706
|
-
|
|
675
|
+
def get_records(
|
|
676
|
+
self,
|
|
677
|
+
model_name: str,
|
|
678
|
+
parsed_model_structure: Dict[str, Any] = None,
|
|
679
|
+
primary_field: str = None,
|
|
680
|
+
is_secondary_index: bool = None,
|
|
681
|
+
fields: list = None,
|
|
682
|
+
) -> list | None:
|
|
707
683
|
if model_name in self.records_cache:
|
|
708
684
|
return self.records_cache[model_name]
|
|
709
685
|
|
|
@@ -724,9 +700,16 @@ class AmplifyClient:
|
|
|
724
700
|
self.records_cache[model_name] = records
|
|
725
701
|
return records
|
|
726
702
|
|
|
727
|
-
def get_record(
|
|
728
|
-
|
|
729
|
-
|
|
703
|
+
def get_record(
|
|
704
|
+
self,
|
|
705
|
+
model_name: str,
|
|
706
|
+
parsed_model_structure: Dict[str, Any] = None,
|
|
707
|
+
value: str = None,
|
|
708
|
+
record_id: str = None,
|
|
709
|
+
primary_field: str = None,
|
|
710
|
+
is_secondary_index: bool = None,
|
|
711
|
+
fields: list = None,
|
|
712
|
+
) -> Dict | None:
|
|
730
713
|
if record_id:
|
|
731
714
|
return self.get_record_by_id(model_name, record_id)
|
|
732
715
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: amplify-excel-migrator
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: A CLI tool to migrate Excel data to AWS Amplify
|
|
5
5
|
Home-page: https://github.com/EyalPoly/amplify-excel-migrator
|
|
6
6
|
Author: Eyal Politansky
|
|
@@ -44,6 +44,14 @@ Developed for the MECO project - https://github.com/sworgkh/meco-observations-am
|
|
|
44
44
|
|
|
45
45
|
## Installation
|
|
46
46
|
|
|
47
|
+
### From PyPI (Recommended)
|
|
48
|
+
|
|
49
|
+
Install the latest stable version from PyPI:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install amplify-excel-migrator
|
|
53
|
+
```
|
|
54
|
+
|
|
47
55
|
### From GitHub
|
|
48
56
|
|
|
49
57
|
Install directly from GitHub:
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
amplify_client.py,sha256=bv3iuSRQvykgiU4gSfmGK8QufbnXHcRRtxnZtnR5dck,26211
|
|
2
|
+
migrator.py,sha256=iUrYAmKhwkuKBjayiuo9awN_5-5kEOrqHvHrBm4qQqc,12091
|
|
3
|
+
model_field_parser.py,sha256=1dyfdzq3hIeK81TWAx8XnU6Bk-txt-PCYS_p8ZeQnTM,4482
|
|
4
|
+
amplify_excel_migrator-1.0.1.dist-info/licenses/LICENSE,sha256=i8Sf8mXscGI9l-HTQ5RLQkAJU6Iv5hPYctJksPY70U0,1071
|
|
5
|
+
amplify_excel_migrator-1.0.1.dist-info/METADATA,sha256=uUR_qYCRaOkUPe9ZdY9p0rCgzIeW3VoWX6goGf-0kmE,5675
|
|
6
|
+
amplify_excel_migrator-1.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
amplify_excel_migrator-1.0.1.dist-info/entry_points.txt,sha256=Ifd7YnV4lNbjFbbnjsmlHWiIAfIpiC5POgJtxfSlDT4,51
|
|
8
|
+
amplify_excel_migrator-1.0.1.dist-info/top_level.txt,sha256=C-ffRe3F26GYiM7f6xy-pPvbwnh7Wnieyt-jS-cbdTU,43
|
|
9
|
+
amplify_excel_migrator-1.0.1.dist-info/RECORD,,
|
migrator.py
CHANGED
|
@@ -12,11 +12,11 @@ import pandas as pd
|
|
|
12
12
|
from amplify_client import AmplifyClient
|
|
13
13
|
from model_field_parser import ModelFieldParser
|
|
14
14
|
|
|
15
|
-
logging.basicConfig(level=logging.INFO, format=
|
|
15
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
|
16
16
|
logger = logging.getLogger(__name__)
|
|
17
17
|
|
|
18
|
-
CONFIG_DIR = Path.home() /
|
|
19
|
-
CONFIG_FILE = CONFIG_DIR /
|
|
18
|
+
CONFIG_DIR = Path.home() / ".amplify-migrator"
|
|
19
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class ExcelToAmplifyMigrator:
|
|
@@ -25,8 +25,16 @@ class ExcelToAmplifyMigrator:
|
|
|
25
25
|
self.excel_file_path = excel_file_path
|
|
26
26
|
self.amplify_client = None
|
|
27
27
|
|
|
28
|
-
def init_client(
|
|
29
|
-
|
|
28
|
+
def init_client(
|
|
29
|
+
self,
|
|
30
|
+
api_endpoint: str,
|
|
31
|
+
region: str,
|
|
32
|
+
user_pool_id: str,
|
|
33
|
+
is_aws_admin: bool = False,
|
|
34
|
+
client_id: str = None,
|
|
35
|
+
username: str = None,
|
|
36
|
+
aws_profile: str = None,
|
|
37
|
+
):
|
|
30
38
|
|
|
31
39
|
self.amplify_client = AmplifyClient(
|
|
32
40
|
api_endpoint=api_endpoint,
|
|
@@ -36,8 +44,9 @@ class ExcelToAmplifyMigrator:
|
|
|
36
44
|
)
|
|
37
45
|
|
|
38
46
|
try:
|
|
39
|
-
self.amplify_client.init_cognito_client(
|
|
40
|
-
|
|
47
|
+
self.amplify_client.init_cognito_client(
|
|
48
|
+
is_aws_admin=is_aws_admin, username=username, aws_profile=aws_profile
|
|
49
|
+
)
|
|
41
50
|
|
|
42
51
|
except RuntimeError or Exception:
|
|
43
52
|
sys.exit(1)
|
|
@@ -99,31 +108,32 @@ class ExcelToAmplifyMigrator:
|
|
|
99
108
|
|
|
100
109
|
model_record = {}
|
|
101
110
|
|
|
102
|
-
for field in parsed_model_structure[
|
|
111
|
+
for field in parsed_model_structure["fields"]:
|
|
103
112
|
input = self.parse_input(row, field, parsed_model_structure)
|
|
104
113
|
if input:
|
|
105
|
-
model_record[field[
|
|
114
|
+
model_record[field["name"]] = input
|
|
106
115
|
|
|
107
116
|
return model_record
|
|
108
117
|
|
|
109
118
|
def parse_input(self, row: pd.Series, field: Dict[str, Any], parsed_model_structure: Dict[str, Any]) -> Any:
|
|
110
|
-
field_name = field[
|
|
119
|
+
field_name = field["name"][:-2] if field["is_id"] else field["name"]
|
|
111
120
|
if field_name not in row.index or pd.isna(row[field_name]):
|
|
112
|
-
if field[
|
|
121
|
+
if field["is_required"]:
|
|
113
122
|
raise ValueError(f"Required field '{field_name}' is missing in row {row.name}")
|
|
114
123
|
else:
|
|
115
124
|
return None
|
|
116
125
|
|
|
117
|
-
value = row.get(field[
|
|
118
|
-
if field[
|
|
119
|
-
related_model = (temp := field[
|
|
120
|
-
record = self.amplify_client.get_record(
|
|
121
|
-
|
|
126
|
+
value = row.get(field["name"])
|
|
127
|
+
if field["is_id"]:
|
|
128
|
+
related_model = (temp := field["name"][:-2])[0].upper() + temp[1:]
|
|
129
|
+
record = self.amplify_client.get_record(
|
|
130
|
+
related_model, parsed_model_structure=parsed_model_structure, value=value, fields=["id"]
|
|
131
|
+
)
|
|
122
132
|
if record:
|
|
123
|
-
if record[
|
|
133
|
+
if record["id"] is None and field["is_required"]:
|
|
124
134
|
raise ValueError(f"{related_model}: {value} does not exist")
|
|
125
135
|
else:
|
|
126
|
-
value = record[
|
|
136
|
+
value = record["id"]
|
|
127
137
|
else:
|
|
128
138
|
raise ValueError(f"Error fetching related record {related_model}: {value}")
|
|
129
139
|
|
|
@@ -132,13 +142,13 @@ class ExcelToAmplifyMigrator:
|
|
|
132
142
|
@staticmethod
|
|
133
143
|
def to_camel_case(s: str) -> str:
|
|
134
144
|
# Handle PascalCase
|
|
135
|
-
s_with_spaces = re.sub(r
|
|
145
|
+
s_with_spaces = re.sub(r"(?<!^)(?=[A-Z])", " ", s)
|
|
136
146
|
|
|
137
|
-
parts = re.split(r
|
|
138
|
-
return parts[0].lower() +
|
|
147
|
+
parts = re.split(r"[\s_\-]+", s_with_spaces.strip())
|
|
148
|
+
return parts[0].lower() + "".join(word.capitalize() for word in parts[1:])
|
|
139
149
|
|
|
140
150
|
|
|
141
|
-
def get_config_value(prompt: str, default: str =
|
|
151
|
+
def get_config_value(prompt: str, default: str = "", secret: bool = False) -> str:
|
|
142
152
|
if default:
|
|
143
153
|
prompt = f"{prompt} [{default}]: "
|
|
144
154
|
else:
|
|
@@ -155,9 +165,9 @@ def get_config_value(prompt: str, default: str = '', secret: bool = False) -> st
|
|
|
155
165
|
def save_config(config: Dict[str, str]) -> None:
|
|
156
166
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
157
167
|
|
|
158
|
-
cache_config = {k: v for k, v in config.items() if k not in [
|
|
168
|
+
cache_config = {k: v for k, v in config.items() if k not in ["password", "ADMIN_PASSWORD"]}
|
|
159
169
|
|
|
160
|
-
with open(CONFIG_FILE,
|
|
170
|
+
with open(CONFIG_FILE, "w") as f:
|
|
161
171
|
json.dump(cache_config, f, indent=2)
|
|
162
172
|
|
|
163
173
|
logger.info(f"✅ Configuration saved to {CONFIG_FILE}")
|
|
@@ -168,14 +178,14 @@ def load_cached_config() -> Dict[str, str]:
|
|
|
168
178
|
return {}
|
|
169
179
|
|
|
170
180
|
try:
|
|
171
|
-
with open(CONFIG_FILE,
|
|
181
|
+
with open(CONFIG_FILE, "r") as f:
|
|
172
182
|
return json.load(f)
|
|
173
183
|
except Exception as e:
|
|
174
184
|
logger.warning(f"Failed to load cached config: {e}")
|
|
175
185
|
return {}
|
|
176
186
|
|
|
177
187
|
|
|
178
|
-
def get_cached_or_prompt(key: str, prompt: str, cached_config: Dict, default: str =
|
|
188
|
+
def get_cached_or_prompt(key: str, prompt: str, cached_config: Dict, default: str = "", secret: bool = False) -> str:
|
|
179
189
|
if key in cached_config:
|
|
180
190
|
return cached_config[key]
|
|
181
191
|
|
|
@@ -183,11 +193,13 @@ def get_cached_or_prompt(key: str, prompt: str, cached_config: Dict, default: st
|
|
|
183
193
|
|
|
184
194
|
|
|
185
195
|
def cmd_show(args=None):
|
|
186
|
-
print(
|
|
196
|
+
print(
|
|
197
|
+
"""
|
|
187
198
|
╔════════════════════════════════════════════════════╗
|
|
188
199
|
║ Amplify Migrator - Current Configuration ║
|
|
189
200
|
╚════════════════════════════════════════════════════╝
|
|
190
|
-
"""
|
|
201
|
+
"""
|
|
202
|
+
)
|
|
191
203
|
|
|
192
204
|
cached_config = load_cached_config()
|
|
193
205
|
|
|
@@ -210,18 +222,22 @@ def cmd_show(args=None):
|
|
|
210
222
|
|
|
211
223
|
|
|
212
224
|
def cmd_config(args=None):
|
|
213
|
-
print(
|
|
225
|
+
print(
|
|
226
|
+
"""
|
|
214
227
|
╔════════════════════════════════════════════════════╗
|
|
215
228
|
║ Amplify Migrator - Configuration Setup ║
|
|
216
229
|
╚════════════════════════════════════════════════════╝
|
|
217
|
-
"""
|
|
230
|
+
"""
|
|
231
|
+
)
|
|
218
232
|
|
|
219
|
-
config = {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
233
|
+
config = {
|
|
234
|
+
"excel_path": get_config_value("Excel file path", "data.xlsx"),
|
|
235
|
+
"api_endpoint": get_config_value("AWS Amplify API endpoint"),
|
|
236
|
+
"region": get_config_value("AWS Region", "us-east-1"),
|
|
237
|
+
"user_pool_id": get_config_value("Cognito User Pool ID"),
|
|
238
|
+
"client_id": get_config_value("Cognito Client ID"),
|
|
239
|
+
"username": get_config_value("Admin Username"),
|
|
240
|
+
}
|
|
225
241
|
|
|
226
242
|
save_config(config)
|
|
227
243
|
print("\n✅ Configuration saved successfully!")
|
|
@@ -229,13 +245,15 @@ def cmd_config(args=None):
|
|
|
229
245
|
|
|
230
246
|
|
|
231
247
|
def cmd_migrate(args=None):
|
|
232
|
-
print(
|
|
248
|
+
print(
|
|
249
|
+
"""
|
|
233
250
|
╔════════════════════════════════════════════════════╗
|
|
234
251
|
║ Migrator Tool for Amplify ║
|
|
235
252
|
╠════════════════════════════════════════════════════╣
|
|
236
253
|
║ This tool requires admin privileges to execute ║
|
|
237
254
|
╚════════════════════════════════════════════════════╝
|
|
238
|
-
"""
|
|
255
|
+
"""
|
|
256
|
+
)
|
|
239
257
|
|
|
240
258
|
cached_config = load_cached_config()
|
|
241
259
|
|
|
@@ -244,20 +262,19 @@ def cmd_migrate(args=None):
|
|
|
244
262
|
print("💡 Run 'amplify-migrator config' first to set up your configuration.")
|
|
245
263
|
sys.exit(1)
|
|
246
264
|
|
|
247
|
-
excel_path = get_cached_or_prompt(
|
|
248
|
-
api_endpoint = get_cached_or_prompt(
|
|
249
|
-
region = get_cached_or_prompt(
|
|
250
|
-
user_pool_id = get_cached_or_prompt(
|
|
251
|
-
client_id = get_cached_or_prompt(
|
|
252
|
-
username = get_cached_or_prompt(
|
|
265
|
+
excel_path = get_cached_or_prompt("excel_path", "Excel file path", cached_config, "data.xlsx")
|
|
266
|
+
api_endpoint = get_cached_or_prompt("api_endpoint", "AWS Amplify API endpoint", cached_config)
|
|
267
|
+
region = get_cached_or_prompt("region", "AWS Region", cached_config, "us-east-1")
|
|
268
|
+
user_pool_id = get_cached_or_prompt("user_pool_id", "Cognito User Pool ID", cached_config)
|
|
269
|
+
client_id = get_cached_or_prompt("client_id", "Cognito Client ID", cached_config)
|
|
270
|
+
username = get_cached_or_prompt("username", "Admin Username", cached_config)
|
|
253
271
|
|
|
254
272
|
print("\n🔐 Authentication:")
|
|
255
273
|
print("-" * 54)
|
|
256
|
-
password = get_config_value(
|
|
274
|
+
password = get_config_value("ADMIN_PASSWORD", "Admin Password", secret=True)
|
|
257
275
|
|
|
258
276
|
migrator = ExcelToAmplifyMigrator(excel_path)
|
|
259
|
-
migrator.init_client(api_endpoint, region, user_pool_id, client_id=client_id,
|
|
260
|
-
username=username)
|
|
277
|
+
migrator.init_client(api_endpoint, region, user_pool_id, client_id=client_id, username=username)
|
|
261
278
|
if not migrator.authenticate(username, password):
|
|
262
279
|
return
|
|
263
280
|
|
|
@@ -266,19 +283,19 @@ def cmd_migrate(args=None):
|
|
|
266
283
|
|
|
267
284
|
def main():
|
|
268
285
|
parser = argparse.ArgumentParser(
|
|
269
|
-
description=
|
|
270
|
-
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
286
|
+
description="Amplify Excel Migrator - Migrate Excel data to AWS Amplify GraphQL API",
|
|
287
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
271
288
|
)
|
|
272
289
|
|
|
273
|
-
subparsers = parser.add_subparsers(dest=
|
|
290
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
274
291
|
|
|
275
|
-
config_parser = subparsers.add_parser(
|
|
292
|
+
config_parser = subparsers.add_parser("config", help="Configure the migration tool")
|
|
276
293
|
config_parser.set_defaults(func=cmd_config)
|
|
277
294
|
|
|
278
|
-
show_parser = subparsers.add_parser(
|
|
295
|
+
show_parser = subparsers.add_parser("show", help="Show current configuration")
|
|
279
296
|
show_parser.set_defaults(func=cmd_show)
|
|
280
297
|
|
|
281
|
-
migrate_parser = subparsers.add_parser(
|
|
298
|
+
migrate_parser = subparsers.add_parser("migrate", help="Run the migration")
|
|
282
299
|
migrate_parser.set_defaults(func=cmd_migrate)
|
|
283
300
|
|
|
284
301
|
args = parser.parse_args()
|
model_field_parser.py
CHANGED
|
@@ -6,47 +6,60 @@ class ModelFieldParser:
|
|
|
6
6
|
|
|
7
7
|
def __init__(self):
|
|
8
8
|
self.scalar_types = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
"String",
|
|
10
|
+
"Int",
|
|
11
|
+
"Float",
|
|
12
|
+
"Boolean",
|
|
13
|
+
"AWSDate",
|
|
14
|
+
"AWSTime",
|
|
15
|
+
"AWSDateTime",
|
|
16
|
+
"AWSTimestamp",
|
|
17
|
+
"AWSEmail",
|
|
18
|
+
"AWSJSON",
|
|
19
|
+
"AWSURL",
|
|
20
|
+
"AWSPhone",
|
|
21
|
+
"AWSIPAddress",
|
|
12
22
|
}
|
|
13
|
-
self.metadata_fields = {
|
|
23
|
+
self.metadata_fields = {"id", "createdAt", "updatedAt", "owner"}
|
|
14
24
|
|
|
15
25
|
def parse_model_structure(self, introspection_result: Dict) -> Dict[str, Any]:
|
|
16
|
-
if
|
|
17
|
-
type_data = introspection_result[
|
|
26
|
+
if "data" in introspection_result and "__type" in introspection_result["data"]:
|
|
27
|
+
type_data = introspection_result["data"]["__type"]
|
|
18
28
|
else:
|
|
19
29
|
type_data = introspection_result
|
|
20
30
|
|
|
21
31
|
model_info = {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
"name": type_data.get("name"),
|
|
33
|
+
"kind": type_data.get("kind"),
|
|
34
|
+
"description": type_data.get("description"),
|
|
35
|
+
"fields": [],
|
|
26
36
|
}
|
|
27
37
|
|
|
28
|
-
if type_data.get(
|
|
29
|
-
for field in type_data[
|
|
38
|
+
if type_data.get("fields"):
|
|
39
|
+
for field in type_data["fields"]:
|
|
30
40
|
parsed_field = self._parse_field(field)
|
|
31
|
-
model_info[
|
|
41
|
+
model_info["fields"].append(parsed_field) if parsed_field else None
|
|
32
42
|
|
|
33
43
|
return model_info
|
|
34
44
|
|
|
35
45
|
def _parse_field(self, field: Dict) -> Dict[str, Any]:
|
|
36
|
-
base_type = self._get_base_type_name(field.get(
|
|
37
|
-
if
|
|
38
|
-
|
|
46
|
+
base_type = self._get_base_type_name(field.get("type", {}))
|
|
47
|
+
if (
|
|
48
|
+
"Connection" in base_type
|
|
49
|
+
or field.get("name") in self.metadata_fields
|
|
50
|
+
or self._get_type_kind(field.get("type", {})) in ["OBJECT", "INTERFACE"]
|
|
51
|
+
):
|
|
39
52
|
return {}
|
|
40
53
|
|
|
41
54
|
field_info = {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
"name": field.get("name"),
|
|
56
|
+
"description": field.get("description"),
|
|
57
|
+
"type": base_type,
|
|
58
|
+
"is_required": self._is_required_field(field.get("type", {})),
|
|
59
|
+
"is_list": self._is_list_type(field.get("type", {})),
|
|
60
|
+
"is_scalar": base_type in self.scalar_types,
|
|
61
|
+
"is_id": base_type == "ID",
|
|
62
|
+
"is_enum": field.get("type", {}).get("kind") == "ENUM",
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
return field_info
|
|
@@ -57,17 +70,17 @@ class ModelFieldParser:
|
|
|
57
70
|
"""
|
|
58
71
|
|
|
59
72
|
if not type_obj:
|
|
60
|
-
return {
|
|
73
|
+
return {"name": "Unknown", "kind": "UNKNOWN"}
|
|
61
74
|
|
|
62
75
|
type_info = {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
76
|
+
"name": type_obj.get("name"),
|
|
77
|
+
"kind": type_obj.get("kind"),
|
|
78
|
+
"full_type": self._get_full_type_string(type_obj),
|
|
66
79
|
}
|
|
67
80
|
|
|
68
81
|
# If there's nested type info (NON_NULL, LIST), include it
|
|
69
|
-
if type_obj.get(
|
|
70
|
-
type_info[
|
|
82
|
+
if type_obj.get("ofType"):
|
|
83
|
+
type_info["of_type"] = self._parse_type(type_obj["ofType"])
|
|
71
84
|
|
|
72
85
|
return type_info
|
|
73
86
|
|
|
@@ -77,20 +90,20 @@ class ModelFieldParser:
|
|
|
77
90
|
"""
|
|
78
91
|
|
|
79
92
|
if not type_obj:
|
|
80
|
-
return
|
|
93
|
+
return "Unknown"
|
|
81
94
|
|
|
82
|
-
if type_obj.get(
|
|
83
|
-
return type_obj[
|
|
95
|
+
if type_obj.get("name"):
|
|
96
|
+
return type_obj["name"]
|
|
84
97
|
|
|
85
|
-
if type_obj[
|
|
86
|
-
inner = self._get_full_type_string(type_obj.get(
|
|
98
|
+
if type_obj["kind"] == "NON_NULL":
|
|
99
|
+
inner = self._get_full_type_string(type_obj.get("ofType", {}))
|
|
87
100
|
return f"{inner}!"
|
|
88
101
|
|
|
89
|
-
if type_obj[
|
|
90
|
-
inner = self._get_full_type_string(type_obj.get(
|
|
102
|
+
if type_obj["kind"] == "LIST":
|
|
103
|
+
inner = self._get_full_type_string(type_obj.get("ofType", {}))
|
|
91
104
|
return f"[{inner}]"
|
|
92
105
|
|
|
93
|
-
return type_obj.get(
|
|
106
|
+
return type_obj.get("kind", "Unknown")
|
|
94
107
|
|
|
95
108
|
def _get_base_type_name(self, type_obj: Dict) -> str:
|
|
96
109
|
"""
|
|
@@ -98,37 +111,37 @@ class ModelFieldParser:
|
|
|
98
111
|
"""
|
|
99
112
|
|
|
100
113
|
if not type_obj:
|
|
101
|
-
return
|
|
114
|
+
return "Unknown"
|
|
102
115
|
|
|
103
|
-
if type_obj.get(
|
|
104
|
-
return type_obj[
|
|
116
|
+
if type_obj.get("name"):
|
|
117
|
+
return type_obj["name"]
|
|
105
118
|
|
|
106
|
-
if type_obj.get(
|
|
107
|
-
return self._get_base_type_name(type_obj[
|
|
119
|
+
if type_obj.get("ofType"):
|
|
120
|
+
return self._get_base_type_name(type_obj["ofType"])
|
|
108
121
|
|
|
109
|
-
return
|
|
122
|
+
return "Unknown"
|
|
110
123
|
|
|
111
124
|
def _get_type_kind(self, type_obj: Dict) -> str:
|
|
112
125
|
if not type_obj:
|
|
113
|
-
return
|
|
126
|
+
return "UNKNOWN"
|
|
114
127
|
|
|
115
|
-
if type_obj[
|
|
116
|
-
return self._get_type_kind(type_obj[
|
|
128
|
+
if type_obj["kind"] in ["NON_NULL", "LIST"] and type_obj.get("ofType"):
|
|
129
|
+
return self._get_type_kind(type_obj["ofType"])
|
|
117
130
|
|
|
118
|
-
return type_obj.get(
|
|
131
|
+
return type_obj.get("kind", "UNKNOWN")
|
|
119
132
|
|
|
120
133
|
@staticmethod
|
|
121
134
|
def _is_required_field(type_obj: Dict) -> bool:
|
|
122
|
-
return type_obj and type_obj.get(
|
|
135
|
+
return type_obj and type_obj.get("kind") == "NON_NULL"
|
|
123
136
|
|
|
124
137
|
def _is_list_type(self, type_obj: Dict) -> bool:
|
|
125
138
|
if not type_obj:
|
|
126
139
|
return False
|
|
127
140
|
|
|
128
|
-
if type_obj[
|
|
141
|
+
if type_obj["kind"] == "LIST":
|
|
129
142
|
return True
|
|
130
143
|
|
|
131
|
-
if type_obj.get(
|
|
132
|
-
return self._is_list_type(type_obj[
|
|
144
|
+
if type_obj.get("ofType"):
|
|
145
|
+
return self._is_list_type(type_obj["ofType"])
|
|
133
146
|
|
|
134
147
|
return False
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
amplify_client.py,sha256=fZ9LKCzEMOY3upNGaPCEuofVXI8IN2JemQFxTsQN6q8,26916
|
|
2
|
-
migrator.py,sha256=03YO5n6jymPA5bDr1Ks6FeSwi7bb8lZ_9LBjFSJCDi8,12081
|
|
3
|
-
model_field_parser.py,sha256=u7f55WYg6eRS-_iyq9swzxntqyUQMH9vaX3j-RUG76w,4328
|
|
4
|
-
amplify_excel_migrator-1.0.0.dist-info/licenses/LICENSE,sha256=i8Sf8mXscGI9l-HTQ5RLQkAJU6Iv5hPYctJksPY70U0,1071
|
|
5
|
-
amplify_excel_migrator-1.0.0.dist-info/METADATA,sha256=7qXRVir9VJq6NEM7B1WswHe0jx4YF95iutb2nPWMLog,5552
|
|
6
|
-
amplify_excel_migrator-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
-
amplify_excel_migrator-1.0.0.dist-info/entry_points.txt,sha256=Ifd7YnV4lNbjFbbnjsmlHWiIAfIpiC5POgJtxfSlDT4,51
|
|
8
|
-
amplify_excel_migrator-1.0.0.dist-info/top_level.txt,sha256=C-ffRe3F26GYiM7f6xy-pPvbwnh7Wnieyt-jS-cbdTU,43
|
|
9
|
-
amplify_excel_migrator-1.0.0.dist-info/RECORD,,
|
|
File without changes
|
{amplify_excel_migrator-1.0.0.dist-info → amplify_excel_migrator-1.0.1.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{amplify_excel_migrator-1.0.0.dist-info → amplify_excel_migrator-1.0.1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{amplify_excel_migrator-1.0.0.dist-info → amplify_excel_migrator-1.0.1.dist-info}/top_level.txt
RENAMED
|
File without changes
|