boto3-assist 0.2.9__tar.gz → 0.2.11__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 (102) hide show
  1. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/PKG-INFO +1 -1
  2. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/pyproject.toml +1 -1
  3. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/dynamodb/dynamodb.py +1 -1
  4. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/dynamodb/dynamodb_index.py +13 -1
  5. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/dynamodb/dynamodb_model_base.py +1 -0
  6. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/utilities/serialization_utility.py +29 -3
  7. boto3_assist-0.2.11/src/boto3_assist/version.py +1 -0
  8. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/tests/dynamodb/dynamodb_model_serializtion_test.py +36 -0
  9. boto3_assist-0.2.11/tests/dynamodb/models/user_required_fields_model.py +74 -0
  10. boto3_assist-0.2.9/src/boto3_assist/version.py +0 -1
  11. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/.env.docker +0 -0
  12. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/.env.docker.001 +0 -0
  13. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/.env.docker.nosql.workbench +0 -0
  14. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/.env.unittest +0 -0
  15. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/.gitignore +0 -0
  16. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/.vscode/launch.json +0 -0
  17. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/.vscode/settings.json +0 -0
  18. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/.vscode/tasks.json +0 -0
  19. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/LICENSE-EXPLAINED.txt +0 -0
  20. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/LICENSE.txt +0 -0
  21. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/README.md +0 -0
  22. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/aws_regions_with_status.csv +0 -0
  23. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/aws_regions_with_status.json +0 -0
  24. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/devops/build.py +0 -0
  25. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/devops/readme.md +0 -0
  26. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/examples/__init__.py +0 -0
  27. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/examples/cloudwatch/log_report.py +0 -0
  28. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/examples/dynamodb/models/order_item_model.py +0 -0
  29. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/examples/dynamodb/models/order_model.py +0 -0
  30. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/examples/dynamodb/models/product_model.py +0 -0
  31. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/examples/dynamodb/models/user_model.py +0 -0
  32. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/examples/dynamodb/models/user_post_model.py +0 -0
  33. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/examples/dynamodb/order_example/main.py +0 -0
  34. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/examples/dynamodb/order_example/products.json +0 -0
  35. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/examples/dynamodb/services/order_item_service.py +0 -0
  36. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/examples/dynamodb/services/order_service.py +0 -0
  37. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/examples/dynamodb/services/product_service.py +0 -0
  38. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/examples/dynamodb/services/table_service.py +0 -0
  39. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/examples/dynamodb/services/user_post_service.py +0 -0
  40. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/examples/dynamodb/services/user_service.py +0 -0
  41. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/examples/dynamodb/services/user_service_client_example.py +0 -0
  42. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/examples/dynamodb/services/user_service_resource_example.py +0 -0
  43. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/examples/dynamodb/user_post_example/main.py +0 -0
  44. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/examples/ec2/regions_report.py +0 -0
  45. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/module-headers.txt +0 -0
  46. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/mypy.ini +0 -0
  47. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/requirements-dev.txt +0 -0
  48. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/requirements.txt +0 -0
  49. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/run-checks.sh +0 -0
  50. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/__init__.py +0 -0
  51. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/boto3session.py +0 -0
  52. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/cloudwatch/cloudwatch_connection.py +0 -0
  53. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/cloudwatch/cloudwatch_connection_tracker.py +0 -0
  54. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/cloudwatch/cloudwatch_log_connection.py +0 -0
  55. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/cloudwatch/cloudwatch_logs.py +0 -0
  56. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/cloudwatch/cloudwatch_query.py +0 -0
  57. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/connection.py +0 -0
  58. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/connection_tracker.py +0 -0
  59. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/dynamodb/dynamodb_connection.py +0 -0
  60. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/dynamodb/dynamodb_connection_tracker.py +0 -0
  61. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/dynamodb/dynamodb_helpers.py +0 -0
  62. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/dynamodb/dynamodb_importer.py +0 -0
  63. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/dynamodb/dynamodb_iservice.py +0 -0
  64. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/dynamodb/dynamodb_key.py +0 -0
  65. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/dynamodb/dynamodb_model_base_interfaces.py +0 -0
  66. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/dynamodb/dynamodb_reindexer.py +0 -0
  67. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/dynamodb/dynamodb_reserved_words.py +0 -0
  68. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/dynamodb/dynamodb_reserved_words.txt +0 -0
  69. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/dynamodb/readme.md +0 -0
  70. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/dynamodb/troubleshooting.md +0 -0
  71. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/ec2/ec2_connection.py +0 -0
  72. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/environment_services/__init__.py +0 -0
  73. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/environment_services/environment_loader.py +0 -0
  74. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/environment_services/environment_variables.py +0 -0
  75. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/errors/custom_exceptions.py +0 -0
  76. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/s3/s3.py +0 -0
  77. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/s3/s3_connection.py +0 -0
  78. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/utilities/datetime_utility.py +0 -0
  79. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/utilities/file_operations.py +0 -0
  80. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/utilities/http_utility.py +0 -0
  81. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/utilities/logging_utility.py +0 -0
  82. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/src/boto3_assist/utilities/string_utility.py +0 -0
  83. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/tests/__init__.py +0 -0
  84. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/tests/__top/__init__.py +0 -0
  85. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/tests/dynamodb/__init__.py +0 -0
  86. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/tests/dynamodb/dynamodb_model_base_test.py +0 -0
  87. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/tests/dynamodb/dynamodb_model_projections_test.py +0 -0
  88. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/tests/dynamodb/dynamodb_moto_sorting_test.py +0 -0
  89. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/tests/dynamodb/dynamodb_reindex_test.py +0 -0
  90. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/tests/dynamodb/models/cms/base.py +0 -0
  91. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/tests/dynamodb/models/cms/content_block.py +0 -0
  92. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/tests/dynamodb/models/cms/page.py +0 -0
  93. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/tests/dynamodb/models/cms/template.py +0 -0
  94. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/tests/dynamodb/models/simple_model.py +0 -0
  95. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/tests/dynamodb/models/user_model.py +0 -0
  96. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/tests/examples_test/__init__.py +0 -0
  97. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/tests/examples_test/user_service_test.py +0 -0
  98. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/tests/s3/__init__.py +0 -0
  99. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/tests/s3/files/test.txt +0 -0
  100. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/tests/s3/s3_file_upload_test.py +0 -0
  101. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/tests/utilities/__init__.py +0 -0
  102. {boto3_assist-0.2.9 → boto3_assist-0.2.11}/tests/utilities/serialization_utility_test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boto3_assist
3
- Version: 0.2.9
3
+ Version: 0.2.11
4
4
  Summary: Additional boto3 wrappers to make your life a little easier
5
5
  Author-email: Eric Wilson <boto3-assist@geekcafe.com>
6
6
  License-File: LICENSE-EXPLAINED.txt
@@ -7,7 +7,7 @@ packages = ["src/boto3_assist"]
7
7
 
8
8
  [project]
9
9
  name = "boto3_assist"
10
- version = "0.2.9"
10
+ version = "0.2.11"
11
11
 
12
12
  authors = [
13
13
  { name="Eric Wilson", email="boto3-assist@geekcafe.com" }
@@ -94,7 +94,7 @@ class DynamoDB(DynamoDBConnection):
94
94
  except Exception as e: # pylint: disable=w0718
95
95
  logger.exception(e)
96
96
  raise RuntimeError(
97
- "An error occured during model converation. The entry was not saved."
97
+ "An error occured during model converation. The entry was not saved. "
98
98
  ) from e
99
99
 
100
100
  if isinstance(item, dict):
@@ -28,6 +28,14 @@ class DynamoDBIndexes:
28
28
  def add_primary(self, index: DynamoDBIndex):
29
29
  """Add an index"""
30
30
  index.name = DynamoDBIndexes.PRIMARY_INDEX
31
+
32
+ if index.name in self.__indexes:
33
+ raise ValueError(
34
+ f"The index {index.name} is already defined in your model somewhere. "
35
+ "This error is generated to protect you from unforseen issues. "
36
+ "If you models are inheriting from other models, you may have the primary defined twice."
37
+ )
38
+
31
39
  self.__indexes[DynamoDBIndexes.PRIMARY_INDEX] = index
32
40
 
33
41
  def add_secondary(self, index: DynamoDBIndex):
@@ -37,7 +45,11 @@ class DynamoDBIndexes:
37
45
 
38
46
  # if the index already exists, raise an exception
39
47
  if index.name in self.__indexes:
40
- raise ValueError(f"Index {index.name} already exists")
48
+ raise ValueError(
49
+ f"The index {index.name} is already defined in your model somewhere. "
50
+ "This error is generated to protect you from unforseen issues. "
51
+ "If you models are inheriting from other models, you may have the primary defined twice."
52
+ )
41
53
  if index.name == DynamoDBIndexes.PRIMARY_INDEX:
42
54
  raise ValueError(f"Index {index.name} is reserved for the primary index")
43
55
  if index.partition_key is None:
@@ -49,6 +49,7 @@ class DynamoDBModelBase:
49
49
  self.__indexes: DynamoDBIndexes | None = None
50
50
  self.__reserved_words: DynamoDBReservedWords = DynamoDBReservedWords()
51
51
  self.__auto_generate_projections: bool = auto_generate_projections
52
+ self.__actively_serializing_data__: bool = False
52
53
 
53
54
  @property
54
55
  @exclude_from_serialization
@@ -67,7 +67,6 @@ class Serialization:
67
67
  raise ValueError("Unable to convert object to dictionary")
68
68
 
69
69
  @staticmethod
70
- @tracer.capture_method
71
70
  def map(source: object, target: T) -> T | None:
72
71
  """Map an object from one object to another"""
73
72
  source_dict: dict | object
@@ -80,7 +79,6 @@ class Serialization:
80
79
  return Serialization.load_properties(source_dict, target=target)
81
80
 
82
81
  @staticmethod
83
- @tracer.capture_method
84
82
  def load_properties(source: dict, target: T) -> T | None:
85
83
  """
86
84
  Converts a source to an object
@@ -93,8 +91,11 @@ class Serialization:
93
91
  if hasattr(source, "__dict__"):
94
92
  source = source.__dict__
95
93
 
94
+ if hasattr(target, "__actively_serializing_data__"):
95
+ setattr(target, "__actively_serializing_data__", True)
96
+
96
97
  for key, value in source.items():
97
- if hasattr(target, key):
98
+ if Serialization.has_attribute(target, key): # hasattr(target, key):
98
99
  attr = getattr(target, key)
99
100
  if isinstance(attr, (int, float, str, bool, type(None))):
100
101
  try:
@@ -116,4 +117,29 @@ class Serialization:
116
117
  Serialization.load_properties(value, attr)
117
118
  else:
118
119
  setattr(target, key, value)
120
+
121
+ if hasattr(target, "__actively_serializing_data__"):
122
+ setattr(target, "__actively_serializing_data__", False)
123
+
119
124
  return target
125
+
126
+ @staticmethod
127
+ def has_attribute(obj: object, attribute_name: str) -> bool:
128
+ """Check if an object has an attribute"""
129
+ try:
130
+ return hasattr(obj, attribute_name)
131
+ except AttributeError:
132
+ return False
133
+ except Exception as e: # pylint: disable=w0718
134
+ raise RuntimeError(
135
+ "Failed to serialize the object. \n"
136
+ "You may have some validation that is preventing this routine "
137
+ "from completing. Such as a None checker on a getter. \n\n"
138
+ "To work around this create a boolean (bool) property named __actively_serializing_data__. \n"
139
+ "e.g. self.__actively_serializing_data__: bool = False\n\n"
140
+ "Only issue/raise your exception if __actively_serializing_data__ is not True. \n\n"
141
+ "e.g. if not self.some_propert and not self.__actively_serializing_data__:\n"
142
+ ' raise ValueError("some_property must be set")\n\n'
143
+ "This procedure will update the property from False to True while serializing, "
144
+ "then back to False once serialization is complete. "
145
+ ) from e
@@ -0,0 +1 @@
1
+ __version__ = '0.2.11'
@@ -8,6 +8,7 @@ import unittest
8
8
 
9
9
 
10
10
  from tests.dynamodb.models.user_model import User
11
+ from tests.dynamodb.models.user_required_fields_model import User as User2
11
12
 
12
13
 
13
14
  class DynamoDBModelSerializationUnitTest(unittest.TestCase):
@@ -48,3 +49,38 @@ class DynamoDBModelSerializationUnitTest(unittest.TestCase):
48
49
  user: User = User()
49
50
  dictionary = user.to_resource_dictionary()
50
51
  self.assertIsInstance(dictionary, dict)
52
+
53
+ def test_required_fields(self):
54
+ """Test Required Fields"""
55
+ # Arrange
56
+ data = {
57
+ "id": "123456",
58
+ "first_name": "John",
59
+ "age": 30,
60
+ "email": "john@example.com",
61
+ }
62
+
63
+ # Act
64
+ serialized_data: User2 = User2().map(data)
65
+
66
+ # Assert
67
+ self.assertEqual(serialized_data.first_name, "John")
68
+ self.assertEqual(serialized_data.age, 30)
69
+ self.assertEqual(serialized_data.email, "john@example.com")
70
+ self.assertIsInstance(serialized_data, User2)
71
+
72
+ key = serialized_data.indexes.primary.key()
73
+ self.assertIsInstance(key, dict)
74
+
75
+ dictionary = serialized_data.to_resource_dictionary()
76
+ self.assertIsInstance(dictionary, dict)
77
+ keys = dictionary.keys()
78
+ self.assertIn("first_name", keys)
79
+ self.assertIn("age", keys)
80
+ self.assertIn("email", keys)
81
+ self.assertIn("id", keys)
82
+ self.assertNotIn("T", keys)
83
+
84
+ user: User = User()
85
+ dictionary = user.to_resource_dictionary()
86
+ self.assertIsInstance(dictionary, dict)
@@ -0,0 +1,74 @@
1
+ """
2
+ Geek Cafe, LLC
3
+ Maintainers: Eric Wilson
4
+ MIT License. See Project Root for the license information.
5
+ """
6
+
7
+ from typing import Optional
8
+
9
+ from boto3_assist.dynamodb.dynamodb_model_base import DynamoDBModelBase
10
+ from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex
11
+ from boto3_assist.dynamodb.dynamodb_key import DynamoDBKey
12
+
13
+
14
+ class User(DynamoDBModelBase):
15
+ """User Model"""
16
+
17
+ def __init__(
18
+ self,
19
+ id: Optional[str] = None, # pylint: disable=redefined-builtin
20
+ ):
21
+ super().__init__(self)
22
+ self.id: Optional[str] = id
23
+ self.first_name: Optional[str] = None
24
+ self.last_name: Optional[str] = None
25
+ self.age: Optional[int] = None
26
+ self.__email: Optional[str] = None
27
+ # known reserved words
28
+ self.status: Optional[str] = None
29
+
30
+ self.__setup_indexes()
31
+
32
+ def __setup_indexes(self):
33
+ primay: DynamoDBIndex = DynamoDBIndex()
34
+ primay.partition_key.attribute_name = "pk"
35
+ # allows for a wild card search on all "sites"
36
+ primay.partition_key.value = lambda: DynamoDBKey.build_key(("user", self.id))
37
+ primay.sort_key.attribute_name = "sk"
38
+ primay.sort_key.value = lambda: DynamoDBKey.build_key(("user", self.id))
39
+ self.indexes.add_primary(primay)
40
+
41
+ gsi0: DynamoDBIndex = DynamoDBIndex(index_name="gsi0")
42
+ gsi0.partition_key.attribute_name = "gsi0_pk"
43
+ gsi0.partition_key.value = lambda: DynamoDBKey.build_key(("users", None))
44
+ gsi0.sort_key.attribute_name = "gsi0_sk"
45
+ gsi0.sort_key.value = lambda: DynamoDBKey.build_key(("email", self.email))
46
+ self.indexes.add_secondary(gsi0)
47
+
48
+ gsi1: DynamoDBIndex = DynamoDBIndex(index_name="gsi1")
49
+ gsi1.partition_key.attribute_name = "gsi1_pk"
50
+ gsi1.partition_key.value = lambda: DynamoDBKey.build_key(("users", None))
51
+ gsi1.sort_key.attribute_name = "gsi1_sk"
52
+ gsi1.sort_key.value = lambda: DynamoDBKey.build_key(
53
+ ("lastname", self.last_name), ("firstname", self.first_name)
54
+ )
55
+ self.indexes.add_secondary(gsi1)
56
+
57
+ gsi0: DynamoDBIndex = DynamoDBIndex(index_name="gsi2")
58
+ gsi0.partition_key.attribute_name = "gsi2_pk"
59
+ gsi0.partition_key.value = lambda: DynamoDBKey.build_key(("users", None))
60
+ gsi0.sort_key.attribute_name = "gsi2_sk"
61
+ gsi0.sort_key.value = lambda: DynamoDBKey.build_key(
62
+ ("firstname", self.first_name), ("lastname", self.last_name)
63
+ )
64
+ self.indexes.add_secondary(gsi0)
65
+
66
+ @property
67
+ def email(self) -> str:
68
+ if not self.__email and not self.__actively_serializing_data__:
69
+ raise ValueError("Email is required.")
70
+ return self.__email
71
+
72
+ @email.setter
73
+ def email(self, value: str):
74
+ self.__email = value.lower()
@@ -1 +0,0 @@
1
- __version__ = '0.2.9'
File without changes
File without changes
File without changes
File without changes
File without changes