amplify-excel-migrator 1.1.5__py3-none-any.whl → 1.2.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. amplify_excel_migrator/__init__.py +17 -0
  2. amplify_excel_migrator/auth/__init__.py +6 -0
  3. amplify_excel_migrator/auth/cognito_auth.py +306 -0
  4. amplify_excel_migrator/auth/provider.py +42 -0
  5. amplify_excel_migrator/cli/__init__.py +5 -0
  6. amplify_excel_migrator/cli/commands.py +165 -0
  7. amplify_excel_migrator/client.py +47 -0
  8. amplify_excel_migrator/core/__init__.py +5 -0
  9. amplify_excel_migrator/core/config.py +98 -0
  10. amplify_excel_migrator/data/__init__.py +7 -0
  11. amplify_excel_migrator/data/excel_reader.py +23 -0
  12. amplify_excel_migrator/data/transformer.py +119 -0
  13. amplify_excel_migrator/data/validator.py +48 -0
  14. amplify_excel_migrator/graphql/__init__.py +8 -0
  15. amplify_excel_migrator/graphql/client.py +137 -0
  16. amplify_excel_migrator/graphql/executor.py +405 -0
  17. amplify_excel_migrator/graphql/mutation_builder.py +80 -0
  18. amplify_excel_migrator/graphql/query_builder.py +194 -0
  19. amplify_excel_migrator/migration/__init__.py +8 -0
  20. amplify_excel_migrator/migration/batch_uploader.py +23 -0
  21. amplify_excel_migrator/migration/failure_tracker.py +92 -0
  22. amplify_excel_migrator/migration/orchestrator.py +143 -0
  23. amplify_excel_migrator/migration/progress_reporter.py +57 -0
  24. amplify_excel_migrator/schema/__init__.py +6 -0
  25. model_field_parser.py → amplify_excel_migrator/schema/field_parser.py +100 -22
  26. amplify_excel_migrator/schema/introspector.py +95 -0
  27. {amplify_excel_migrator-1.1.5.dist-info → amplify_excel_migrator-1.2.15.dist-info}/METADATA +121 -26
  28. amplify_excel_migrator-1.2.15.dist-info/RECORD +40 -0
  29. amplify_excel_migrator-1.2.15.dist-info/entry_points.txt +2 -0
  30. amplify_excel_migrator-1.2.15.dist-info/top_level.txt +2 -0
  31. tests/__init__.py +1 -0
  32. tests/test_cli_commands.py +292 -0
  33. tests/test_client.py +187 -0
  34. tests/test_cognito_auth.py +363 -0
  35. tests/test_config_manager.py +347 -0
  36. tests/test_field_parser.py +615 -0
  37. tests/test_mutation_builder.py +391 -0
  38. tests/test_query_builder.py +384 -0
  39. amplify_client.py +0 -941
  40. amplify_excel_migrator-1.1.5.dist-info/RECORD +0 -9
  41. amplify_excel_migrator-1.1.5.dist-info/entry_points.txt +0 -2
  42. amplify_excel_migrator-1.1.5.dist-info/top_level.txt +0 -3
  43. migrator.py +0 -437
  44. {amplify_excel_migrator-1.1.5.dist-info → amplify_excel_migrator-1.2.15.dist-info}/WHEEL +0 -0
  45. {amplify_excel_migrator-1.1.5.dist-info → amplify_excel_migrator-1.2.15.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,23 @@
1
+ """Handles batch uploading of records to Amplify."""
2
+
3
+ import logging
4
+ from typing import Dict, List, Tuple, Any
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class BatchUploader:
10
+ def __init__(self, amplify_client):
11
+ self.amplify_client = amplify_client
12
+
13
+ def upload_records(
14
+ self, records: List[Dict], sheet_name: str, parsed_model_structure: Dict[str, Any]
15
+ ) -> Tuple[int, int, List[Dict]]:
16
+ if not records:
17
+ return 0, 0, []
18
+
19
+ success_count, upload_error_count, failed_uploads = self.amplify_client.upload(
20
+ records, sheet_name, parsed_model_structure
21
+ )
22
+
23
+ return success_count, upload_error_count, failed_uploads
@@ -0,0 +1,92 @@
1
+ """Tracks and manages failed records during migration."""
2
+
3
+ import logging
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Dict, List, Optional
7
+
8
+ import pandas as pd
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class FailureTracker:
14
+ def __init__(self):
15
+ self._failures_by_sheet: Dict[str, List[Dict]] = {}
16
+ self._current_sheet: Optional[str] = None
17
+
18
+ def set_current_sheet(self, sheet_name: str) -> None:
19
+ self._current_sheet = sheet_name
20
+ if sheet_name not in self._failures_by_sheet:
21
+ self._failures_by_sheet[sheet_name] = []
22
+
23
+ def record_failure(
24
+ self,
25
+ primary_field: str,
26
+ primary_field_value: str,
27
+ error: str,
28
+ original_row: Optional[Dict] = None,
29
+ ) -> None:
30
+ if self._current_sheet is None:
31
+ raise RuntimeError("No current sheet set. Call set_current_sheet() first.")
32
+
33
+ failure_record = {
34
+ "primary_field": primary_field,
35
+ "primary_field_value": primary_field_value,
36
+ "error": error,
37
+ }
38
+
39
+ if original_row is not None:
40
+ failure_record["original_row"] = original_row
41
+
42
+ self._failures_by_sheet[self._current_sheet].append(failure_record)
43
+
44
+ def get_failures(self, sheet_name: Optional[str] = None) -> List[Dict]:
45
+ if sheet_name:
46
+ return self._failures_by_sheet.get(sheet_name, [])
47
+ return [failure for failures in self._failures_by_sheet.values() for failure in failures]
48
+
49
+ def get_failures_by_sheet(self) -> Dict[str, List[Dict]]:
50
+ return self._failures_by_sheet.copy()
51
+
52
+ def get_total_failure_count(self) -> int:
53
+ return sum(len(failures) for failures in self._failures_by_sheet.values())
54
+
55
+ def has_failures(self) -> bool:
56
+ return any(len(failures) > 0 for failures in self._failures_by_sheet.values())
57
+
58
+ def export_to_excel(self, original_excel_path: str) -> Optional[str]:
59
+ if not self.has_failures():
60
+ return None
61
+
62
+ input_path = Path(original_excel_path)
63
+ base_name = input_path.stem
64
+ if "_failed_records_" in base_name:
65
+ base_name = base_name.split("_failed_records_")[0]
66
+
67
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
68
+ output_filename = f"{base_name}_failed_records_{timestamp}.xlsx"
69
+ output_path = input_path.parent / output_filename
70
+
71
+ logger.info(f"Writing failed records to {output_path}")
72
+
73
+ with pd.ExcelWriter(output_path, engine="openpyxl") as writer:
74
+ for sheet_name, failed_records in self._failures_by_sheet.items():
75
+ if not failed_records:
76
+ continue
77
+
78
+ rows_data = []
79
+ for record in failed_records:
80
+ row_data = record.get("original_row", {}).copy()
81
+ row_data["ERROR"] = record["error"]
82
+ rows_data.append(row_data)
83
+
84
+ df = pd.DataFrame(rows_data)
85
+ df.to_excel(writer, sheet_name=sheet_name, index=False)
86
+
87
+ logger.info(f"Successfully wrote failed records to {output_path}")
88
+ return str(output_path)
89
+
90
+ def clear(self) -> None:
91
+ self._failures_by_sheet.clear()
92
+ self._current_sheet = None
@@ -0,0 +1,143 @@
1
+ """Main migration orchestrator that coordinates the entire migration process."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Dict, Any, Optional
6
+
7
+ import pandas as pd
8
+
9
+ from amplify_excel_migrator.client import AmplifyClient
10
+ from amplify_excel_migrator.data import ExcelReader, DataTransformer
11
+ from amplify_excel_migrator.schema import FieldParser
12
+ from amplify_excel_migrator.migration import FailureTracker, ProgressReporter, BatchUploader
13
+ from amplify_excel_migrator.core import ConfigManager
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class MigrationOrchestrator:
19
+ def __init__(
20
+ self,
21
+ excel_reader: ExcelReader,
22
+ data_transformer: DataTransformer,
23
+ amplify_client: AmplifyClient,
24
+ failure_tracker: FailureTracker,
25
+ progress_reporter: ProgressReporter,
26
+ batch_uploader: BatchUploader,
27
+ field_parser: FieldParser,
28
+ ):
29
+ self.excel_reader = excel_reader
30
+ self.data_transformer = data_transformer
31
+ self.amplify_client = amplify_client
32
+ self.failure_tracker = failure_tracker
33
+ self.progress_reporter = progress_reporter
34
+ self.batch_uploader = batch_uploader
35
+ self.field_parser = field_parser
36
+
37
+ def run(self) -> int:
38
+ all_sheets = self.excel_reader.read_all_sheets()
39
+
40
+ total_success = 0
41
+
42
+ for sheet_name, df in all_sheets.items():
43
+ logger.info(f"Processing {sheet_name} sheet with {len(df)} rows")
44
+ total_success += self.process_sheet(df, sheet_name)
45
+
46
+ self._display_summary(len(all_sheets), total_success)
47
+
48
+ return total_success
49
+
50
+ def process_sheet(self, df: pd.DataFrame, sheet_name: str) -> int:
51
+ self.failure_tracker.set_current_sheet(sheet_name)
52
+
53
+ parsed_model_structure = self._get_parsed_model_structure(sheet_name)
54
+
55
+ records, row_dict_by_primary = self._transform_rows_to_records(df, parsed_model_structure, sheet_name)
56
+
57
+ confirm = input(f"\nUpload {len(records)} records of {sheet_name} to Amplify? (yes/no): ")
58
+ if confirm.lower() != "yes":
59
+ logger.info(f"Upload cancelled for {sheet_name} sheet")
60
+ return 0
61
+
62
+ success_count, upload_error_count, failed_uploads = self.batch_uploader.upload_records(
63
+ records, sheet_name, parsed_model_structure
64
+ )
65
+
66
+ for failed_upload in failed_uploads:
67
+ primary_value = str(failed_upload["primary_field_value"])
68
+ original_row = row_dict_by_primary.get(primary_value, {})
69
+
70
+ self.failure_tracker.record_failure(
71
+ primary_field=failed_upload["primary_field"],
72
+ primary_field_value=failed_upload["primary_field_value"],
73
+ error=failed_upload["error"],
74
+ original_row=original_row,
75
+ )
76
+
77
+ failures = self.failure_tracker.get_failures(sheet_name)
78
+ parsing_failures = len(failures) - upload_error_count
79
+
80
+ self.progress_reporter.print_sheet_result(
81
+ sheet_name=sheet_name,
82
+ success_count=success_count,
83
+ total_rows=len(df),
84
+ parsing_failures=parsing_failures,
85
+ upload_failures=upload_error_count,
86
+ )
87
+
88
+ return success_count
89
+
90
+ def _transform_rows_to_records(
91
+ self,
92
+ df: pd.DataFrame,
93
+ parsed_model_structure: Dict[str, Any],
94
+ sheet_name: str,
95
+ ) -> tuple[list[Any], Dict[str, Dict]]:
96
+ df.columns = [self.data_transformer.to_camel_case(c) for c in df.columns]
97
+ primary_field, _, _ = self.amplify_client.get_primary_field_name(sheet_name, parsed_model_structure)
98
+
99
+ fk_lookup_cache = {}
100
+ if self.amplify_client:
101
+ logger.info("🚀 Pre-fetching foreign key lookups...")
102
+ fk_lookup_cache = self.amplify_client.build_foreign_key_lookups(df, parsed_model_structure)
103
+
104
+ records, row_dict_by_primary, failed_rows = self.data_transformer.transform_rows_to_records(
105
+ df, parsed_model_structure, primary_field, fk_lookup_cache
106
+ )
107
+
108
+ for failed_row in failed_rows:
109
+ self.failure_tracker.record_failure(
110
+ primary_field=failed_row["primary_field"],
111
+ primary_field_value=failed_row["primary_field_value"],
112
+ error=failed_row["error"],
113
+ original_row=failed_row["original_row"],
114
+ )
115
+
116
+ return records, row_dict_by_primary
117
+
118
+ def _get_parsed_model_structure(self, sheet_name: str) -> Dict[str, Any]:
119
+ model_structure = self.amplify_client.get_model_structure(sheet_name)
120
+ return self.field_parser.parse_model_structure(model_structure)
121
+
122
+ def _display_summary(self, sheets_processed: int, total_success: int) -> None:
123
+ failures_by_sheet = self.failure_tracker.get_failures_by_sheet()
124
+
125
+ self.progress_reporter.print_migration_summary(sheets_processed, total_success, failures_by_sheet)
126
+
127
+ if self.failure_tracker.has_failures():
128
+ export_confirm = input("\nExport failed records to Excel? (yes/no): ")
129
+ if export_confirm.lower() == "yes":
130
+ failed_records_file = self.failure_tracker.export_to_excel(self.excel_reader.file_path)
131
+ if failed_records_file:
132
+ print(f"📁 Failed records exported to: {failed_records_file}")
133
+ print("=" * 60)
134
+
135
+ update_config = input("\nUpdate config to use this failed records file for next run? (yes/no): ")
136
+ if update_config.lower() == "yes":
137
+ config_manager = ConfigManager()
138
+ config_manager.update({"excel_path": failed_records_file})
139
+ print(f"✅ Config updated! Next 'migrate' will use: {Path(failed_records_file).name}")
140
+ print("=" * 60)
141
+ else:
142
+ print("Failed records export skipped.")
143
+ print("=" * 60)
@@ -0,0 +1,57 @@
1
+ """Handles progress and summary reporting during migration."""
2
+
3
+ from typing import Dict, List
4
+
5
+
6
+ class ProgressReporter:
7
+ @staticmethod
8
+ def print_sheet_result(
9
+ sheet_name: str, success_count: int, total_rows: int, parsing_failures: int, upload_failures: int
10
+ ) -> None:
11
+ print(f"=== Upload of Excel sheet: {sheet_name} Complete ===")
12
+ print(f"✅ Success: {success_count}")
13
+ total_failures = parsing_failures + upload_failures
14
+ print(f"❌ Failed: {total_failures} (Parsing: {parsing_failures}, Upload: {upload_failures})")
15
+ print(f"📊 Total: {total_rows}")
16
+
17
+ @staticmethod
18
+ def print_migration_summary(
19
+ sheets_processed: int, total_success: int, failures_by_sheet: Dict[str, List[Dict]]
20
+ ) -> None:
21
+ total_failed = sum(len(failures) for failures in failures_by_sheet.values())
22
+
23
+ print("\n" + "=" * 60)
24
+ print("MIGRATION SUMMARY")
25
+ print("=" * 60)
26
+ print(f"📊 Sheets processed: {sheets_processed}")
27
+ print(f"✅ Total successful: {total_success}")
28
+ print(f"❌ Total failed: {total_failed}")
29
+
30
+ if (total_success + total_failed) > 0:
31
+ success_rate = (total_success / (total_success + total_failed)) * 100
32
+ print(f"📈 Success rate: {success_rate:.1f}%")
33
+ else:
34
+ print("📈 Success rate: N/A")
35
+
36
+ if total_failed > 0:
37
+ print("\n" + "=" * 60)
38
+ print("FAILED RECORDS DETAILS")
39
+ print("=" * 60)
40
+
41
+ for sheet_name, failed_records in failures_by_sheet.items():
42
+ if not failed_records:
43
+ continue
44
+
45
+ print(f"\n📄 {sheet_name}:")
46
+ print("-" * 60)
47
+ for record in failed_records:
48
+ primary_field_value = record.get("primary_field_value", "Unknown")
49
+ error = record.get("error", "Unknown error")
50
+ print(f" • Record: {primary_field_value}")
51
+ print(f" Error: {error}")
52
+
53
+ print("\n" + "=" * 60)
54
+ else:
55
+ print("\n✨ No failed records!")
56
+
57
+ print("=" * 60)
@@ -0,0 +1,6 @@
1
+ """Schema introspection and field parsing."""
2
+
3
+ from .introspector import SchemaIntrospector
4
+ from .field_parser import FieldParser
5
+
6
+ __all__ = ["SchemaIntrospector", "FieldParser"]
@@ -8,7 +8,7 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(
8
8
  logger = logging.getLogger(__name__)
9
9
 
10
10
 
11
- class ModelFieldParser:
11
+ class FieldParser:
12
12
  """Parse GraphQL model fields from introspection results"""
13
13
 
14
14
  def __init__(self):
@@ -220,29 +220,47 @@ class ModelFieldParser:
220
220
 
221
221
  return custom_type_objects
222
222
 
223
- def parse_field_input(self, field: Dict[str, Any], field_name: str, input_value: Any) -> Any:
224
- try:
225
- if field["type"] in ["Int", "Integer"] or field["type"] == "Float":
223
+ def _convert_single_value(
224
+ self,
225
+ field: Dict[str, Any],
226
+ field_name: str,
227
+ input_value: Any,
228
+ use_dash_notation: bool = False,
229
+ index: int = None,
230
+ ) -> Any:
231
+ if field["type"] in ["Int", "Integer"]:
232
+ if use_dash_notation:
226
233
  parsed_value = self.parse_number_dash_notation(input_value)
227
- return int(parsed_value) if field["type"] in ["Int", "Integer"] else float(parsed_value)
228
- elif field["type"] == "Float":
229
- return float(input_value)
230
- elif field["type"] == "Boolean":
231
- if isinstance(input_value, bool):
232
- return input_value
233
- if str(input_value).strip().lower() in ["true", "1", "v", "y", "yes"]:
234
- return True
235
- elif str(input_value).strip().lower() in ["false", "0", "n", "x", "no"]:
236
- return False
237
- else:
238
- logger.error(f"Invalid Boolean value for field '{field_name}': {input_value}")
239
- return None
240
- elif field["is_enum"]:
241
- return str(input_value).strip().replace(" ", "_").upper()
242
- elif field["type"] == "AWSDate" or field["type"] == "AWSDateTime":
243
- return self.parse_date(input_value)
234
+ return int(parsed_value)
235
+ return int(input_value)
236
+ elif field["type"] == "Float":
237
+ if use_dash_notation:
238
+ parsed_value = self.parse_number_dash_notation(input_value)
239
+ return float(parsed_value)
240
+ return float(input_value)
241
+ elif field["type"] == "Boolean":
242
+ if isinstance(input_value, bool):
243
+ return input_value
244
+ input_str = str(input_value).strip().lower()
245
+ if input_str in ["true", "1", "v", "y", "yes"]:
246
+ return True
247
+ elif input_str in ["false", "0", "n", "x", "no"]:
248
+ return False
244
249
  else:
245
- return str(input_value).strip()
250
+ context = f"array '{field_name}[{index}]'" if index is not None else f"field '{field_name}'"
251
+ logger.error(f"Invalid Boolean value for {context}: {input_value}")
252
+ return None
253
+ elif field.get("is_enum", False):
254
+ return str(input_value).strip().replace(" ", "_").upper()
255
+ elif field["type"] in ["AWSDate", "AWSDateTime"]:
256
+ return self.parse_date(input_value)
257
+ else:
258
+ return str(input_value).strip()
259
+
260
+ def parse_field_input(self, field: Dict[str, Any], field_name: str, input_value: Any) -> Any:
261
+ """Parse a single field value from Excel"""
262
+ try:
263
+ return self._convert_single_value(field, field_name, input_value, use_dash_notation=True)
246
264
  except (ValueError, TypeError) as e:
247
265
  logger.warning(
248
266
  f"Failed to parse field '{field_name}' with value '{input_value}' (type: {type(input_value).__name__}) "
@@ -251,6 +269,66 @@ class ModelFieldParser:
251
269
  )
252
270
  return None
253
271
 
272
+ def parse_scalar_array(self, field: Dict[str, Any], field_name: str, input_value: Any) -> list | None:
273
+ """
274
+ Parse scalar array from Excel cell supporting multiple formats:
275
+ - JSON: ["value1", "value2", "value3"]
276
+ - Semicolon: value1; value2; value3
277
+ - Comma: value1, value2, value3
278
+ - Space: value1 value2 value3
279
+ """
280
+ if pd.isna(input_value):
281
+ return None
282
+
283
+ input_str = str(input_value).strip()
284
+ if not input_str:
285
+ return None
286
+
287
+ if input_str.startswith("[") and input_str.endswith("]"):
288
+ try:
289
+ import json
290
+
291
+ parsed_json = json.loads(input_str)
292
+ if isinstance(parsed_json, list):
293
+ return self._convert_array_elements(field, field_name, parsed_json)
294
+ except json.JSONDecodeError:
295
+ logger.warning(f"Failed to parse JSON array for field '{field_name}': {input_str}")
296
+
297
+ if ";" in input_str:
298
+ values = [v.strip() for v in input_str.split(";") if v.strip()]
299
+ return self._convert_array_elements(field, field_name, values)
300
+
301
+ if "," in input_str:
302
+ values = [v.strip() for v in input_str.split(",") if v.strip()]
303
+ return self._convert_array_elements(field, field_name, values)
304
+
305
+ values = [v.strip() for v in input_str.split() if v.strip()]
306
+ if len(values) > 1:
307
+ return self._convert_array_elements(field, field_name, values)
308
+
309
+ return self._convert_array_elements(field, field_name, [input_str])
310
+
311
+ def _convert_array_elements(self, field: Dict[str, Any], field_name: str, values: list) -> list:
312
+ converted = []
313
+ for i, value in enumerate(values):
314
+ cleaned_value = self.clean_input(value)
315
+
316
+ if not cleaned_value or (isinstance(cleaned_value, str) and not cleaned_value.strip()):
317
+ continue
318
+
319
+ try:
320
+ result = self._convert_single_value(field, field_name, cleaned_value, use_dash_notation=False, index=i)
321
+ if result is not None:
322
+ converted.append(result)
323
+ except (ValueError, TypeError) as e:
324
+ logger.warning(
325
+ f"Failed to convert array element '{field_name}[{i}]' with value '{cleaned_value}' "
326
+ f"to type '{field['type']}': {e}"
327
+ )
328
+ continue
329
+
330
+ return converted if converted else None
331
+
254
332
  @staticmethod
255
333
  def parse_number_dash_notation(input_value: Any) -> int | float:
256
334
  """
@@ -0,0 +1,95 @@
1
+ """Schema introspection for GraphQL models."""
2
+
3
+ import logging
4
+ from typing import Dict, Any, Optional
5
+
6
+ import inflect
7
+
8
+ from amplify_excel_migrator.graphql import GraphQLClient
9
+ from amplify_excel_migrator.graphql.query_builder import QueryBuilder
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class SchemaIntrospector:
15
+ def __init__(self, client: GraphQLClient):
16
+ self.client = client
17
+
18
+ def get_model_structure(self, model_type: str) -> Dict[str, Any]:
19
+ query = QueryBuilder.build_introspection_query(model_type)
20
+ response = self.client.request(query)
21
+
22
+ if response and "data" in response and "__type" in response["data"]:
23
+ return response["data"]["__type"]
24
+
25
+ return {}
26
+
27
+ def get_primary_field_name(self, model_name: str, parsed_model_structure: Dict[str, Any]) -> tuple[str, bool, str]:
28
+ secondary_index = self._get_secondary_index(model_name)
29
+ if secondary_index:
30
+ field_type = "String"
31
+ for field in parsed_model_structure["fields"]:
32
+ if field["name"] == secondary_index:
33
+ field_type = field["type"]
34
+ break
35
+ return secondary_index, True, field_type
36
+
37
+ for field in parsed_model_structure["fields"]:
38
+ if field["is_required"] and field["is_scalar"] and field["name"] != "id":
39
+ return field["name"], False, field["type"]
40
+
41
+ logger.error("No suitable primary field found (required scalar field other than id)")
42
+ return "", False, "String"
43
+
44
+ def _get_secondary_index(self, model_name: str) -> str:
45
+ query_structure = self.get_model_structure("Query")
46
+ if not query_structure:
47
+ logger.error("Query type not found in schema")
48
+ return ""
49
+
50
+ query_fields = query_structure["fields"]
51
+ pattern = f"{model_name}By"
52
+
53
+ for query in query_fields:
54
+ query_name = query["name"]
55
+ if pattern in query_name:
56
+ pattern_index = query_name.index(pattern)
57
+ field_name = query_name[pattern_index + len(pattern) :]
58
+ return field_name[0].lower() + field_name[1:] if field_name else ""
59
+
60
+ return ""
61
+
62
+ def get_list_query_name(self, model_name: str) -> Optional[str]:
63
+ query_structure = self.get_model_structure("Query")
64
+ if not query_structure:
65
+ logger.error("Query type not found in schema")
66
+ return f"list{model_name}s"
67
+
68
+ query_fields = query_structure["fields"]
69
+ p = inflect.engine()
70
+
71
+ candidates = [f"list{model_name}"]
72
+ capitals = [i for i, c in enumerate(model_name) if c.isupper()]
73
+
74
+ if len(capitals) > 1:
75
+ last_word_start = capitals[-1]
76
+ prefix = model_name[:last_word_start]
77
+ last_word = model_name[last_word_start:]
78
+
79
+ last_word_plural = str(p.plural(last_word.lower()))
80
+ last_word_plural_cap = last_word_plural[0].upper() + last_word_plural[1:] if last_word_plural else ""
81
+
82
+ pascal_plural = f"{prefix}{last_word_plural_cap}"
83
+ candidates.append(f"list{pascal_plural}")
84
+
85
+ full_plural = str(p.plural(model_name.lower()))
86
+ full_plural_cap = full_plural[0].upper() + full_plural[1:] if full_plural else ""
87
+ candidates.append(f"list{full_plural_cap}")
88
+
89
+ for query in query_fields:
90
+ query_name = query["name"]
91
+ if query_name in candidates and "By" not in query_name:
92
+ return query_name
93
+
94
+ logger.error(f"No list query found for model {model_name}, tried: {candidates}")
95
+ return None