buildzr 0.0.18__tar.gz → 0.0.20__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.18 → buildzr-0.0.20}/PKG-INFO +1 -1
  2. buildzr-0.0.20/buildzr/__about__.py +1 -0
  3. {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/dsl/dsl.py +109 -3
  4. {buildzr-0.0.18 → buildzr-0.0.20}/tests/test_dsl.py +407 -0
  5. buildzr-0.0.18/buildzr/__about__.py +0 -1
  6. {buildzr-0.0.18 → buildzr-0.0.20}/.gitignore +0 -0
  7. {buildzr-0.0.18 → buildzr-0.0.20}/CONTRIBUTING.md +0 -0
  8. {buildzr-0.0.18 → buildzr-0.0.20}/LICENSE.md +0 -0
  9. {buildzr-0.0.18 → buildzr-0.0.20}/README.md +0 -0
  10. {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/__init__.py +0 -0
  11. {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/dsl/__init__.py +0 -0
  12. {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/dsl/color.py +0 -0
  13. {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/dsl/explorer.py +0 -0
  14. {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/dsl/expression.py +0 -0
  15. {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/dsl/factory/__init__.py +0 -0
  16. {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/dsl/factory/gen_id.py +0 -0
  17. {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/dsl/interfaces/__init__.py +0 -0
  18. {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/dsl/interfaces/interfaces.py +0 -0
  19. {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/dsl/relations.py +0 -0
  20. {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/encoders/__init__.py +0 -0
  21. {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/encoders/encoder.py +0 -0
  22. {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/models/__init__.py +0 -0
  23. {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/models/generate.sh +0 -0
  24. {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/models/models.py +0 -0
  25. {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/sinks/__init__.py +0 -0
  26. {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/sinks/interfaces.py +0 -0
  27. {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/sinks/json_sink.py +0 -0
  28. {buildzr-0.0.18 → buildzr-0.0.20}/pyproject.toml +0 -0
  29. {buildzr-0.0.18 → buildzr-0.0.20}/tests/__init__.py +0 -0
  30. {buildzr-0.0.18 → buildzr-0.0.20}/tests/abstract_builder.py +0 -0
  31. {buildzr-0.0.18 → buildzr-0.0.20}/tests/samples/__init__.py +0 -0
  32. {buildzr-0.0.18 → buildzr-0.0.20}/tests/samples/component_view.py +0 -0
  33. {buildzr-0.0.18 → buildzr-0.0.20}/tests/samples/container_view.py +0 -0
  34. {buildzr-0.0.18 → buildzr-0.0.20}/tests/samples/container_view_sugar.py +0 -0
  35. {buildzr-0.0.18 → buildzr-0.0.20}/tests/samples/groups.py +0 -0
  36. {buildzr-0.0.18 → buildzr-0.0.20}/tests/samples/implied_relationships.py +0 -0
  37. {buildzr-0.0.18 → buildzr-0.0.20}/tests/samples/nested_groups.py +0 -0
  38. {buildzr-0.0.18 → buildzr-0.0.20}/tests/samples/simple.py +0 -0
  39. {buildzr-0.0.18 → buildzr-0.0.20}/tests/samples/simple_dsl.py +0 -0
  40. {buildzr-0.0.18 → buildzr-0.0.20}/tests/samples/system_context_view.py +0 -0
  41. {buildzr-0.0.18 → buildzr-0.0.20}/tests/samples/system_landscape_view.py +0 -0
  42. {buildzr-0.0.18 → buildzr-0.0.20}/tests/test_explorer.py +0 -0
  43. {buildzr-0.0.18 → buildzr-0.0.20}/tests/test_expression.py +0 -0
  44. {buildzr-0.0.18 → buildzr-0.0.20}/tests/test_typehints.py +0 -0
  45. {buildzr-0.0.18 → buildzr-0.0.20}/tests/test_views.py +0 -0
  46. {buildzr-0.0.18 → buildzr-0.0.20}/tests/test_workspaces.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: buildzr
3
- Version: 0.0.18
3
+ Version: 0.0.20
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.20"
@@ -129,14 +129,14 @@ 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
142
  Relationships of `SoftwareSystemInstance`s and `ContainerInstance`s are
@@ -152,6 +152,7 @@ class Workspace(DslWorkspaceElement):
152
152
  from buildzr.dsl.explorer import Explorer
153
153
 
154
154
  explorer = Explorer(self)
155
+ # Take a snapshot of relationships to avoid processing newly created ones
155
156
  relationships = list(explorer.walk_relationships())
156
157
  for relationship in relationships:
157
158
  source = relationship.source
@@ -162,6 +163,11 @@ class Workspace(DslWorkspaceElement):
162
163
  isinstance(destination, (SoftwareSystemInstance, ContainerInstance)):
163
164
  continue
164
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)
165
171
  while destination_parent is not None and \
166
172
  isinstance(source, DslElement) and \
167
173
  not isinstance(source.model, buildzr.models.Workspace) and \
@@ -186,7 +192,38 @@ class Workspace(DslWorkspaceElement):
186
192
  technology=relationship.model.technology,
187
193
  )
188
194
  r.model.linkedRelationshipId = relationship.model.id
189
- 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
190
227
 
191
228
  def person(self) -> TypedDynamicAttribute['Person']:
192
229
  return TypedDynamicAttribute['Person'](self._dynamic_attrs)
@@ -371,6 +408,7 @@ class SoftwareSystem(DslElementRelationOverrides[
371
408
  self.model.id = GenerateId.for_element()
372
409
  self.model.name = name
373
410
  self.model.description = description
411
+ self.model.relationships = []
374
412
  self.model.tags = ','.join(self._tags)
375
413
  self.model.properties = properties
376
414
 
@@ -804,6 +842,10 @@ class DeploymentEnvironment(DslDeploymentEnvironment):
804
842
  new relationship between those software system instances.
805
843
 
806
844
  These implied relationships are used in `DeploymentView`.
845
+
846
+ Relationships are only created between instances that share at least
847
+ one common deployment group. If no deployment groups are specified,
848
+ instances are considered to be in the same default group.
807
849
  """
808
850
 
809
851
  software_instances = [
@@ -839,12 +881,25 @@ class DeploymentEnvironment(DslDeploymentEnvironment):
839
881
  if not relationship.destinationId in other_softwares_ids:
840
882
  continue
841
883
 
884
+ if software.model.id not in software_instance_map:
885
+ continue
886
+
887
+ if relationship.destinationId not in software_instance_map:
888
+ continue
889
+
842
890
  this_software_instances = software_instance_map[software.model.id]
843
891
  other_software_instances = software_instance_map[relationship.destinationId]
844
892
 
845
893
  for this_software_instance in this_software_instances:
846
894
  for other_software_instance in other_software_instances:
847
895
 
896
+ # Only create relationship if instances share a deployment group
897
+ if not self._instances_share_deployment_group(
898
+ this_software_instance,
899
+ other_software_instance
900
+ ):
901
+ continue
902
+
848
903
  already_exists = this_software_instance.model.relationships is not None and any(
849
904
  r.sourceId == this_software_instance.model.id and
850
905
  r.destinationId == other_software_instance.model.id and
@@ -862,6 +917,40 @@ class DeploymentEnvironment(DslDeploymentEnvironment):
862
917
  )
863
918
  r.model.linkedRelationshipId = relationship.id
864
919
 
920
+ def _instances_share_deployment_group(
921
+ self,
922
+ instance1: Union['ContainerInstance', 'SoftwareSystemInstance'],
923
+ instance2: Union['ContainerInstance', 'SoftwareSystemInstance']
924
+ ) -> bool:
925
+ """
926
+ Check if two deployment instances share at least one common deployment group.
927
+
928
+ If either instance has no deployment groups specified, they are considered
929
+ to be in the "default" group and can relate to all other instances without
930
+ deployment groups.
931
+
932
+ Args:
933
+ instance1: First deployment instance
934
+ instance2: Second deployment instance
935
+
936
+ Returns:
937
+ True if instances share at least one deployment group or if both have
938
+ no deployment groups specified, False otherwise.
939
+ """
940
+ groups1 = set(instance1.model.deploymentGroups or [])
941
+ groups2 = set(instance2.model.deploymentGroups or [])
942
+
943
+ # If both have no deployment groups, they can relate
944
+ if not groups1 and not groups2:
945
+ return True
946
+
947
+ # If one has groups and the other doesn't, they cannot relate
948
+ if (groups1 and not groups2) or (not groups1 and groups2):
949
+ return False
950
+
951
+ # Check if they share at least one common group
952
+ return bool(groups1.intersection(groups2))
953
+
865
954
  def _imply_container_instance_relationships(self, workspace: Workspace) -> None:
866
955
 
867
956
  """
@@ -871,6 +960,10 @@ class DeploymentEnvironment(DslDeploymentEnvironment):
871
960
  between those container instances.
872
961
 
873
962
  These implied relationships are used in `DeploymentView`.
963
+
964
+ Relationships are only created between instances that share at least
965
+ one common deployment group. If no deployment groups are specified,
966
+ instances are considered to be in the same default group.
874
967
  """
875
968
 
876
969
  from buildzr.dsl.expression import Expression
@@ -907,12 +1000,25 @@ class DeploymentEnvironment(DslDeploymentEnvironment):
907
1000
  if not relationship.destinationId in other_containers_ids:
908
1001
  continue
909
1002
 
1003
+ if container.model.id not in container_instance_map:
1004
+ continue
1005
+
1006
+ if relationship.destinationId not in container_instance_map:
1007
+ continue
1008
+
910
1009
  this_container_instances = container_instance_map[container.model.id]
911
1010
  other_container_instances = container_instance_map[relationship.destinationId]
912
1011
 
913
1012
  for this_container_instance in this_container_instances:
914
1013
  for other_container_instance in other_container_instances:
915
1014
 
1015
+ # Only create relationship if instances share a deployment group
1016
+ if not self._instances_share_deployment_group(
1017
+ this_container_instance,
1018
+ other_container_instance
1019
+ ):
1020
+ continue
1021
+
916
1022
  already_exists = this_container_instance.model.relationships is not None and any(
917
1023
  r.sourceId == this_container_instance.model.id and
918
1024
  r.destinationId == other_container_instance.model.id and
@@ -399,6 +399,49 @@ def test_implied_relationship() -> Optional[None]:
399
399
  os.remove('workspace.test.json')
400
400
  os.remove('workspace2.test.json')
401
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
+
402
445
  def test_tags_on_elements() -> Optional[None]:
403
446
 
404
447
  u = Person('My User', tags={'admin'})
@@ -1105,6 +1148,7 @@ def test_json_sink_empty_views() -> Optional[None]:
1105
1148
 
1106
1149
  import os
1107
1150
  os.remove("test.json")
1151
+
1108
1152
  def test_deployment_instance_relationships_with_implied_relationships() -> Optional[None]:
1109
1153
  """
1110
1154
  Test that deployment instance relationships are created correctly when
@@ -1183,3 +1227,366 @@ def test_deployment_instance_relationships_with_implied_relationships() -> Optio
1183
1227
  import os
1184
1228
  os.remove('test_deployment1.json')
1185
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
1377
+
1378
+ def test_container_instance_relationships_respect_deployment_groups() -> Optional[None]:
1379
+ """
1380
+ Test that container instance relationships only connect instances within
1381
+ the same deployment group.
1382
+
1383
+ When containers have relationships and are deployed with deployment groups,
1384
+ instance relationships should only be created between instances that share
1385
+ at least one common deployment group.
1386
+
1387
+ This matches the behavior in Structurizr DSL where deployment groups act
1388
+ as boundaries for relationship propagation.
1389
+ """
1390
+
1391
+ with Workspace("w", scope=None) as w:
1392
+ with SoftwareSystem("Software System") as software_system:
1393
+ database = Container("Database")
1394
+ api = Container("Service API")
1395
+ api >> "Reads from and writes to" >> database
1396
+
1397
+ with DeploymentEnvironment("Production") as production:
1398
+ service_instance_1 = DeploymentGroup("Service Instance 1")
1399
+ service_instance_2 = DeploymentGroup("Service Instance 2")
1400
+
1401
+ with DeploymentNode("Server 1") as server_1:
1402
+ api_instance_1 = ContainerInstance(api, [service_instance_1])
1403
+ with DeploymentNode("Database Server"):
1404
+ db_instance_1 = ContainerInstance(database, [service_instance_1])
1405
+
1406
+ with DeploymentNode("Server 2") as server_2:
1407
+ api_instance_2 = ContainerInstance(api, [service_instance_2])
1408
+ with DeploymentNode("Database Server"):
1409
+ db_instance_2 = ContainerInstance(database, [service_instance_2])
1410
+
1411
+ # Verify deployment group assignments
1412
+ assert api_instance_1.model.deploymentGroups == ["Service Instance 1"]
1413
+ assert db_instance_1.model.deploymentGroups == ["Service Instance 1"]
1414
+ assert api_instance_2.model.deploymentGroups == ["Service Instance 2"]
1415
+ assert db_instance_2.model.deploymentGroups == ["Service Instance 2"]
1416
+
1417
+ # Check that api_instance_1 only has relationship to db_instance_1 (same group)
1418
+ api_1_relationships = [
1419
+ r for r in (api_instance_1.model.relationships or [])
1420
+ if r.description == "Reads from and writes to"
1421
+ ]
1422
+ assert len(api_1_relationships) == 1, f"Expected 1 relationship, found {len(api_1_relationships)}"
1423
+ assert api_1_relationships[0].destinationId == db_instance_1.model.id
1424
+ assert api_1_relationships[0].destinationId != db_instance_2.model.id
1425
+
1426
+ # Check that api_instance_2 only has relationship to db_instance_2 (same group)
1427
+ api_2_relationships = [
1428
+ r for r in (api_instance_2.model.relationships or [])
1429
+ if r.description == "Reads from and writes to"
1430
+ ]
1431
+ assert len(api_2_relationships) == 1, f"Expected 1 relationship, found {len(api_2_relationships)}"
1432
+ assert api_2_relationships[0].destinationId == db_instance_2.model.id
1433
+ assert api_2_relationships[0].destinationId != db_instance_1.model.id
1434
+
1435
+ # Verify no cross-group relationships exist
1436
+ all_api_1_destinations = [r.destinationId for r in (api_instance_1.model.relationships or [])]
1437
+ all_api_2_destinations = [r.destinationId for r in (api_instance_2.model.relationships or [])]
1438
+
1439
+ assert db_instance_2.model.id not in all_api_1_destinations, "api_instance_1 should not connect to db_instance_2"
1440
+ assert db_instance_1.model.id not in all_api_2_destinations, "api_instance_2 should not connect to db_instance_1"
1441
+
1442
+ def test_software_system_instance_relationships_respect_deployment_groups() -> Optional[None]:
1443
+ """
1444
+ Test that software system instance relationships only connect instances
1445
+ within the same deployment group.
1446
+
1447
+ When software systems have relationships and are deployed with deployment
1448
+ groups, instance relationships should only be created between instances
1449
+ that share at least one common deployment group.
1450
+ """
1451
+
1452
+ with Workspace("w", scope=None) as w:
1453
+ api_system = SoftwareSystem("API System")
1454
+ db_system = SoftwareSystem("Database System")
1455
+
1456
+ api_system >> "Connects to" >> db_system
1457
+
1458
+ with DeploymentEnvironment("Production") as production:
1459
+ region_1 = DeploymentGroup("Region 1")
1460
+ region_2 = DeploymentGroup("Region 2")
1461
+
1462
+ with DeploymentNode("Datacenter 1") as dc1:
1463
+ api_instance_1 = SoftwareSystemInstance(api_system, [region_1])
1464
+ db_instance_1 = SoftwareSystemInstance(db_system, [region_1])
1465
+
1466
+ with DeploymentNode("Datacenter 2") as dc2:
1467
+ api_instance_2 = SoftwareSystemInstance(api_system, [region_2])
1468
+ db_instance_2 = SoftwareSystemInstance(db_system, [region_2])
1469
+
1470
+ # Verify deployment group assignments
1471
+ assert api_instance_1.model.deploymentGroups == ["Region 1"]
1472
+ assert db_instance_1.model.deploymentGroups == ["Region 1"]
1473
+ assert api_instance_2.model.deploymentGroups == ["Region 2"]
1474
+ assert db_instance_2.model.deploymentGroups == ["Region 2"]
1475
+
1476
+ # Check that api_instance_1 only has relationship to db_instance_1 (same group)
1477
+ api_1_relationships = [
1478
+ r for r in (api_instance_1.model.relationships or [])
1479
+ if r.description == "Connects to"
1480
+ ]
1481
+ assert len(api_1_relationships) == 1, f"Expected 1 relationship, found {len(api_1_relationships)}"
1482
+ assert api_1_relationships[0].destinationId == db_instance_1.model.id
1483
+ assert api_1_relationships[0].destinationId != db_instance_2.model.id
1484
+
1485
+ # Check that api_instance_2 only has relationship to db_instance_2 (same group)
1486
+ api_2_relationships = [
1487
+ r for r in (api_instance_2.model.relationships or [])
1488
+ if r.description == "Connects to"
1489
+ ]
1490
+ assert len(api_2_relationships) == 1, f"Expected 1 relationship, found {len(api_2_relationships)}"
1491
+ assert api_2_relationships[0].destinationId == db_instance_2.model.id
1492
+ assert api_2_relationships[0].destinationId != db_instance_1.model.id
1493
+
1494
+ # Verify no cross-group relationships exist
1495
+ all_api_1_destinations = [r.destinationId for r in (api_instance_1.model.relationships or [])]
1496
+ all_api_2_destinations = [r.destinationId for r in (api_instance_2.model.relationships or [])]
1497
+
1498
+ assert db_instance_2.model.id not in all_api_1_destinations, "api_instance_1 should not connect to db_instance_2"
1499
+ assert db_instance_1.model.id not in all_api_2_destinations, "api_instance_2 should not connect to db_instance_1"
1500
+
1501
+ def test_container_instance_relationships_with_multiple_shared_deployment_groups() -> Optional[None]:
1502
+ """
1503
+ Test that container instances with overlapping deployment groups can have
1504
+ relationships.
1505
+
1506
+ If two container instances share at least one deployment group, they should
1507
+ be able to have relationships even if they belong to other groups as well.
1508
+ """
1509
+
1510
+ with Workspace("w", scope=None) as w:
1511
+ with SoftwareSystem("Software System") as software_system:
1512
+ frontend = Container("Frontend")
1513
+ backend = Container("Backend")
1514
+ frontend >> "Calls" >> backend
1515
+
1516
+ with DeploymentEnvironment("Production") as production:
1517
+ group_a = DeploymentGroup("Group A")
1518
+ group_b = DeploymentGroup("Group B")
1519
+ group_shared = DeploymentGroup("Shared Group")
1520
+
1521
+ with DeploymentNode("Server 1"):
1522
+ # Frontend in Group A and Shared Group
1523
+ frontend_instance = ContainerInstance(frontend, [group_a, group_shared])
1524
+
1525
+ with DeploymentNode("Server 2"):
1526
+ # Backend in Group B and Shared Group
1527
+ backend_instance = ContainerInstance(backend, [group_b, group_shared])
1528
+
1529
+ # Verify deployment group assignments
1530
+ assert set(frontend_instance.model.deploymentGroups) == {"Group A", "Shared Group"}
1531
+ assert set(backend_instance.model.deploymentGroups) == {"Group B", "Shared Group"}
1532
+
1533
+ # Frontend and backend share "Shared Group", so relationship should exist
1534
+ frontend_relationships = [
1535
+ r for r in (frontend_instance.model.relationships or [])
1536
+ if r.description == "Calls"
1537
+ ]
1538
+ assert len(frontend_relationships) == 1, f"Expected 1 relationship, found {len(frontend_relationships)}"
1539
+ assert frontend_relationships[0].destinationId == backend_instance.model.id
1540
+
1541
+ def test_container_instance_relationships_with_no_deployment_groups() -> Optional[None]:
1542
+ """
1543
+ Test that container instances without deployment groups can still have
1544
+ relationships (backward compatibility).
1545
+
1546
+ When no deployment groups are specified, all instances should be able to
1547
+ relate to each other as before.
1548
+ """
1549
+
1550
+ with Workspace("w", scope=None) as w:
1551
+ with SoftwareSystem("Software System") as software_system:
1552
+ service_a = Container("Service A")
1553
+ service_b = Container("Service B")
1554
+ service_a >> "Communicates with" >> service_b
1555
+
1556
+ with DeploymentEnvironment("Production") as production:
1557
+ with DeploymentNode("Server 1"):
1558
+ # No deployment groups specified
1559
+ service_a_instance_1 = ContainerInstance(service_a)
1560
+ service_b_instance_1 = ContainerInstance(service_b)
1561
+
1562
+ with DeploymentNode("Server 2"):
1563
+ # No deployment groups specified
1564
+ service_a_instance_2 = ContainerInstance(service_a)
1565
+ service_b_instance_2 = ContainerInstance(service_b)
1566
+
1567
+ # When no deployment groups are specified, instances should be able to
1568
+ # relate to each other (all instances are in the "default" group)
1569
+ # This maintains backward compatibility
1570
+
1571
+ # Each service_a instance should relate to ALL service_b instances
1572
+ service_a_1_rels = [
1573
+ r for r in (service_a_instance_1.model.relationships or [])
1574
+ if r.description == "Communicates with"
1575
+ ]
1576
+ service_a_2_rels = [
1577
+ r for r in (service_a_instance_2.model.relationships or [])
1578
+ if r.description == "Communicates with"
1579
+ ]
1580
+
1581
+ # When no groups specified, all instances should connect to all other instances
1582
+ assert len(service_a_1_rels) == 2, f"Expected 2 relationships (to both service_b instances), found {len(service_a_1_rels)}"
1583
+ assert len(service_a_2_rels) == 2, f"Expected 2 relationships (to both service_b instances), found {len(service_a_2_rels)}"
1584
+
1585
+ # Verify destinations
1586
+ service_a_1_destinations = {r.destinationId for r in service_a_1_rels}
1587
+ service_a_2_destinations = {r.destinationId for r in service_a_2_rels}
1588
+
1589
+ assert service_b_instance_1.model.id in service_a_1_destinations
1590
+ assert service_b_instance_2.model.id in service_a_1_destinations
1591
+ assert service_b_instance_1.model.id in service_a_2_destinations
1592
+ assert service_b_instance_2.model.id in service_a_2_destinations
@@ -1 +0,0 @@
1
- VERSION = "0.0.18"
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