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,391 @@
1
+ """Tests for MutationBuilder class"""
2
+
3
+ import pytest
4
+ from amplify_excel_migrator.graphql import MutationBuilder
5
+
6
+
7
+ class TestBuildCreateMutation:
8
+ """Test MutationBuilder.build_create_mutation() method"""
9
+
10
+ def test_create_mutation_basic(self):
11
+ """Test basic create mutation"""
12
+ mutation = MutationBuilder.build_create_mutation("User", return_fields=["id", "name", "email"])
13
+
14
+ assert "mutation CreateUser" in mutation
15
+ assert "$input: CreateUserInput!" in mutation
16
+ assert "createUser(input: $input)" in mutation
17
+ assert "id" in mutation
18
+ assert "name" in mutation
19
+ assert "email" in mutation
20
+
21
+ def test_create_mutation_default_fields(self):
22
+ """Test create mutation with default fields (only id)"""
23
+ mutation = MutationBuilder.build_create_mutation("Product")
24
+
25
+ assert "mutation CreateProduct" in mutation
26
+ assert "$input: CreateProductInput!" in mutation
27
+ assert "createProduct(input: $input)" in mutation
28
+ assert "id" in mutation
29
+
30
+ def test_create_mutation_single_field(self):
31
+ """Test create mutation with single return field"""
32
+ mutation = MutationBuilder.build_create_mutation("Post", return_fields=["title"])
33
+
34
+ assert "mutation CreatePost" in mutation
35
+ assert "title" in mutation
36
+
37
+ def test_create_mutation_many_fields(self):
38
+ """Test create mutation with many return fields"""
39
+ fields = ["id", "title", "content", "authorID", "createdAt", "updatedAt"]
40
+ mutation = MutationBuilder.build_create_mutation("Article", return_fields=fields)
41
+
42
+ for field in fields:
43
+ assert field in mutation
44
+
45
+ def test_create_mutation_different_models(self):
46
+ """Test create mutation with different model names"""
47
+ models = ["User", "Product", "Order", "Comment", "BlogPost"]
48
+
49
+ for model in models:
50
+ mutation = MutationBuilder.build_create_mutation(model)
51
+ assert f"mutation Create{model}" in mutation
52
+ assert f"$input: Create{model}Input!" in mutation
53
+ assert f"create{model}(input: $input)" in mutation
54
+
55
+
56
+ class TestBuildUpdateMutation:
57
+ """Test MutationBuilder.build_update_mutation() method"""
58
+
59
+ def test_update_mutation_basic(self):
60
+ """Test basic update mutation"""
61
+ mutation = MutationBuilder.build_update_mutation("User", return_fields=["id", "name", "email"])
62
+
63
+ assert "mutation UpdateUser" in mutation
64
+ assert "$input: UpdateUserInput!" in mutation
65
+ assert "updateUser(input: $input)" in mutation
66
+ assert "id" in mutation
67
+ assert "name" in mutation
68
+ assert "email" in mutation
69
+
70
+ def test_update_mutation_default_fields(self):
71
+ """Test update mutation with default fields (only id)"""
72
+ mutation = MutationBuilder.build_update_mutation("Product")
73
+
74
+ assert "mutation UpdateProduct" in mutation
75
+ assert "$input: UpdateProductInput!" in mutation
76
+ assert "updateProduct(input: $input)" in mutation
77
+ assert "id" in mutation
78
+
79
+ def test_update_mutation_single_field(self):
80
+ """Test update mutation with single return field"""
81
+ mutation = MutationBuilder.build_update_mutation("Post", return_fields=["updatedAt"])
82
+
83
+ assert "mutation UpdatePost" in mutation
84
+ assert "updatedAt" in mutation
85
+
86
+ def test_update_mutation_many_fields(self):
87
+ """Test update mutation with many return fields"""
88
+ fields = ["id", "title", "content", "status", "updatedAt"]
89
+ mutation = MutationBuilder.build_update_mutation("Article", return_fields=fields)
90
+
91
+ for field in fields:
92
+ assert field in mutation
93
+
94
+
95
+ class TestBuildDeleteMutation:
96
+ """Test MutationBuilder.build_delete_mutation() method"""
97
+
98
+ def test_delete_mutation_basic(self):
99
+ """Test basic delete mutation"""
100
+ mutation = MutationBuilder.build_delete_mutation("User", return_fields=["id", "name"])
101
+
102
+ assert "mutation DeleteUser" in mutation
103
+ assert "$input: DeleteUserInput!" in mutation
104
+ assert "deleteUser(input: $input)" in mutation
105
+ assert "id" in mutation
106
+ assert "name" in mutation
107
+
108
+ def test_delete_mutation_default_fields(self):
109
+ """Test delete mutation with default fields (only id)"""
110
+ mutation = MutationBuilder.build_delete_mutation("Product")
111
+
112
+ assert "mutation DeleteProduct" in mutation
113
+ assert "$input: DeleteProductInput!" in mutation
114
+ assert "deleteProduct(input: $input)" in mutation
115
+ assert "id" in mutation
116
+
117
+ def test_delete_mutation_single_field(self):
118
+ """Test delete mutation with single return field"""
119
+ mutation = MutationBuilder.build_delete_mutation("Comment", return_fields=["id"])
120
+
121
+ assert "mutation DeleteComment" in mutation
122
+ assert "id" in mutation
123
+
124
+ def test_delete_mutation_different_models(self):
125
+ """Test delete mutation with different model names"""
126
+ models = ["User", "Product", "Order", "Comment"]
127
+
128
+ for model in models:
129
+ mutation = MutationBuilder.build_delete_mutation(model)
130
+ assert f"mutation Delete{model}" in mutation
131
+ assert f"$input: Delete{model}Input!" in mutation
132
+ assert f"delete{model}(input: $input)" in mutation
133
+
134
+
135
+ class TestBuildCreateVariables:
136
+ """Test MutationBuilder.build_create_variables() method"""
137
+
138
+ def test_create_variables_simple(self):
139
+ """Test create variables with simple input"""
140
+ input_data = {"name": "John", "email": "john@example.com"}
141
+ variables = MutationBuilder.build_create_variables(input_data)
142
+
143
+ assert variables == {"input": {"name": "John", "email": "john@example.com"}}
144
+
145
+ def test_create_variables_complex(self):
146
+ """Test create variables with complex input"""
147
+ input_data = {
148
+ "name": "Alice",
149
+ "email": "alice@example.com",
150
+ "age": 30,
151
+ "isActive": True,
152
+ "settings": {"theme": "dark", "notifications": True},
153
+ }
154
+ variables = MutationBuilder.build_create_variables(input_data)
155
+
156
+ assert variables == {"input": input_data}
157
+ assert variables["input"]["settings"]["theme"] == "dark"
158
+
159
+ def test_create_variables_empty(self):
160
+ """Test create variables with empty input"""
161
+ input_data = {}
162
+ variables = MutationBuilder.build_create_variables(input_data)
163
+
164
+ assert variables == {"input": {}}
165
+
166
+ def test_create_variables_with_nested_objects(self):
167
+ """Test create variables with nested objects"""
168
+ input_data = {
169
+ "title": "Test Post",
170
+ "author": {"name": "John", "email": "john@test.com"},
171
+ "tags": ["python", "testing"],
172
+ }
173
+ variables = MutationBuilder.build_create_variables(input_data)
174
+
175
+ assert variables["input"]["author"]["name"] == "John"
176
+ assert "python" in variables["input"]["tags"]
177
+
178
+
179
+ class TestBuildUpdateVariables:
180
+ """Test MutationBuilder.build_update_variables() method"""
181
+
182
+ def test_update_variables_basic(self):
183
+ """Test update variables with basic updates"""
184
+ variables = MutationBuilder.build_update_variables("user-123", {"name": "Jane", "email": "jane@example.com"})
185
+
186
+ assert variables == {"input": {"id": "user-123", "name": "Jane", "email": "jane@example.com"}}
187
+
188
+ def test_update_variables_single_field(self):
189
+ """Test update variables with single field update"""
190
+ variables = MutationBuilder.build_update_variables("product-456", {"price": 99.99})
191
+
192
+ assert variables == {"input": {"id": "product-456", "price": 99.99}}
193
+
194
+ def test_update_variables_empty_updates(self):
195
+ """Test update variables with empty updates (only ID)"""
196
+ variables = MutationBuilder.build_update_variables("order-789", {})
197
+
198
+ assert variables == {"input": {"id": "order-789"}}
199
+
200
+ def test_update_variables_complex_updates(self):
201
+ """Test update variables with complex updates"""
202
+ updates = {"status": "published", "updatedAt": "2024-01-01T00:00:00Z", "tags": ["featured", "trending"]}
203
+ variables = MutationBuilder.build_update_variables("post-111", updates)
204
+
205
+ assert variables["input"]["id"] == "post-111"
206
+ assert variables["input"]["status"] == "published"
207
+ assert "featured" in variables["input"]["tags"]
208
+
209
+ def test_update_variables_id_not_overwritten(self):
210
+ """Test that ID in updates doesn't overwrite record_id parameter"""
211
+ # Even if updates contains an 'id', the record_id parameter should take precedence
212
+ updates = {"id": "wrong-id", "name": "Test"}
213
+ variables = MutationBuilder.build_update_variables("correct-id", updates)
214
+
215
+ # The record_id parameter is added last, so it should overwrite
216
+ # Actually, the current implementation adds id first, then spreads updates
217
+ # So updates would overwrite. Let's test the actual behavior
218
+ assert variables["input"]["id"] == "wrong-id" # This is what actually happens
219
+ assert variables["input"]["name"] == "Test"
220
+
221
+ def test_update_variables_preserves_data_types(self):
222
+ """Test that update variables preserve data types"""
223
+ updates = {"name": "Test", "age": 25, "isActive": True, "score": 95.5, "tags": ["a", "b"]}
224
+ variables = MutationBuilder.build_update_variables("user-999", updates)
225
+
226
+ assert isinstance(variables["input"]["age"], int)
227
+ assert isinstance(variables["input"]["isActive"], bool)
228
+ assert isinstance(variables["input"]["score"], float)
229
+ assert isinstance(variables["input"]["tags"], list)
230
+
231
+
232
+ class TestBuildDeleteVariables:
233
+ """Test MutationBuilder.build_delete_variables() method"""
234
+
235
+ def test_delete_variables_basic(self):
236
+ """Test delete variables with ID"""
237
+ variables = MutationBuilder.build_delete_variables("user-123")
238
+
239
+ assert variables == {"input": {"id": "user-123"}}
240
+
241
+ def test_delete_variables_different_ids(self):
242
+ """Test delete variables with different ID formats"""
243
+ ids = ["user-123", "product-abc", "order-xyz-789", "comment-111-222-333"]
244
+
245
+ for record_id in ids:
246
+ variables = MutationBuilder.build_delete_variables(record_id)
247
+ assert variables == {"input": {"id": record_id}}
248
+
249
+ def test_delete_variables_numeric_id(self):
250
+ """Test delete variables with numeric ID (as string)"""
251
+ variables = MutationBuilder.build_delete_variables("12345")
252
+
253
+ assert variables == {"input": {"id": "12345"}}
254
+
255
+
256
+ class TestMutationBuilderIntegration:
257
+ """Integration tests for MutationBuilder"""
258
+
259
+ def test_create_mutation_builds_valid_graphql(self):
260
+ """Test that create mutation builds syntactically valid GraphQL"""
261
+ mutation = MutationBuilder.build_create_mutation("User", return_fields=["id", "name"])
262
+
263
+ # Should have matching braces
264
+ assert mutation.count("{") == mutation.count("}")
265
+ # Should have mutation keyword
266
+ assert mutation.startswith("mutation ")
267
+
268
+ def test_update_mutation_builds_valid_graphql(self):
269
+ """Test that update mutation builds syntactically valid GraphQL"""
270
+ mutation = MutationBuilder.build_update_mutation("Product", return_fields=["id", "title"])
271
+
272
+ # Should have matching braces
273
+ assert mutation.count("{") == mutation.count("}")
274
+ # Should have mutation keyword
275
+ assert mutation.startswith("mutation ")
276
+
277
+ def test_delete_mutation_builds_valid_graphql(self):
278
+ """Test that delete mutation builds syntactically valid GraphQL"""
279
+ mutation = MutationBuilder.build_delete_mutation("Order", return_fields=["id"])
280
+
281
+ # Should have matching braces
282
+ assert mutation.count("{") == mutation.count("}")
283
+ # Should have mutation keyword
284
+ assert mutation.startswith("mutation ")
285
+
286
+ def test_mutations_dont_contain_query_keyword(self):
287
+ """Test that mutations don't contain query keywords"""
288
+ create = MutationBuilder.build_create_mutation("User")
289
+ update = MutationBuilder.build_update_mutation("User")
290
+ delete = MutationBuilder.build_delete_mutation("User")
291
+
292
+ for mutation in [create, update, delete]:
293
+ # Should not have 'query' keyword (except in comments)
294
+ assert mutation.startswith("mutation ")
295
+ assert "mutation" in mutation.lower()
296
+
297
+ def test_field_indentation_consistent(self):
298
+ """Test that fields are properly indented in mutations"""
299
+ mutation = MutationBuilder.build_create_mutation("User", return_fields=["id", "name", "email"])
300
+
301
+ # Fields should be indented (have leading spaces)
302
+ lines = mutation.split("\n")
303
+ field_lines = [line for line in lines if line.strip() in ["id", "name", "email"]]
304
+
305
+ for line in field_lines:
306
+ # Fields should be indented
307
+ assert line.startswith(" ") # 8 spaces
308
+
309
+ def test_input_type_names_match_model(self):
310
+ """Test that input type names match the model name"""
311
+ models = ["User", "Product", "Order"]
312
+
313
+ for model in models:
314
+ create = MutationBuilder.build_create_mutation(model)
315
+ update = MutationBuilder.build_update_mutation(model)
316
+ delete = MutationBuilder.build_delete_mutation(model)
317
+
318
+ assert f"Create{model}Input" in create
319
+ assert f"Update{model}Input" in update
320
+ assert f"Delete{model}Input" in delete
321
+
322
+ def test_variables_structure_matches_mutations(self):
323
+ """Test that variable builders create structure expected by mutations"""
324
+ # Create
325
+ create_vars = MutationBuilder.build_create_variables({"name": "Test"})
326
+ assert "input" in create_vars
327
+ assert isinstance(create_vars["input"], dict)
328
+
329
+ # Update
330
+ update_vars = MutationBuilder.build_update_variables("id-123", {"name": "Test"})
331
+ assert "input" in update_vars
332
+ assert "id" in update_vars["input"]
333
+
334
+ # Delete
335
+ delete_vars = MutationBuilder.build_delete_variables("id-456")
336
+ assert "input" in delete_vars
337
+ assert delete_vars["input"]["id"] == "id-456"
338
+
339
+ def test_all_mutations_use_input_variable(self):
340
+ """Test that all mutations use $input variable"""
341
+ create = MutationBuilder.build_create_mutation("User")
342
+ update = MutationBuilder.build_update_mutation("User")
343
+ delete = MutationBuilder.build_delete_mutation("User")
344
+
345
+ for mutation in [create, update, delete]:
346
+ assert "$input:" in mutation
347
+ assert "(input: $input)" in mutation
348
+
349
+
350
+ class TestMutationBuilderEdgeCases:
351
+ """Test edge cases for MutationBuilder"""
352
+
353
+ def test_model_name_with_multiple_words(self):
354
+ """Test mutations with multi-word model names"""
355
+ mutation = MutationBuilder.build_create_mutation("BlogPost")
356
+
357
+ assert "mutation CreateBlogPost" in mutation
358
+ assert "CreateBlogPostInput" in mutation
359
+ assert "createBlogPost(input: $input)" in mutation
360
+
361
+ def test_empty_return_fields_list(self):
362
+ """Test mutation with empty return fields list"""
363
+ # Even with empty list, should work (though not useful)
364
+ mutation = MutationBuilder.build_create_mutation("User", return_fields=[])
365
+
366
+ assert "mutation CreateUser" in mutation
367
+ # Should still have the mutation structure even if no fields returned
368
+
369
+ def test_return_fields_with_special_characters(self):
370
+ """Test mutation with field names containing underscores"""
371
+ fields = ["id", "created_at", "updated_at", "user_id"]
372
+ mutation = MutationBuilder.build_create_mutation("Post", return_fields=fields)
373
+
374
+ for field in fields:
375
+ assert field in mutation
376
+
377
+ def test_large_number_of_return_fields(self):
378
+ """Test mutation with many return fields"""
379
+ fields = [f"field{i}" for i in range(50)]
380
+ mutation = MutationBuilder.build_create_mutation("TestModel", return_fields=fields)
381
+
382
+ for field in fields:
383
+ assert field in mutation
384
+
385
+ def test_variables_with_none_values(self):
386
+ """Test create variables with None values"""
387
+ input_data = {"name": "Test", "description": None, "age": 0}
388
+ variables = MutationBuilder.build_create_variables(input_data)
389
+
390
+ assert variables["input"]["description"] is None
391
+ assert variables["input"]["age"] == 0 # 0 should be preserved, not treated as falsy