buildzr 0.0.18__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.18 → buildzr-0.0.19}/PKG-INFO +1 -1
  2. buildzr-0.0.19/buildzr/__about__.py +1 -0
  3. {buildzr-0.0.18 → buildzr-0.0.19}/buildzr/dsl/dsl.py +53 -3
  4. {buildzr-0.0.18 → buildzr-0.0.19}/tests/test_dsl.py +191 -0
  5. buildzr-0.0.18/buildzr/__about__.py +0 -1
  6. {buildzr-0.0.18 → buildzr-0.0.19}/.gitignore +0 -0
  7. {buildzr-0.0.18 → buildzr-0.0.19}/CONTRIBUTING.md +0 -0
  8. {buildzr-0.0.18 → buildzr-0.0.19}/LICENSE.md +0 -0
  9. {buildzr-0.0.18 → buildzr-0.0.19}/README.md +0 -0
  10. {buildzr-0.0.18 → buildzr-0.0.19}/buildzr/__init__.py +0 -0
  11. {buildzr-0.0.18 → buildzr-0.0.19}/buildzr/dsl/__init__.py +0 -0
  12. {buildzr-0.0.18 → buildzr-0.0.19}/buildzr/dsl/color.py +0 -0
  13. {buildzr-0.0.18 → buildzr-0.0.19}/buildzr/dsl/explorer.py +0 -0
  14. {buildzr-0.0.18 → buildzr-0.0.19}/buildzr/dsl/expression.py +0 -0
  15. {buildzr-0.0.18 → buildzr-0.0.19}/buildzr/dsl/factory/__init__.py +0 -0
  16. {buildzr-0.0.18 → buildzr-0.0.19}/buildzr/dsl/factory/gen_id.py +0 -0
  17. {buildzr-0.0.18 → buildzr-0.0.19}/buildzr/dsl/interfaces/__init__.py +0 -0
  18. {buildzr-0.0.18 → buildzr-0.0.19}/buildzr/dsl/interfaces/interfaces.py +0 -0
  19. {buildzr-0.0.18 → buildzr-0.0.19}/buildzr/dsl/relations.py +0 -0
  20. {buildzr-0.0.18 → buildzr-0.0.19}/buildzr/encoders/__init__.py +0 -0
  21. {buildzr-0.0.18 → buildzr-0.0.19}/buildzr/encoders/encoder.py +0 -0
  22. {buildzr-0.0.18 → buildzr-0.0.19}/buildzr/models/__init__.py +0 -0
  23. {buildzr-0.0.18 → buildzr-0.0.19}/buildzr/models/generate.sh +0 -0
  24. {buildzr-0.0.18 → buildzr-0.0.19}/buildzr/models/models.py +0 -0
  25. {buildzr-0.0.18 → buildzr-0.0.19}/buildzr/sinks/__init__.py +0 -0
  26. {buildzr-0.0.18 → buildzr-0.0.19}/buildzr/sinks/interfaces.py +0 -0
  27. {buildzr-0.0.18 → buildzr-0.0.19}/buildzr/sinks/json_sink.py +0 -0
  28. {buildzr-0.0.18 → buildzr-0.0.19}/pyproject.toml +0 -0
  29. {buildzr-0.0.18 → buildzr-0.0.19}/tests/__init__.py +0 -0
  30. {buildzr-0.0.18 → buildzr-0.0.19}/tests/abstract_builder.py +0 -0
  31. {buildzr-0.0.18 → buildzr-0.0.19}/tests/samples/__init__.py +0 -0
  32. {buildzr-0.0.18 → buildzr-0.0.19}/tests/samples/component_view.py +0 -0
  33. {buildzr-0.0.18 → buildzr-0.0.19}/tests/samples/container_view.py +0 -0
  34. {buildzr-0.0.18 → buildzr-0.0.19}/tests/samples/container_view_sugar.py +0 -0
  35. {buildzr-0.0.18 → buildzr-0.0.19}/tests/samples/groups.py +0 -0
  36. {buildzr-0.0.18 → buildzr-0.0.19}/tests/samples/implied_relationships.py +0 -0
  37. {buildzr-0.0.18 → buildzr-0.0.19}/tests/samples/nested_groups.py +0 -0
  38. {buildzr-0.0.18 → buildzr-0.0.19}/tests/samples/simple.py +0 -0
  39. {buildzr-0.0.18 → buildzr-0.0.19}/tests/samples/simple_dsl.py +0 -0
  40. {buildzr-0.0.18 → buildzr-0.0.19}/tests/samples/system_context_view.py +0 -0
  41. {buildzr-0.0.18 → buildzr-0.0.19}/tests/samples/system_landscape_view.py +0 -0
  42. {buildzr-0.0.18 → buildzr-0.0.19}/tests/test_explorer.py +0 -0
  43. {buildzr-0.0.18 → buildzr-0.0.19}/tests/test_expression.py +0 -0
  44. {buildzr-0.0.18 → buildzr-0.0.19}/tests/test_typehints.py +0 -0
  45. {buildzr-0.0.18 → buildzr-0.0.19}/tests/test_views.py +0 -0
  46. {buildzr-0.0.18 → 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.18
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,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
 
@@ -839,6 +877,12 @@ class DeploymentEnvironment(DslDeploymentEnvironment):
839
877
  if not relationship.destinationId in other_softwares_ids:
840
878
  continue
841
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
+
842
886
  this_software_instances = software_instance_map[software.model.id]
843
887
  other_software_instances = software_instance_map[relationship.destinationId]
844
888
 
@@ -907,6 +951,12 @@ class DeploymentEnvironment(DslDeploymentEnvironment):
907
951
  if not relationship.destinationId in other_containers_ids:
908
952
  continue
909
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
+
910
960
  this_container_instances = container_instance_map[container.model.id]
911
961
  other_container_instances = container_instance_map[relationship.destinationId]
912
962
 
@@ -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,150 @@ 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
@@ -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