amplify-excel-migrator 1.0.0__tar.gz → 1.0.1__tar.gz

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.

Files changed (24) hide show
  1. {amplify_excel_migrator-1.0.0/amplify_excel_migrator.egg-info → amplify_excel_migrator-1.0.1}/PKG-INFO +9 -1
  2. {amplify_excel_migrator-1.0.0 → amplify_excel_migrator-1.0.1}/README.md +8 -0
  3. {amplify_excel_migrator-1.0.0 → amplify_excel_migrator-1.0.1}/amplify_client.py +126 -143
  4. {amplify_excel_migrator-1.0.0 → amplify_excel_migrator-1.0.1/amplify_excel_migrator.egg-info}/PKG-INFO +9 -1
  5. {amplify_excel_migrator-1.0.0 → amplify_excel_migrator-1.0.1}/migrator.py +70 -53
  6. amplify_excel_migrator-1.0.1/model_field_parser.py +147 -0
  7. {amplify_excel_migrator-1.0.0 → amplify_excel_migrator-1.0.1}/setup.py +2 -2
  8. amplify_excel_migrator-1.0.1/tests/test_cli_commands.py +250 -0
  9. amplify_excel_migrator-1.0.1/tests/test_config.py +175 -0
  10. amplify_excel_migrator-1.0.1/tests/test_migrator_class.py +236 -0
  11. amplify_excel_migrator-1.0.0/model_field_parser.py +0 -134
  12. amplify_excel_migrator-1.0.0/tests/test_cli_commands.py +0 -249
  13. amplify_excel_migrator-1.0.0/tests/test_config.py +0 -174
  14. amplify_excel_migrator-1.0.0/tests/test_migrator_class.py +0 -256
  15. {amplify_excel_migrator-1.0.0 → amplify_excel_migrator-1.0.1}/LICENSE +0 -0
  16. {amplify_excel_migrator-1.0.0 → amplify_excel_migrator-1.0.1}/MANIFEST.in +0 -0
  17. {amplify_excel_migrator-1.0.0 → amplify_excel_migrator-1.0.1}/amplify_excel_migrator.egg-info/SOURCES.txt +0 -0
  18. {amplify_excel_migrator-1.0.0 → amplify_excel_migrator-1.0.1}/amplify_excel_migrator.egg-info/dependency_links.txt +0 -0
  19. {amplify_excel_migrator-1.0.0 → amplify_excel_migrator-1.0.1}/amplify_excel_migrator.egg-info/entry_points.txt +0 -0
  20. {amplify_excel_migrator-1.0.0 → amplify_excel_migrator-1.0.1}/amplify_excel_migrator.egg-info/requires.txt +0 -0
  21. {amplify_excel_migrator-1.0.0 → amplify_excel_migrator-1.0.1}/amplify_excel_migrator.egg-info/top_level.txt +0 -0
  22. {amplify_excel_migrator-1.0.0 → amplify_excel_migrator-1.0.1}/requirements.txt +0 -0
  23. {amplify_excel_migrator-1.0.0 → amplify_excel_migrator-1.0.1}/setup.cfg +0 -0
  24. {amplify_excel_migrator-1.0.0 → amplify_excel_migrator-1.0.1}/tests/__init__.py +0 -0
@@ -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:
@@ -5,6 +5,14 @@ Developed for the MECO project - https://github.com/sworgkh/meco-observations-am
5
5
 
6
6
  ## Installation
7
7
 
8
+ ### From PyPI (Recommended)
9
+
10
+ Install the latest stable version from PyPI:
11
+
12
+ ```bash
13
+ pip install amplify-excel-migrator
14
+ ```
15
+
8
16
  ### From GitHub
9
17
 
10
18
  Install directly from GitHub:
@@ -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: