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 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='%(asctime)s - %(levelname)s - %(message)s')
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 = 'ADMINS'
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('cognito-idp', region_name=self.region)
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('cognito-idp', region_name=self.region)
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('Error', {}).get('Code', 'Unknown')
99
- error_msg = e.response.get('Error', {}).get('Message', str(e))
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, 'get_tokens'):
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='ADMIN_USER_PASSWORD_AUTH',
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 'AuthenticationResult' in response:
183
- self.id_token = response['AuthenticationResult']['IdToken']
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('ChallengeName', 'SMS_MFA')
204
+ challenge_name = self.mfa_tokens.get("ChallengeName", "SMS_MFA")
212
205
 
213
- if 'SOFTWARE_TOKEN' in challenge_name:
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['UserPoolClients']:
244
- client_id = response['UserPoolClients'][0]['ClientId']
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 'ChallengeName' in response:
256
- challenge = response['ChallengeName']
239
+ if "ChallengeName" in response:
240
+ challenge = response["ChallengeName"]
257
241
 
258
- if challenge == 'MFA_SETUP':
242
+ if challenge == "MFA_SETUP":
259
243
  logger.error("MFA setup required")
260
244
  return False
261
245
 
262
- elif challenge == 'SMS_MFA' or challenge == 'SOFTWARE_TOKEN_MFA':
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['Session'],
252
+ Session=response["Session"],
269
253
  ChallengeResponses={
270
- 'USERNAME': username,
271
- 'SMS_MFA_CODE' if challenge == 'SMS_MFA' else 'SOFTWARE_TOKEN_MFA_CODE': mfa_code
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 == 'NEW_PASSWORD_REQUIRED':
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['Session'],
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 'errors' in result:
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 'NameResolutionError' in str(e):
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 'errors' in result:
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(self, session: aiohttp.ClientSession, data: Dict, model_name: str,
395
- primary_field: str) -> Dict | None:
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, {'input': data})
372
+ result = await self._request_async(session, mutation, {"input": data})
406
373
 
407
- if result and 'data' in result:
408
- created = result['data'].get(f'create{model_name}')
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(self, session: aiohttp.ClientSession, model_name: str,
416
- primary_field: str, value: str, is_secondary_index: bool,
417
- record: Dict) -> Dict | None:
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 'data' in result:
431
- items = result['data'].get(query_name, {}).get('items', [])
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 'data' in result:
450
- items = result['data'].get(query_name, {}).get('items', [])
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(self, batch: list, model_name: str, primary_field: str,
459
- is_secondary_index: bool) -> tuple[int, int]:
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(session, model_name, primary_field,
463
- record[primary_field], is_secondary_index, record)
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 'data' in response and '__type' in response['data']:
518
- return response['data']['__type']
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['fields']:
529
- if field['is_required'] and field['is_scalar'] and field['name'] != 'id':
530
- return field['name'], False
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('No suitable primary field found (required scalar field other than id)')
533
- return '', False
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['fields']
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['name']
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['fields']
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['name']
570
- if query_name in candidates and 'By' not in query_name:
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(self, model_name: str, secondary_index: str, value: str = None,
603
- fields: list = None) -> Dict | None:
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 = ['id', secondary_index]
578
+ fields = ["id", secondary_index]
606
579
 
607
- fields_str = '\n'.join(fields)
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 'data' in result:
635
- items = result['data'].get(query_name, {}).get('items', [])
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 = ['id']
615
+ fields = ["id"]
643
616
 
644
- fields_str = '\n'.join(fields)
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 'data' in result:
658
- return result['data'].get(query_name)
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(self, model_name: str, field_name: str, value: str = None,
663
- fields: list = None) -> Dict | None:
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 = ['id', field_name]
639
+ fields = ["id", field_name]
666
640
 
667
- fields_str = '\n'.join(fields)
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 'data' in result:
700
- items = result['data'].get(query_name, {}).get('items', [])
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(self, model_name: str, parsed_model_structure: Dict[str, Any] = None, primary_field: str = None,
706
- is_secondary_index: bool = None, fields: list = None) -> list | None:
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(self, model_name: str, parsed_model_structure: Dict[str, Any] = None, value: str = None,
728
- record_id: str = None, primary_field: str = None, is_secondary_index: bool = None,
729
- fields: list = None) -> Dict | None:
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.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='%(asctime)s - %(levelname)s - %(message)s')
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() / '.amplify-migrator'
19
- CONFIG_FILE = CONFIG_DIR / 'config.json'
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(self, api_endpoint: str, region: str, user_pool_id: str, is_aws_admin: bool = False,
29
- client_id: str = None, username: str = None, aws_profile: str = None):
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(is_aws_admin=is_aws_admin, username=username,
40
- aws_profile=aws_profile)
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['fields']:
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['name']] = input
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['name'][:-2] if field['is_id'] else field['name']
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['is_required']:
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['name'])
118
- if field['is_id']:
119
- related_model = (temp := field['name'][:-2])[0].upper() + temp[1:]
120
- record = self.amplify_client.get_record(related_model, parsed_model_structure=parsed_model_structure,
121
- value=value, fields=['id'])
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['id'] is None and field['is_required']:
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['id']
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'(?<!^)(?=[A-Z])', ' ', s)
145
+ s_with_spaces = re.sub(r"(?<!^)(?=[A-Z])", " ", s)
136
146
 
137
- parts = re.split(r'[\s_\-]+', s_with_spaces.strip())
138
- return parts[0].lower() + ''.join(word.capitalize() for word in parts[1:])
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 = '', secret: bool = False) -> 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 ['password', 'ADMIN_PASSWORD']}
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, 'w') as f:
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, 'r') as f:
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 = '', secret: bool = False) -> 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 = {'excel_path': get_config_value('Excel file path', 'data.xlsx'),
220
- 'api_endpoint': get_config_value('AWS Amplify API endpoint'),
221
- 'region': get_config_value('AWS Region', 'us-east-1'),
222
- 'user_pool_id': get_config_value('Cognito User Pool ID'),
223
- 'client_id': get_config_value('Cognito Client ID'),
224
- 'username': get_config_value('Admin Username')}
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('excel_path', 'Excel file path', cached_config, 'data.xlsx')
248
- api_endpoint = get_cached_or_prompt('api_endpoint', 'AWS Amplify API endpoint', cached_config)
249
- region = get_cached_or_prompt('region', 'AWS Region', cached_config, 'us-east-1')
250
- user_pool_id = get_cached_or_prompt('user_pool_id', 'Cognito User Pool ID', cached_config)
251
- client_id = get_cached_or_prompt('client_id', 'Cognito Client ID', cached_config)
252
- username = get_cached_or_prompt('username', 'Admin Username', cached_config)
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('ADMIN_PASSWORD', 'Admin Password', secret=True)
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='Amplify Excel Migrator - Migrate Excel data to AWS Amplify GraphQL API',
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='command', help='Available commands')
290
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
274
291
 
275
- config_parser = subparsers.add_parser('config', help='Configure the migration tool')
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('show', help='Show current configuration')
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('migrate', help='Run the migration')
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
- 'String', 'Int', 'Float', 'Boolean',
10
- 'AWSDate', 'AWSTime', 'AWSDateTime', 'AWSTimestamp',
11
- 'AWSEmail', 'AWSJSON', 'AWSURL', 'AWSPhone', 'AWSIPAddress'
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 = {'id', 'createdAt', 'updatedAt', 'owner'}
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 'data' in introspection_result and '__type' in introspection_result['data']:
17
- type_data = introspection_result['data']['__type']
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
- 'name': type_data.get('name'),
23
- 'kind': type_data.get('kind'),
24
- 'description': type_data.get('description'),
25
- 'fields': []
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('fields'):
29
- for field in type_data['fields']:
38
+ if type_data.get("fields"):
39
+ for field in type_data["fields"]:
30
40
  parsed_field = self._parse_field(field)
31
- model_info['fields'].append(parsed_field) if parsed_field else None
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('type', {}))
37
- if 'Connection' in base_type or field.get('name') in self.metadata_fields or self._get_type_kind(
38
- field.get('type', {})) in ['OBJECT', 'INTERFACE']:
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
- 'name': field.get('name'),
43
- 'description': field.get('description'),
44
- 'type': base_type,
45
- 'is_required': self._is_required_field(field.get('type', {})),
46
- 'is_list': self._is_list_type(field.get('type', {})),
47
- 'is_scalar': base_type in self.scalar_types,
48
- 'is_id': base_type == 'ID',
49
- 'is_enum': field.get('type', {}).get('kind') == 'ENUM',
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 {'name': 'Unknown', 'kind': 'UNKNOWN'}
73
+ return {"name": "Unknown", "kind": "UNKNOWN"}
61
74
 
62
75
  type_info = {
63
- 'name': type_obj.get('name'),
64
- 'kind': type_obj.get('kind'),
65
- 'full_type': self._get_full_type_string(type_obj)
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('ofType'):
70
- type_info['of_type'] = self._parse_type(type_obj['ofType'])
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 'Unknown'
93
+ return "Unknown"
81
94
 
82
- if type_obj.get('name'):
83
- return type_obj['name']
95
+ if type_obj.get("name"):
96
+ return type_obj["name"]
84
97
 
85
- if type_obj['kind'] == 'NON_NULL':
86
- inner = self._get_full_type_string(type_obj.get('ofType', {}))
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['kind'] == 'LIST':
90
- inner = self._get_full_type_string(type_obj.get('ofType', {}))
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('kind', 'Unknown')
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 'Unknown'
114
+ return "Unknown"
102
115
 
103
- if type_obj.get('name'):
104
- return type_obj['name']
116
+ if type_obj.get("name"):
117
+ return type_obj["name"]
105
118
 
106
- if type_obj.get('ofType'):
107
- return self._get_base_type_name(type_obj['ofType'])
119
+ if type_obj.get("ofType"):
120
+ return self._get_base_type_name(type_obj["ofType"])
108
121
 
109
- return 'Unknown'
122
+ return "Unknown"
110
123
 
111
124
  def _get_type_kind(self, type_obj: Dict) -> str:
112
125
  if not type_obj:
113
- return 'UNKNOWN'
126
+ return "UNKNOWN"
114
127
 
115
- if type_obj['kind'] in ['NON_NULL', 'LIST'] and type_obj.get('ofType'):
116
- return self._get_type_kind(type_obj['ofType'])
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('kind', 'UNKNOWN')
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('kind') == 'NON_NULL'
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['kind'] == 'LIST':
141
+ if type_obj["kind"] == "LIST":
129
142
  return True
130
143
 
131
- if type_obj.get('ofType'):
132
- return self._is_list_type(type_obj['ofType'])
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,,