django-bulk-hooks 0.1.259__tar.gz → 0.1.261__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.
Files changed (17) hide show
  1. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.261}/PKG-INFO +1 -1
  2. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.261}/django_bulk_hooks/queryset.py +196 -112
  3. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.261}/pyproject.toml +1 -1
  4. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.261}/LICENSE +0 -0
  5. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.261}/README.md +0 -0
  6. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.261}/django_bulk_hooks/__init__.py +0 -0
  7. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.261}/django_bulk_hooks/conditions.py +0 -0
  8. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.261}/django_bulk_hooks/constants.py +0 -0
  9. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.261}/django_bulk_hooks/context.py +0 -0
  10. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.261}/django_bulk_hooks/decorators.py +0 -0
  11. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.261}/django_bulk_hooks/engine.py +0 -0
  12. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.261}/django_bulk_hooks/enums.py +0 -0
  13. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.261}/django_bulk_hooks/handler.py +0 -0
  14. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.261}/django_bulk_hooks/manager.py +0 -0
  15. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.261}/django_bulk_hooks/models.py +0 -0
  16. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.261}/django_bulk_hooks/priority.py +0 -0
  17. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.261}/django_bulk_hooks/registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.259
3
+ Version: 0.1.261
4
4
  Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
5
5
  License: MIT
6
6
  Keywords: django,bulk,hooks
@@ -89,7 +89,7 @@ class HookQuerySetMixin:
89
89
  try:
90
90
  from django.db.models import Subquery
91
91
 
92
- logger.debug(f"Successfully imported Subquery from django.db.models")
92
+ logger.debug("Successfully imported Subquery from django.db.models")
93
93
  except ImportError as e:
94
94
  logger.error(f"Failed to import Subquery: {e}")
95
95
  raise
@@ -367,7 +367,7 @@ class HookQuerySetMixin:
367
367
 
368
368
  if has_nested_subquery:
369
369
  logger.debug(
370
- f"Expression contains Subquery, ensuring proper output_field"
370
+ "Expression contains Subquery, ensuring proper output_field"
371
371
  )
372
372
  # Try to resolve the expression to ensure it's properly formatted
373
373
  try:
@@ -531,6 +531,10 @@ class HookQuerySetMixin:
531
531
  # Check which records already exist in the database based on unique fields
532
532
  existing_records = []
533
533
  new_records = []
534
+
535
+ # Store the records for AFTER hooks to avoid duplicate queries
536
+ ctx.upsert_existing_records = existing_records
537
+ ctx.upsert_new_records = new_records
534
538
 
535
539
  # Build a filter to check which records already exist
536
540
  unique_values = []
@@ -541,9 +545,9 @@ class HookQuerySetMixin:
541
545
  unique_value[field_name] = getattr(obj, field_name)
542
546
  if unique_value:
543
547
  unique_values.append(unique_value)
544
-
548
+
545
549
  if unique_values:
546
- # Query the database to see which records already exist
550
+ # Query the database to see which records already exist - SINGLE BULK QUERY
547
551
  from django.db.models import Q
548
552
  existing_filters = Q()
549
553
  for unique_value in unique_values:
@@ -551,25 +555,25 @@ class HookQuerySetMixin:
551
555
  for field_name, value in unique_value.items():
552
556
  filter_kwargs[field_name] = value
553
557
  existing_filters |= Q(**filter_kwargs)
554
-
555
- existing_pks = set(
556
- model_cls.objects.filter(existing_filters).values_list('pk', flat=True)
557
- )
558
-
558
+
559
+ # Get all existing records in one query and create a lookup set
560
+ existing_records_lookup = set()
561
+ for existing_record in model_cls.objects.filter(existing_filters).values_list(*unique_fields):
562
+ # Convert tuple to a hashable key for lookup
563
+ existing_records_lookup.add(existing_record)
564
+
559
565
  # Separate records based on whether they already exist
560
566
  for obj in objs:
561
567
  obj_unique_value = {}
562
568
  for field_name in unique_fields:
563
569
  if hasattr(obj, field_name):
564
570
  obj_unique_value[field_name] = getattr(obj, field_name)
565
-
566
- # Check if this record already exists
571
+
572
+ # Check if this record already exists using our bulk lookup
567
573
  if obj_unique_value:
568
- existing_q = Q()
569
- for field_name, value in obj_unique_value.items():
570
- existing_q &= Q(**{field_name: value})
571
-
572
- if model_cls.objects.filter(existing_q).exists():
574
+ # Convert object values to tuple for comparison with existing records
575
+ obj_unique_tuple = tuple(obj_unique_value[field_name] for field_name in unique_fields)
576
+ if obj_unique_tuple in existing_records_lookup:
573
577
  existing_records.append(obj)
574
578
  else:
575
579
  new_records.append(obj)
@@ -579,7 +583,7 @@ class HookQuerySetMixin:
579
583
  else:
580
584
  # If no unique fields, treat all as new
581
585
  new_records = objs
582
-
586
+
583
587
  # Handle auto_now fields intelligently for upsert operations
584
588
  # Only set auto_now fields on records that will actually be created
585
589
  for obj in new_records:
@@ -619,11 +623,23 @@ class HookQuerySetMixin:
619
623
  key = tuple(getattr(db_record, field) for field in unique_fields)
620
624
  existing_db_map[key] = db_record
621
625
 
622
- # For existing records, set auto_now fields using Django's pre_save method
626
+ # For existing records, populate all fields from database and set auto_now fields
623
627
  for obj in existing_records:
624
628
  key = tuple(getattr(obj, field) for field in unique_fields)
625
629
  if key in existing_db_map:
626
- # Use Django's pre_save method to set auto_now fields (like updated_at)
630
+ db_record = existing_db_map[key]
631
+ # Copy all fields from the database record to ensure completeness
632
+ populated_fields = []
633
+ for field in model_cls._meta.local_fields:
634
+ if field.name != 'id': # Don't overwrite the ID
635
+ db_value = getattr(db_record, field.name)
636
+ if db_value is not None: # Only set non-None values
637
+ setattr(obj, field.name, db_value)
638
+ populated_fields.append(field.name)
639
+ print(f"DEBUG: Populated {len(populated_fields)} fields for existing record: {populated_fields}")
640
+ logger.debug(f"Populated {len(populated_fields)} fields for existing record: {populated_fields}")
641
+
642
+ # Now set auto_now fields using Django's pre_save method
627
643
  for field in model_cls._meta.local_fields:
628
644
  if hasattr(field, "auto_now") and field.auto_now:
629
645
  field.pre_save(obj, add=False) # add=False for updates
@@ -632,38 +648,38 @@ class HookQuerySetMixin:
632
648
 
633
649
  # Remove duplicate code since we're now handling this above
634
650
 
635
- # CRITICAL: Exclude auto_now fields from update_fields for existing records
636
- # This prevents Django from including them in the ON CONFLICT DO UPDATE clause
651
+ # CRITICAL: Handle auto_now fields intelligently for existing records
652
+ # We need to exclude them from Django's ON CONFLICT DO UPDATE clause to prevent
653
+ # Django's default behavior, but still ensure they get updated via pre_save
637
654
  if existing_records and update_fields:
638
655
  logger.debug(f"Processing {len(existing_records)} existing records with update_fields: {update_fields}")
639
-
640
- # Identify auto_now fields that should be excluded from updates
641
- auto_now_fields_to_exclude = set()
656
+
657
+ # Identify auto_now fields
658
+ auto_now_fields = set()
642
659
  for field in model_cls._meta.local_fields:
643
660
  if hasattr(field, "auto_now") and field.auto_now:
644
- auto_now_fields_to_exclude.add(field.name)
645
-
646
- logger.debug(f"Found auto_now fields: {auto_now_fields_to_exclude}")
647
-
648
- # Filter out auto_now fields from update_fields for existing records
649
- if auto_now_fields_to_exclude:
650
- # Create a filtered version of update_fields that excludes auto_now fields
651
- filtered_update_fields = [f for f in update_fields if f not in auto_now_fields_to_exclude]
652
-
653
- logger.debug(f"Filtered update_fields: {filtered_update_fields}")
654
- logger.debug(f"Excluded auto_now fields: {auto_now_fields_to_exclude}")
655
-
656
- # Store the original update_fields to restore later
661
+ auto_now_fields.add(field.name)
662
+
663
+ logger.debug(f"Found auto_now fields: {auto_now_fields}")
664
+
665
+ if auto_now_fields:
666
+ # Store original update_fields and auto_now fields for later restoration
657
667
  ctx.original_update_fields = update_fields
658
- ctx.auto_now_fields_excluded = auto_now_fields_to_exclude
659
-
660
- # Use the filtered update_fields for the database operation
661
- # This prevents Django from overwriting the timestamps during upsert
668
+ ctx.auto_now_fields = auto_now_fields
669
+
670
+ # Filter out auto_now fields from update_fields for the database operation
671
+ # This prevents Django from including them in ON CONFLICT DO UPDATE
672
+ filtered_update_fields = [f for f in update_fields if f not in auto_now_fields]
673
+
674
+ logger.debug(f"Filtered update_fields: {filtered_update_fields}")
675
+ logger.debug(f"Excluded auto_now fields: {auto_now_fields}")
676
+
677
+ # Use filtered update_fields for Django's bulk_create operation
662
678
  update_fields = filtered_update_fields
663
-
679
+
664
680
  logger.debug(f"Final update_fields for DB operation: {update_fields}")
665
681
  else:
666
- logger.debug("No auto_now fields found to exclude")
682
+ logger.debug("No auto_now fields found to handle")
667
683
  else:
668
684
  logger.debug(f"No existing records or update_fields to process. existing_records: {len(existing_records) if existing_records else 0}, update_fields: {update_fields}")
669
685
 
@@ -733,67 +749,92 @@ class HookQuerySetMixin:
733
749
  # Fire AFTER hooks
734
750
  if not bypass_hooks:
735
751
  if update_conflicts and unique_fields:
752
+ # Handle auto_now fields that were excluded from the main update
753
+ if hasattr(ctx, 'auto_now_fields') and existing_records:
754
+ logger.debug(f"Performing separate update for auto_now fields: {ctx.auto_now_fields}")
755
+
756
+ # Perform a separate bulk_update for the auto_now fields that were set via pre_save
757
+ # This ensures they get saved to the database even though they were excluded from the main upsert
758
+ try:
759
+ # Use Django's base manager to bypass hooks and ensure the update happens
760
+ base_manager = model_cls._base_manager
761
+ auto_now_update_result = base_manager.bulk_update(
762
+ existing_records, list(ctx.auto_now_fields)
763
+ )
764
+ logger.debug(f"Auto_now fields update completed with result: {auto_now_update_result}")
765
+ except Exception as e:
766
+ logger.error(f"Failed to update auto_now fields: {e}")
767
+ # Don't raise the exception - the main operation succeeded
768
+
736
769
  # Restore original update_fields if we modified them
737
770
  if hasattr(ctx, 'original_update_fields'):
738
771
  logger.debug(f"Restoring original update_fields: {ctx.original_update_fields}")
739
772
  update_fields = ctx.original_update_fields
740
773
  delattr(ctx, 'original_update_fields')
741
- delattr(ctx, 'auto_now_fields_excluded')
774
+ if hasattr(ctx, 'auto_now_fields'):
775
+ delattr(ctx, 'auto_now_fields')
742
776
  logger.debug(f"Restored update_fields: {update_fields}")
743
-
744
- # For upsert operations, we need to determine which records were actually created vs updated
745
- # Use the same logic as before to separate records
746
- existing_records = []
747
- new_records = []
748
-
749
- # Build a filter to check which records already exist
750
- unique_values = []
751
- for obj in objs:
752
- unique_value = {}
753
- for field_name in unique_fields:
754
- if hasattr(obj, field_name):
755
- unique_value[field_name] = getattr(obj, field_name)
756
- if unique_value:
757
- unique_values.append(unique_value)
758
-
759
- if unique_values:
760
- # Query the database to see which records already exist
761
- from django.db.models import Q
762
- existing_filters = Q()
763
- for unique_value in unique_values:
764
- filter_kwargs = {}
765
- for field_name, value in unique_value.items():
766
- filter_kwargs[field_name] = value
767
- existing_filters |= Q(**filter_kwargs)
768
-
769
- existing_pks = set(
770
- model_cls.objects.filter(existing_filters).values_list('pk', flat=True)
771
- )
772
-
773
- # Separate records based on whether they already exist
777
+
778
+ # For upsert operations, reuse the existing/new records determination from BEFORE hooks
779
+ # This avoids duplicate queries and improves performance
780
+ if hasattr(ctx, 'upsert_existing_records') and hasattr(ctx, 'upsert_new_records'):
781
+ existing_records = ctx.upsert_existing_records
782
+ new_records = ctx.upsert_new_records
783
+ logger.debug(f"Reusing upsert record classification from BEFORE hooks: {len(existing_records)} existing, {len(new_records)} new")
784
+ else:
785
+ # Fallback: determine records that actually exist after bulk operation
786
+ logger.warning("Upsert record classification not found in context, performing fallback query")
787
+ existing_records = []
788
+ new_records = []
789
+
790
+ # Build a filter to check which records now exist
791
+ unique_values = []
774
792
  for obj in objs:
775
- obj_unique_value = {}
793
+ unique_value = {}
776
794
  for field_name in unique_fields:
777
795
  if hasattr(obj, field_name):
778
- obj_unique_value[field_name] = getattr(obj, field_name)
779
-
780
- # Check if this record already exists
781
- if obj_unique_value:
782
- existing_q = Q()
783
- for field_name, value in obj_unique_value.items():
784
- existing_q &= Q(**{field_name: value})
785
-
786
- if model_cls.objects.filter(existing_q).exists():
787
- existing_records.append(obj)
796
+ unique_value[field_name] = getattr(obj, field_name)
797
+ if unique_value:
798
+ unique_values.append(unique_value)
799
+
800
+ if unique_values:
801
+ # Query the database to see which records exist after bulk operation
802
+ from django.db.models import Q
803
+ existing_filters = Q()
804
+ for unique_value in unique_values:
805
+ filter_kwargs = {}
806
+ for field_name, value in unique_value.items():
807
+ filter_kwargs[field_name] = value
808
+ existing_filters |= Q(**filter_kwargs)
809
+
810
+ # Get all existing records in one query and create a lookup set
811
+ existing_records_lookup = set()
812
+ for existing_record in model_cls.objects.filter(existing_filters).values_list(*unique_fields):
813
+ # Convert tuple to a hashable key for lookup
814
+ existing_records_lookup.add(existing_record)
815
+
816
+ # Separate records based on whether they now exist
817
+ for obj in objs:
818
+ obj_unique_value = {}
819
+ for field_name in unique_fields:
820
+ if hasattr(obj, field_name):
821
+ obj_unique_value[field_name] = getattr(obj, field_name)
822
+
823
+ # Check if this record exists using our bulk lookup
824
+ if obj_unique_value:
825
+ # Convert object values to tuple for comparison with existing records
826
+ obj_unique_tuple = tuple(obj_unique_value[field_name] for field_name in unique_fields)
827
+ if obj_unique_tuple in existing_records_lookup:
828
+ existing_records.append(obj)
829
+ else:
830
+ new_records.append(obj)
788
831
  else:
832
+ # If we can't determine uniqueness, treat as new
789
833
  new_records.append(obj)
790
- else:
791
- # If we can't determine uniqueness, treat as new
792
- new_records.append(obj)
793
- else:
794
- # If no unique fields, treat all as new
795
- new_records = objs
796
-
834
+ else:
835
+ # If no unique fields, treat all as new
836
+ new_records = objs
837
+
797
838
  # Run appropriate AFTER hooks based on what actually happened
798
839
  if new_records:
799
840
  engine.run(model_cls, AFTER_CREATE, new_records, ctx=ctx)
@@ -849,7 +890,8 @@ class HookQuerySetMixin:
849
890
  fields_set = set(fields)
850
891
  pk_fields = model_cls._meta.pk_fields
851
892
  auto_now_fields = []
852
- logger.debug(f"Checking for auto_now fields in {model_cls.__name__}")
893
+ custom_update_fields = [] # Fields that need pre_save() called on update
894
+ logger.debug(f"Checking for auto_now and custom update fields in {model_cls.__name__}")
853
895
  for field in model_cls._meta.local_concrete_fields:
854
896
  # Only add auto_now fields (like updated_at) that aren't already in the fields list
855
897
  # Don't include auto_now_add fields (like created_at) as they should only be set on creation
@@ -868,6 +910,13 @@ class HookQuerySetMixin:
868
910
  print(f"DEBUG: Auto_now field {field.name} already in fields list or is PK")
869
911
  elif hasattr(field, "auto_now_add") and field.auto_now_add:
870
912
  logger.debug(f"Found auto_now_add field: {field.name} (skipping)")
913
+ # Check for custom fields that might need pre_save() on update (like CurrentUserField)
914
+ elif hasattr(field, 'pre_save'):
915
+ # Only call pre_save on fields that aren't already being updated
916
+ if field.name not in fields_set and field.name not in pk_fields:
917
+ custom_update_fields.append(field)
918
+ logger.debug(f"Found custom field with pre_save: {field.name}")
919
+ print(f"DEBUG: Found custom field with pre_save: {field.name}")
871
920
 
872
921
  logger.debug(f"Auto_now fields detected: {auto_now_fields}")
873
922
  print(f"DEBUG: Auto_now fields detected: {auto_now_fields}")
@@ -884,6 +933,28 @@ class HookQuerySetMixin:
884
933
  setattr(obj, field_name, current_time)
885
934
  print(f"DEBUG: Set {field_name} to {current_time} for object {obj.pk}")
886
935
 
936
+ # Call pre_save() on custom fields that need update handling
937
+ if custom_update_fields:
938
+ logger.debug(f"Calling pre_save() on custom update fields: {[f.name for f in custom_update_fields]}")
939
+ print(f"DEBUG: Calling pre_save() on custom update fields: {[f.name for f in custom_update_fields]}")
940
+ for obj in objs:
941
+ for field in custom_update_fields:
942
+ try:
943
+ # Call pre_save with add=False to indicate this is an update
944
+ new_value = field.pre_save(obj, add=False)
945
+ # Only update the field if pre_save returned a new value
946
+ if new_value is not None:
947
+ setattr(obj, field.name, new_value)
948
+ # Add this field to the update fields if it's not already there
949
+ if field.name not in fields_set:
950
+ fields_set.add(field.name)
951
+ fields.append(field.name)
952
+ logger.debug(f"Custom field {field.name} updated via pre_save() for object {obj.pk}")
953
+ print(f"DEBUG: Custom field {field.name} updated via pre_save() for object {obj.pk}")
954
+ except Exception as e:
955
+ logger.warning(f"Failed to call pre_save() on custom field {field.name}: {e}")
956
+ print(f"DEBUG: Failed to call pre_save() on custom field {field.name}: {e}")
957
+
887
958
  # Handle MTI models differently
888
959
  if is_mti:
889
960
  result = self._mti_bulk_update(objs, fields, **kwargs)
@@ -1238,13 +1309,14 @@ class HookQuerySetMixin:
1238
1309
 
1239
1310
  return child_obj
1240
1311
 
1241
- def _mti_bulk_update(self, objs, fields, **kwargs):
1312
+ def _mti_bulk_update(self, objs, fields, field_groups=None, inheritance_chain=None, **kwargs):
1242
1313
  """
1243
1314
  Custom bulk update implementation for MTI models.
1244
1315
  Updates each table in the inheritance chain efficiently using Django's batch_size.
1245
1316
  """
1246
1317
  model_cls = self.model
1247
- inheritance_chain = self._get_inheritance_chain()
1318
+ if inheritance_chain is None:
1319
+ inheritance_chain = self._get_inheritance_chain()
1248
1320
 
1249
1321
  # Remove custom hook kwargs before passing to Django internals
1250
1322
  django_kwargs = {
@@ -1259,13 +1331,24 @@ class HookQuerySetMixin:
1259
1331
  "Inheritance chain too deep - possible infinite recursion detected"
1260
1332
  )
1261
1333
 
1262
- # Handle auto_now fields by calling pre_save on objects
1263
- # Check all models in the inheritance chain for auto_now fields
1334
+ # Handle auto_now fields and custom fields by calling pre_save on objects
1335
+ # Check all models in the inheritance chain for auto_now and custom fields
1336
+ custom_update_fields = []
1264
1337
  for obj in objs:
1265
1338
  for model in inheritance_chain:
1266
1339
  for field in model._meta.local_fields:
1267
1340
  if hasattr(field, "auto_now") and field.auto_now:
1268
1341
  field.pre_save(obj, add=False)
1342
+ # Check for custom fields that might need pre_save() on update (like CurrentUserField)
1343
+ elif hasattr(field, 'pre_save') and field.name not in fields:
1344
+ try:
1345
+ new_value = field.pre_save(obj, add=False)
1346
+ if new_value is not None:
1347
+ setattr(obj, field.name, new_value)
1348
+ custom_update_fields.append(field.name)
1349
+ logger.debug(f"Custom field {field.name} updated via pre_save() for MTI object {obj.pk}")
1350
+ except Exception as e:
1351
+ logger.warning(f"Failed to call pre_save() on custom field {field.name} in MTI: {e}")
1269
1352
 
1270
1353
  # Add auto_now fields to the fields list so they get updated in the database
1271
1354
  auto_now_fields = set()
@@ -1274,20 +1357,21 @@ class HookQuerySetMixin:
1274
1357
  if hasattr(field, "auto_now") and field.auto_now:
1275
1358
  auto_now_fields.add(field.name)
1276
1359
 
1277
- # Combine original fields with auto_now fields
1278
- all_fields = list(fields) + list(auto_now_fields)
1279
-
1280
- # Group fields by model in the inheritance chain
1281
- field_groups = {}
1282
- for field_name in all_fields:
1283
- field = model_cls._meta.get_field(field_name)
1284
- # Find which model in the inheritance chain this field belongs to
1285
- for model in inheritance_chain:
1286
- if field in model._meta.local_fields:
1287
- if model not in field_groups:
1288
- field_groups[model] = []
1289
- field_groups[model].append(field_name)
1290
- break
1360
+ # Add custom fields that were updated to the fields list
1361
+ all_fields = list(fields) + list(auto_now_fields) + custom_update_fields
1362
+
1363
+ # Group fields by model in the inheritance chain (if not provided)
1364
+ if field_groups is None:
1365
+ field_groups = {}
1366
+ for field_name in all_fields:
1367
+ field = model_cls._meta.get_field(field_name)
1368
+ # Find which model in the inheritance chain this field belongs to
1369
+ for model in inheritance_chain:
1370
+ if field in model._meta.local_fields:
1371
+ if model not in field_groups:
1372
+ field_groups[model] = []
1373
+ field_groups[model].append(field_name)
1374
+ break
1291
1375
 
1292
1376
  # Process in batches
1293
1377
  batch_size = django_kwargs.get("batch_size") or len(objs)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.1.259"
3
+ version = "0.1.261"
4
4
  description = "Hook-style hooks for Django bulk operations like bulk_create and bulk_update."
5
5
  authors = ["Konrad Beck <konrad.beck@merchantcapital.co.za>"]
6
6
  readme = "README.md"