buildzr 0.0.17__tar.gz → 0.0.19__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 (46) hide show
  1. {buildzr-0.0.17 → buildzr-0.0.19}/PKG-INFO +1 -1
  2. buildzr-0.0.19/buildzr/__about__.py +1 -0
  3. {buildzr-0.0.17 → buildzr-0.0.19}/buildzr/dsl/dsl.py +60 -3
  4. {buildzr-0.0.17 → buildzr-0.0.19}/tests/test_dsl.py +275 -1
  5. {buildzr-0.0.17 → buildzr-0.0.19}/tests/test_explorer.py +2 -2
  6. buildzr-0.0.17/buildzr/__about__.py +0 -1
  7. {buildzr-0.0.17 → buildzr-0.0.19}/.gitignore +0 -0
  8. {buildzr-0.0.17 → buildzr-0.0.19}/CONTRIBUTING.md +0 -0
  9. {buildzr-0.0.17 → buildzr-0.0.19}/LICENSE.md +0 -0
  10. {buildzr-0.0.17 → buildzr-0.0.19}/README.md +0 -0
  11. {buildzr-0.0.17 → buildzr-0.0.19}/buildzr/__init__.py +0 -0
  12. {buildzr-0.0.17 → buildzr-0.0.19}/buildzr/dsl/__init__.py +0 -0
  13. {buildzr-0.0.17 → buildzr-0.0.19}/buildzr/dsl/color.py +0 -0
  14. {buildzr-0.0.17 → buildzr-0.0.19}/buildzr/dsl/explorer.py +0 -0
  15. {buildzr-0.0.17 → buildzr-0.0.19}/buildzr/dsl/expression.py +0 -0
  16. {buildzr-0.0.17 → buildzr-0.0.19}/buildzr/dsl/factory/__init__.py +0 -0
  17. {buildzr-0.0.17 → buildzr-0.0.19}/buildzr/dsl/factory/gen_id.py +0 -0
  18. {buildzr-0.0.17 → buildzr-0.0.19}/buildzr/dsl/interfaces/__init__.py +0 -0
  19. {buildzr-0.0.17 → buildzr-0.0.19}/buildzr/dsl/interfaces/interfaces.py +0 -0
  20. {buildzr-0.0.17 → buildzr-0.0.19}/buildzr/dsl/relations.py +0 -0
  21. {buildzr-0.0.17 → buildzr-0.0.19}/buildzr/encoders/__init__.py +0 -0
  22. {buildzr-0.0.17 → buildzr-0.0.19}/buildzr/encoders/encoder.py +0 -0
  23. {buildzr-0.0.17 → buildzr-0.0.19}/buildzr/models/__init__.py +0 -0
  24. {buildzr-0.0.17 → buildzr-0.0.19}/buildzr/models/generate.sh +0 -0
  25. {buildzr-0.0.17 → buildzr-0.0.19}/buildzr/models/models.py +0 -0
  26. {buildzr-0.0.17 → buildzr-0.0.19}/buildzr/sinks/__init__.py +0 -0
  27. {buildzr-0.0.17 → buildzr-0.0.19}/buildzr/sinks/interfaces.py +0 -0
  28. {buildzr-0.0.17 → buildzr-0.0.19}/buildzr/sinks/json_sink.py +0 -0
  29. {buildzr-0.0.17 → buildzr-0.0.19}/pyproject.toml +0 -0
  30. {buildzr-0.0.17 → buildzr-0.0.19}/tests/__init__.py +0 -0
  31. {buildzr-0.0.17 → buildzr-0.0.19}/tests/abstract_builder.py +0 -0
  32. {buildzr-0.0.17 → buildzr-0.0.19}/tests/samples/__init__.py +0 -0
  33. {buildzr-0.0.17 → buildzr-0.0.19}/tests/samples/component_view.py +0 -0
  34. {buildzr-0.0.17 → buildzr-0.0.19}/tests/samples/container_view.py +0 -0
  35. {buildzr-0.0.17 → buildzr-0.0.19}/tests/samples/container_view_sugar.py +0 -0
  36. {buildzr-0.0.17 → buildzr-0.0.19}/tests/samples/groups.py +0 -0
  37. {buildzr-0.0.17 → buildzr-0.0.19}/tests/samples/implied_relationships.py +0 -0
  38. {buildzr-0.0.17 → buildzr-0.0.19}/tests/samples/nested_groups.py +0 -0
  39. {buildzr-0.0.17 → buildzr-0.0.19}/tests/samples/simple.py +0 -0
  40. {buildzr-0.0.17 → buildzr-0.0.19}/tests/samples/simple_dsl.py +0 -0
  41. {buildzr-0.0.17 → buildzr-0.0.19}/tests/samples/system_context_view.py +0 -0
  42. {buildzr-0.0.17 → buildzr-0.0.19}/tests/samples/system_landscape_view.py +0 -0
  43. {buildzr-0.0.17 → buildzr-0.0.19}/tests/test_expression.py +0 -0
  44. {buildzr-0.0.17 → buildzr-0.0.19}/tests/test_typehints.py +0 -0
  45. {buildzr-0.0.17 → buildzr-0.0.19}/tests/test_views.py +0 -0
  46. {buildzr-0.0.17 → buildzr-0.0.19}/tests/test_workspaces.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: buildzr
3
- Version: 0.0.17
3
+ Version: 0.0.19
4
4
  Summary: Structurizr for the `buildzr`s 🧱⚒️
5
5
  Project-URL: homepage, https://github.com/amirulmenjeni/buildzr
6
6
  Project-URL: issues, https://github.com/amirulmenjeni/buildzr/issues
@@ -0,0 +1 @@
1
+ VERSION = "0.0.19"
@@ -129,16 +129,19 @@ class Workspace(DslWorkspaceElement):
129
129
 
130
130
  _current_workspace.reset(self._token)
131
131
 
132
- def _imply_relationships(
133
- self,
132
+ def _imply_relationships( self,
134
133
  ) -> None:
135
134
 
136
135
  """
137
136
  Process implied relationships:
138
137
  If we have relationship s >> do >> a.b, then create s >> do >> a.
139
138
  If we have relationship s.ss >> do >> a.b.c, then create s.ss >> do >> a.b and s.ss >> do >> a.
139
+ If we have relationship s.ss >> do >> a, then create s >> do >> a.
140
140
  And so on...
141
141
 
142
+ Relationships of `SoftwareSystemInstance`s and `ContainerInstance`s are
143
+ skipped.
144
+
142
145
  This process is idempotent, which means this can be called multiple times
143
146
  without duplicating similar relationships.
144
147
  """
@@ -149,12 +152,22 @@ class Workspace(DslWorkspaceElement):
149
152
  from buildzr.dsl.explorer import Explorer
150
153
 
151
154
  explorer = Explorer(self)
155
+ # Take a snapshot of relationships to avoid processing newly created ones
152
156
  relationships = list(explorer.walk_relationships())
153
157
  for relationship in relationships:
154
158
  source = relationship.source
155
159
  destination = relationship.destination
156
160
  destination_parent = destination.parent
157
161
 
162
+ if isinstance(source, (SoftwareSystemInstance, ContainerInstance)) or \
163
+ isinstance(destination, (SoftwareSystemInstance, ContainerInstance)):
164
+ continue
165
+
166
+ # Skip relationships that are already implied (have linkedRelationshipId)
167
+ if relationship.model.linkedRelationshipId is not None:
168
+ continue
169
+
170
+ # Handle case: s >> a.b => s >> a (destination is child)
158
171
  while destination_parent is not None and \
159
172
  isinstance(source, DslElement) and \
160
173
  not isinstance(source.model, buildzr.models.Workspace) and \
@@ -179,7 +192,38 @@ class Workspace(DslWorkspaceElement):
179
192
  technology=relationship.model.technology,
180
193
  )
181
194
  r.model.linkedRelationshipId = relationship.model.id
182
- destination_parent = destination_parent.parent
195
+ destination_parent = destination_parent.parent
196
+
197
+ # Handle inverse case: s.ss >> a => s >> a (source is child)
198
+ source_parent = source.parent
199
+ while source_parent is not None and \
200
+ isinstance(destination, DslElement) and \
201
+ not isinstance(destination.model, buildzr.models.Workspace) and \
202
+ not isinstance(source_parent.model, buildzr.models.Workspace) and \
203
+ not isinstance(source_parent, DslWorkspaceElement):
204
+
205
+ if source_parent is destination.parent:
206
+ break
207
+
208
+ rels = source_parent.model.relationships
209
+
210
+ # The parent source relationship might be empty
211
+ # (i.e., []).
212
+ if rels is not None:
213
+ already_exists = any(
214
+ r.destinationId == destination.model.id and
215
+ r.description == relationship.model.description and
216
+ r.technology == relationship.model.technology
217
+ for r in rels
218
+ )
219
+ if not already_exists:
220
+ r = source_parent.uses(
221
+ destination,
222
+ description=relationship.model.description,
223
+ technology=relationship.model.technology,
224
+ )
225
+ r.model.linkedRelationshipId = relationship.model.id
226
+ source_parent = source_parent.parent
183
227
 
184
228
  def person(self) -> TypedDynamicAttribute['Person']:
185
229
  return TypedDynamicAttribute['Person'](self._dynamic_attrs)
@@ -364,6 +408,7 @@ class SoftwareSystem(DslElementRelationOverrides[
364
408
  self.model.id = GenerateId.for_element()
365
409
  self.model.name = name
366
410
  self.model.description = description
411
+ self.model.relationships = []
367
412
  self.model.tags = ','.join(self._tags)
368
413
  self.model.properties = properties
369
414
 
@@ -832,6 +877,12 @@ class DeploymentEnvironment(DslDeploymentEnvironment):
832
877
  if not relationship.destinationId in other_softwares_ids:
833
878
  continue
834
879
 
880
+ if software.model.id not in software_instance_map:
881
+ continue
882
+
883
+ if relationship.destinationId not in software_instance_map:
884
+ continue
885
+
835
886
  this_software_instances = software_instance_map[software.model.id]
836
887
  other_software_instances = software_instance_map[relationship.destinationId]
837
888
 
@@ -900,6 +951,12 @@ class DeploymentEnvironment(DslDeploymentEnvironment):
900
951
  if not relationship.destinationId in other_containers_ids:
901
952
  continue
902
953
 
954
+ if container.model.id not in container_instance_map:
955
+ continue
956
+
957
+ if relationship.destinationId not in container_instance_map:
958
+ continue
959
+
903
960
  this_container_instances = container_instance_map[container.model.id]
904
961
  other_container_instances = container_instance_map[relationship.destinationId]
905
962
 
@@ -15,6 +15,7 @@ from buildzr.dsl import (
15
15
  SystemContextView,
16
16
  DeploymentEnvironment,
17
17
  DeploymentNode,
18
+ DeploymentView,
18
19
  InfrastructureNode,
19
20
  DeploymentGroup,
20
21
  SoftwareSystemInstance,
@@ -394,6 +395,53 @@ def test_implied_relationship() -> Optional[None]:
394
395
  assert w.u.model.relationships[1].id in system_context_view_relationships
395
396
  assert w.u.model.relationships[1].linkedRelationshipId == w.u.model.relationships[0].id
396
397
 
398
+ import os
399
+ os.remove('workspace.test.json')
400
+ os.remove('workspace2.test.json')
401
+
402
+ def test_inverse_implied_relationship() -> Optional[None]:
403
+ """
404
+ Test that inverse implied relationships work correctly.
405
+ When a.container >> b (child to parent), it should imply a >> b.
406
+
407
+ See: https://docs.structurizr.com/java/implied-relationships
408
+ """
409
+
410
+ with Workspace("w", implied_relationships=True) as w:
411
+ u = Person('User')
412
+ s = SoftwareSystem('System')
413
+ with s:
414
+ api = Container('API')
415
+ db = Container('Database')
416
+ api >> "Uses" >> db
417
+
418
+ # Create relationship from child to parent: s.api >> u
419
+ # This should imply s >> u
420
+ s.api >> "Notifies" >> u
421
+
422
+ # Invoke implied relationships via view
423
+ SystemContextView(
424
+ software_system_selector=s,
425
+ key='s_context',
426
+ description="System context view",
427
+ )
428
+
429
+ w.to_json('workspace.inverse.test.json')
430
+
431
+ # Check that System has an implied relationship to User
432
+ assert len(s.model.relationships) == 1
433
+ assert s.model.relationships[0].description == "Notifies"
434
+ assert s.model.relationships[0].sourceId == s.model.id
435
+ assert s.model.relationships[0].destinationId == u.model.id
436
+ assert s.model.relationships[0].linkedRelationshipId == s.api.model.relationships[1].id
437
+
438
+ # The implied relationship should appear in system context view
439
+ system_context_view_relationships = [x.id for x in w._m.views.systemContextViews[0].relationships]
440
+ assert s.model.relationships[0].id in system_context_view_relationships
441
+
442
+ import os
443
+ os.remove('workspace.inverse.test.json')
444
+
397
445
  def test_tags_on_elements() -> Optional[None]:
398
446
 
399
447
  u = Person('My User', tags={'admin'})
@@ -1099,4 +1147,230 @@ def test_json_sink_empty_views() -> Optional[None]:
1099
1147
  assert data
1100
1148
 
1101
1149
  import os
1102
- os.remove("test.json")
1150
+ os.remove("test.json")
1151
+
1152
+ def test_deployment_instance_relationships_with_implied_relationships() -> Optional[None]:
1153
+ """
1154
+ Test that deployment instance relationships are created correctly when
1155
+ implied_relationships=True, without creating duplicates.
1156
+
1157
+ This test ensures:
1158
+ 1. Container relationships automatically create ContainerInstance relationships
1159
+ 2. No duplicate instance relationships are created when implied_relationships=True
1160
+ 3. Instance relationships are only created once, even with multiple view/export calls
1161
+ """
1162
+
1163
+ with Workspace('deployment-test', implied_relationships=True) as w:
1164
+ # Create containers with relationships
1165
+ ecommerce = SoftwareSystem('E-Commerce System')
1166
+ with ecommerce:
1167
+ api_gateway = Container('API Gateway', technology='Kong')
1168
+ order_svc = Container('Order Service', technology='Node.js')
1169
+ db = Container('Database', technology='MongoDB')
1170
+
1171
+ # Define container relationships
1172
+ api_gateway >> "Routes to" >> order_svc
1173
+ order_svc >> "Stores in" >> db
1174
+
1175
+ # Create deployment with container instances
1176
+ with DeploymentEnvironment('Production') as prod:
1177
+ with DeploymentNode('AWS', technology='Cloud Provider'):
1178
+ api_gw_instance = ContainerInstance(api_gateway)
1179
+ order_instance = ContainerInstance(order_svc)
1180
+ db_instance = ContainerInstance(db)
1181
+
1182
+ # Create views and export (triggers implied relationships multiple times)
1183
+ SystemContextView(
1184
+ software_system_selector=ecommerce,
1185
+ key='test-system-context',
1186
+ description="Test System Context",
1187
+ )
1188
+
1189
+ DeploymentView(
1190
+ environment=prod,
1191
+ key='test-deployment',
1192
+ )
1193
+
1194
+ # Export multiple times to ensure idempotency
1195
+ w.to_json('test_deployment1.json')
1196
+ w.to_json('test_deployment2.json')
1197
+
1198
+ # Verify instance relationships exist
1199
+ assert api_gw_instance.model.relationships is not None
1200
+ assert order_instance.model.relationships is not None
1201
+
1202
+ # Get all instance relationships
1203
+ api_gw_rels = api_gw_instance.model.relationships
1204
+ order_rels = order_instance.model.relationships
1205
+
1206
+ # Should have exactly 1 relationship from api_gw_instance to order_instance
1207
+ api_to_order_rels = [
1208
+ r for r in api_gw_rels
1209
+ if r.destinationId == order_instance.model.id
1210
+ ]
1211
+ assert len(api_to_order_rels) == 1, f"Expected 1 relationship, found {len(api_to_order_rels)}"
1212
+ assert api_to_order_rels[0].description == "Routes to"
1213
+
1214
+ # Should have exactly 1 relationship from order_instance to db_instance
1215
+ order_to_db_rels = [
1216
+ r for r in order_rels
1217
+ if r.destinationId == db_instance.model.id
1218
+ ]
1219
+ assert len(order_to_db_rels) == 1, f"Expected 1 relationship, found {len(order_to_db_rels)}"
1220
+ assert order_to_db_rels[0].description == "Stores in"
1221
+
1222
+ # Verify linkedRelationshipId is set correctly
1223
+ assert api_to_order_rels[0].linkedRelationshipId is not None
1224
+ assert order_to_db_rels[0].linkedRelationshipId is not None
1225
+
1226
+ # Clean up
1227
+ import os
1228
+ os.remove('test_deployment1.json')
1229
+ os.remove('test_deployment2.json')
1230
+
1231
+ def test_imply_relationships_before_deployment_environment_not_crashing() -> Optional[None]:
1232
+
1233
+ with Workspace('workspace') as w:
1234
+
1235
+ with SoftwareSystem("X") as x:
1236
+
1237
+ # Notice that we don't need to specify the tags "Application" and "Database"
1238
+ # for styling -- just pass the `wa` and `db` variables directly to the `StyleElements` class.
1239
+ wa = Container("Web Application", technology="Java and Spring boot")
1240
+ db = Container("Database Schema")
1241
+
1242
+ wa >> "Reads from and writes to" >> db
1243
+
1244
+ with DeploymentEnvironment("Live") as live:
1245
+ with DeploymentNode("Amazon Web Services") as aws:
1246
+ aws.add_tags("Amazon Web Services - Cloud")
1247
+
1248
+ with DeploymentNode("US-East-1") as region:
1249
+ region.add_tags("Amazon Web Services - Region")
1250
+
1251
+ dns = InfrastructureNode(
1252
+ "DNS Router",
1253
+ description="Routes incoming requests based upon domain name.",
1254
+ technology="Route 53",
1255
+ tags={"Amazon Web Services - Route 53"}
1256
+ )
1257
+
1258
+ lb = InfrastructureNode(
1259
+ "Load Balancer",
1260
+ description="Automatically distributes incoming application traffic.",
1261
+ technology="Elastic Load Balancer",
1262
+ tags={"Amazon Web Services - Elastic Load Balancer"}
1263
+ )
1264
+
1265
+ dns >> ("Fowards requests to", "HTTP") >> lb
1266
+
1267
+ with DeploymentNode("Amazon EC2", tags={"Amazon Web Services - EC2"}) as asg:
1268
+ with DeploymentNode("Amazon EC2 - Ubuntu Server", tags={"Amazon Web Services - EC2 Instance"}):
1269
+ lb >> "Forwards requests to" >> ContainerInstance(wa)
1270
+
1271
+ with DeploymentNode("Amazon RDS", tags={"Amazon Web Services - RDS Instance"}) as rds:
1272
+ with DeploymentNode("MySQL", tags={"Amazon Web Services - RDS MySQL instance"}):
1273
+ database_instance = ContainerInstance(db)
1274
+
1275
+ DeploymentView(
1276
+ environment=live,
1277
+ key='aws-deployment-view',
1278
+ software_system_selector=x,
1279
+ title="Amazon Web Services Deployment",
1280
+ description="Deployment view of the web application on AWS",
1281
+ auto_layout='lr',
1282
+ )
1283
+
1284
+ w.to_json('amazon_web_services.json', pretty=True)
1285
+
1286
+ def test_software_system_instance_relationships_with_missing_instances() -> Optional[None]:
1287
+ """
1288
+ Test that _imply_software_system_instance_relationships doesn't crash when
1289
+ a software system has a relationship to another software system, but only
1290
+ one of them has instances deployed.
1291
+
1292
+ This reproduces the bug where:
1293
+ - E-Commerce System has instances deployed
1294
+ - Payment Provider has NO instances deployed
1295
+ - E-Commerce System -> Payment Provider relationship exists
1296
+ - Should not crash with KeyError when trying to look up Payment Provider instances
1297
+ """
1298
+
1299
+ with Workspace('test-workspace') as w:
1300
+ # Create two software systems with a relationship
1301
+ ecommerce = SoftwareSystem('E-Commerce System')
1302
+ payment_provider = SoftwareSystem('Payment Provider')
1303
+
1304
+ ecommerce >> "Processes payments via" >> payment_provider
1305
+
1306
+ # Deploy only the E-Commerce System, NOT the Payment Provider
1307
+ with DeploymentEnvironment('Production') as prod:
1308
+ with DeploymentNode('AWS'):
1309
+ ecommerce_instance = SoftwareSystemInstance(ecommerce)
1310
+
1311
+ # This should not crash - the implication happens in DeploymentEnvironment.__exit__
1312
+ # Even though ecommerce has a relationship to payment_provider,
1313
+ # payment_provider has no instances deployed
1314
+
1315
+ # If we get here without a KeyError, the test passes
1316
+ assert ecommerce_instance.model.softwareSystemId == ecommerce.model.id
1317
+
1318
+ def test_container_instance_relationships_with_missing_instances() -> Optional[None]:
1319
+ """
1320
+ Test that _imply_container_instance_relationships doesn't crash when
1321
+ a container has a relationship to another container, but only one of
1322
+ them has instances deployed.
1323
+
1324
+ This tests the same bug pattern but for containers instead of software systems.
1325
+ """
1326
+
1327
+ with Workspace('test-workspace') as w:
1328
+ # Create software system with containers that have relationships
1329
+ with SoftwareSystem('E-Commerce System') as ecommerce:
1330
+ web_app = Container('Web Application')
1331
+ api = Container('API')
1332
+ database = Container('Database')
1333
+ external_service = Container('External Payment Service')
1334
+
1335
+ # Create relationships
1336
+ web_app >> "Calls" >> api
1337
+ api >> "Stores data in" >> database
1338
+ api >> "Processes payments via" >> external_service
1339
+
1340
+ # Deploy only some containers, NOT all of them
1341
+ with DeploymentEnvironment('Production') as prod:
1342
+ with DeploymentNode('AWS'):
1343
+ web_app_instance = ContainerInstance(web_app)
1344
+ api_instance = ContainerInstance(api)
1345
+ db_instance = ContainerInstance(database)
1346
+ # Note: external_service is NOT deployed
1347
+
1348
+ # This should not crash - even though api has a relationship to external_service,
1349
+ # external_service has no instances deployed
1350
+
1351
+ # If we get here without a KeyError, the test passes
1352
+ assert web_app_instance.model.containerId == web_app.model.id
1353
+ assert api_instance.model.containerId == api.model.id
1354
+ assert db_instance.model.containerId == database.model.id
1355
+
1356
+ # Verify that the deployed instances DO have implied relationships
1357
+ # web_app_instance should have relationship to api_instance
1358
+ web_to_api_rels = [
1359
+ r for r in (web_app_instance.model.relationships or [])
1360
+ if r.destinationId == api_instance.model.id
1361
+ ]
1362
+ assert len(web_to_api_rels) == 1
1363
+
1364
+ # api_instance should have relationship to db_instance
1365
+ api_to_db_rels = [
1366
+ r for r in (api_instance.model.relationships or [])
1367
+ if r.destinationId == db_instance.model.id
1368
+ ]
1369
+ assert len(api_to_db_rels) == 1
1370
+
1371
+ # But api_instance should NOT have a relationship to external_service
1372
+ # (because it wasn't deployed)
1373
+ all_api_destinations = [
1374
+ r.destinationId for r in (api_instance.model.relationships or [])
1375
+ ]
1376
+ assert external_service.model.id not in all_api_destinations
@@ -90,7 +90,7 @@ def test_walk_relationships(workspace: Workspace) -> Optional[None]:
90
90
 
91
91
  # 5 explicit relationships.
92
92
  # Add one additional implied relationship.
93
- # And four additional from container instances for each two container instance (2x2=4 + 2x2=4 more).
93
+ # And four additional from container instances for each two container instance (2x2=4).
94
94
  #
95
95
  # Explanation: if we have containers A and B with relationship A >> "Uses" >> B,
96
96
  # and container instances ci_A_1, ci_A_2, ci_B_1, ci_B_2, then we have the
@@ -99,7 +99,7 @@ def test_walk_relationships(workspace: Workspace) -> Optional[None]:
99
99
  # ci_A_1 >> "Uses" >> ci_B_2
100
100
  # ci_A_2 >> "Uses" >> ci_B_1
101
101
  # ci_A_2 >> "Uses" >> ci_B_2
102
- assert len(relationships) == 14
102
+ assert len(relationships) == 10
103
103
 
104
104
  for relationship in relationships:
105
105
  relationship_set = (
@@ -1 +0,0 @@
1
- VERSION = "0.0.17"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes