buildzr 0.0.16__tar.gz → 0.0.18__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.16 → buildzr-0.0.18}/CONTRIBUTING.md +69 -63
  2. {buildzr-0.0.16 → buildzr-0.0.18}/PKG-INFO +1 -1
  3. buildzr-0.0.18/buildzr/__about__.py +1 -0
  4. {buildzr-0.0.16 → buildzr-0.0.18}/buildzr/dsl/dsl.py +29 -8
  5. {buildzr-0.0.16 → buildzr-0.0.18}/buildzr/sinks/json_sink.py +3 -1
  6. {buildzr-0.0.16 → buildzr-0.0.18}/tests/test_dsl.py +122 -12
  7. {buildzr-0.0.16 → buildzr-0.0.18}/tests/test_explorer.py +2 -2
  8. buildzr-0.0.16/buildzr/__about__.py +0 -1
  9. {buildzr-0.0.16 → buildzr-0.0.18}/.gitignore +0 -0
  10. {buildzr-0.0.16 → buildzr-0.0.18}/LICENSE.md +0 -0
  11. {buildzr-0.0.16 → buildzr-0.0.18}/README.md +0 -0
  12. {buildzr-0.0.16 → buildzr-0.0.18}/buildzr/__init__.py +0 -0
  13. {buildzr-0.0.16 → buildzr-0.0.18}/buildzr/dsl/__init__.py +0 -0
  14. {buildzr-0.0.16 → buildzr-0.0.18}/buildzr/dsl/color.py +0 -0
  15. {buildzr-0.0.16 → buildzr-0.0.18}/buildzr/dsl/explorer.py +0 -0
  16. {buildzr-0.0.16 → buildzr-0.0.18}/buildzr/dsl/expression.py +0 -0
  17. {buildzr-0.0.16 → buildzr-0.0.18}/buildzr/dsl/factory/__init__.py +0 -0
  18. {buildzr-0.0.16 → buildzr-0.0.18}/buildzr/dsl/factory/gen_id.py +0 -0
  19. {buildzr-0.0.16 → buildzr-0.0.18}/buildzr/dsl/interfaces/__init__.py +0 -0
  20. {buildzr-0.0.16 → buildzr-0.0.18}/buildzr/dsl/interfaces/interfaces.py +0 -0
  21. {buildzr-0.0.16 → buildzr-0.0.18}/buildzr/dsl/relations.py +0 -0
  22. {buildzr-0.0.16 → buildzr-0.0.18}/buildzr/encoders/__init__.py +0 -0
  23. {buildzr-0.0.16 → buildzr-0.0.18}/buildzr/encoders/encoder.py +0 -0
  24. {buildzr-0.0.16 → buildzr-0.0.18}/buildzr/models/__init__.py +0 -0
  25. {buildzr-0.0.16 → buildzr-0.0.18}/buildzr/models/generate.sh +0 -0
  26. {buildzr-0.0.16 → buildzr-0.0.18}/buildzr/models/models.py +0 -0
  27. {buildzr-0.0.16 → buildzr-0.0.18}/buildzr/sinks/__init__.py +0 -0
  28. {buildzr-0.0.16 → buildzr-0.0.18}/buildzr/sinks/interfaces.py +0 -0
  29. {buildzr-0.0.16 → buildzr-0.0.18}/pyproject.toml +0 -0
  30. {buildzr-0.0.16 → buildzr-0.0.18}/tests/__init__.py +0 -0
  31. {buildzr-0.0.16 → buildzr-0.0.18}/tests/abstract_builder.py +0 -0
  32. {buildzr-0.0.16 → buildzr-0.0.18}/tests/samples/__init__.py +0 -0
  33. {buildzr-0.0.16 → buildzr-0.0.18}/tests/samples/component_view.py +0 -0
  34. {buildzr-0.0.16 → buildzr-0.0.18}/tests/samples/container_view.py +0 -0
  35. {buildzr-0.0.16 → buildzr-0.0.18}/tests/samples/container_view_sugar.py +0 -0
  36. {buildzr-0.0.16 → buildzr-0.0.18}/tests/samples/groups.py +0 -0
  37. {buildzr-0.0.16 → buildzr-0.0.18}/tests/samples/implied_relationships.py +0 -0
  38. {buildzr-0.0.16 → buildzr-0.0.18}/tests/samples/nested_groups.py +0 -0
  39. {buildzr-0.0.16 → buildzr-0.0.18}/tests/samples/simple.py +0 -0
  40. {buildzr-0.0.16 → buildzr-0.0.18}/tests/samples/simple_dsl.py +0 -0
  41. {buildzr-0.0.16 → buildzr-0.0.18}/tests/samples/system_context_view.py +0 -0
  42. {buildzr-0.0.16 → buildzr-0.0.18}/tests/samples/system_landscape_view.py +0 -0
  43. {buildzr-0.0.16 → buildzr-0.0.18}/tests/test_expression.py +0 -0
  44. {buildzr-0.0.16 → buildzr-0.0.18}/tests/test_typehints.py +0 -0
  45. {buildzr-0.0.16 → buildzr-0.0.18}/tests/test_views.py +0 -0
  46. {buildzr-0.0.16 → buildzr-0.0.18}/tests/test_workspaces.py +0 -0
@@ -58,74 +58,80 @@ pytest --mypy tests
58
58
 
59
59
  Sometimes it is valuable to manually check the output of the Structurizr workspace generated by `buildzr` visually. For this reason, you can create `buildzr` samples in the [`tests/samples`](https://github.com/amirulmenjeni/buildzr/tree/master/tests/samples) directory.
60
60
 
61
- Here's an example of the [`GroupSample`](https://github.com/amirulmenjeni/buildzr/blob/master/tests/samples/groups.py), to showcase the [group](https://docs.structurizr.com/dsl/cookbook/groups/) feature in Structurizr DSL used for grouping elements together:
61
+ Here's an example of a simple workspace to showcase the [group](https://docs.structurizr.com/dsl/cookbook/groups/) feature in Structurizr DSL used for grouping elements together:
62
62
 
63
63
  ```python
64
- import buildzr
65
- from buildzr.dsl import *
66
- from ..abstract_builder import AbstractBuilder
67
-
68
- class GroupsSample(AbstractBuilder):
69
-
70
- def build(self) -> buildzr.models.Workspace:
71
-
72
- w = Workspace("w", scope=None)\
73
- .contains(
74
- Group(
75
- "Company 1",
76
- SoftwareSystem("A")\
77
- .contains(
78
- Container("a1"),
79
- Container("a2"),
80
- )
81
- ),
82
- Group(
83
- "Company 2",
84
- SoftwareSystem("B")\
85
- .contains(
86
- Container("b1"),
87
- Container("b2")
88
- .contains(
89
- Component("c1"),
90
- )
91
- )
92
- ),
93
- SoftwareSystem("C"),
94
- )\
95
- .where(lambda w: [
96
- w.software_system().a >> "Uses" >> w.software_system().b,
97
- w.software_system().a.container().a1 >> "Uses" >> w.software_system().b.container().b1,
98
- w.software_system().a >> "Uses" >> w.software_system().c,
99
- ])\
100
- .with_views(
101
- SystemLandscapeView(
102
- key='groups-sample',
103
- description="Groups Sample"
104
- ),
105
- SystemContextView(
106
- key='groups-sample-a',
107
- software_system_selector=lambda w: cast(SoftwareSystem, w.a),
108
- description="Groups Sample - Software System A"
109
- ),
110
- SystemContextView(
111
- key='groups-sample-b',
112
- software_system_selector=lambda w: cast(SoftwareSystem, w.b),
113
- description="Groups Sample - Software System B"
114
- ),
115
- ContainerView(
116
- key='groups-sample-b2',
117
- software_system_selector=lambda w: w.software_system().b,
118
- description="Groups Sample - Container B2"
119
- ),
120
- )\
121
- .get_workspace()
122
-
123
- return w.model
64
+ from buildzr.dsl import (
65
+ Workspace,
66
+ SoftwareSystem,
67
+ Container,
68
+ Component,
69
+ Group,
70
+ SystemLandscapeView,
71
+ SystemContextView,
72
+ ContainerView,
73
+ )
74
+
75
+ with Workspace("w") as w:
76
+ with Group("Company 1"):
77
+ system_a = SoftwareSystem("A")
78
+ with system_a:
79
+ a1 = Container("a1")
80
+ a2 = Container("a2")
81
+
82
+ with Group("Company 2"):
83
+ system_b = SoftwareSystem("B")
84
+ with system_b:
85
+ b1 = Container("b1")
86
+ b2 = Container("b2")
87
+ with b2:
88
+ c1 = Component("c1")
89
+
90
+ system_c = SoftwareSystem("C")
91
+
92
+ # Define relationships
93
+ system_a >> "Uses" >> system_b
94
+ a1 >> "Uses" >> b1
95
+ system_a >> "Uses" >> system_c
96
+
97
+ # Create views
98
+ SystemLandscapeView(
99
+ key='groups-sample',
100
+ description="Groups Sample"
101
+ )
102
+
103
+ SystemContextView(
104
+ software_system_selector=system_a,
105
+ key='groups-sample-a',
106
+ description="Groups Sample - Software System A"
107
+ )
108
+
109
+ SystemContextView(
110
+ software_system_selector=system_b,
111
+ key='groups-sample-b',
112
+ description="Groups Sample - Software System B"
113
+ )
114
+
115
+ ContainerView(
116
+ software_system_selector=system_b,
117
+ key='groups-sample-b2',
118
+ description="Groups Sample - Container B2"
119
+ )
120
+
121
+ # Export to JSON
122
+ w.to_json('groups-sample.json')
124
123
  ```
125
124
 
126
- By running `pytest tests/test_workspaces.py`, all the sample workspaces would be built, creating the corresponding JSON file with Structurizr schema. In the case of the `GroupSample` class, for example, the resulting output file path would be `tests/samples/.tests.samples.groups.json` -- named after the name of the Python file that contains the `GroupSample` class.
125
+ The example above demonstrates the current `buildzr` API using context managers (`with` statements) for a cleaner, more Pythonic syntax. Key features shown:
127
126
 
128
- Note that the sample class must inherit the `AbstractBuilder` class.
127
+ - Use `with Workspace("name") as w:` to create a workspace context
128
+ - Use `with Group("name"):` to group elements together
129
+ - Use `with software_system:` or `with container:` to nest containers and components
130
+ - Define relationships using the `>>` operator
131
+ - Views and styles are created within the workspace context
132
+ - Export using `w.to_json('filename.json')`
133
+
134
+ You can run this code directly or create sample files in the `tests/samples` directory for testing purposes.
129
135
 
130
136
  ### Working with the `.json` files
131
137
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: buildzr
3
- Version: 0.0.16
3
+ Version: 0.0.18
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.18"
@@ -133,12 +133,24 @@ class Workspace(DslWorkspaceElement):
133
133
  self,
134
134
  ) -> None:
135
135
 
136
+ """
137
+ Process implied relationships:
138
+ If we have relationship s >> do >> a.b, then create s >> do >> a.
139
+ If we have relationship s.ss >> do >> a.b.c, then create s.ss >> do >> a.b and s.ss >> do >> a.
140
+ And so on...
141
+
142
+ Relationships of `SoftwareSystemInstance`s and `ContainerInstance`s are
143
+ skipped.
144
+
145
+ This process is idempotent, which means this can be called multiple times
146
+ without duplicating similar relationships.
147
+ """
148
+
149
+ if not self._use_implied_relationships:
150
+ return
151
+
136
152
  from buildzr.dsl.explorer import Explorer
137
153
 
138
- # Process implied relationships:
139
- # If we have relationship s >> do >> a.b, then create s >> do >> a.
140
- # If we have relationship s.ss >> do >> a.b.c, then create s.ss >> do >> a.b and s.ss >> do >> a.
141
- # And so on...
142
154
  explorer = Explorer(self)
143
155
  relationships = list(explorer.walk_relationships())
144
156
  for relationship in relationships:
@@ -146,6 +158,10 @@ class Workspace(DslWorkspaceElement):
146
158
  destination = relationship.destination
147
159
  destination_parent = destination.parent
148
160
 
161
+ if isinstance(source, (SoftwareSystemInstance, ContainerInstance)) or \
162
+ isinstance(destination, (SoftwareSystemInstance, ContainerInstance)):
163
+ continue
164
+
149
165
  while destination_parent is not None and \
150
166
  isinstance(source, DslElement) and \
151
167
  not isinstance(source.model, buildzr.models.Workspace) and \
@@ -201,8 +217,7 @@ class Workspace(DslWorkspaceElement):
201
217
  else:
202
218
  raise ValueError('Invalid element type: Trying to add an element of type {} to a workspace.'.format(type(model)))
203
219
 
204
- def apply_view(
205
- self,
220
+ def apply_view( self,
206
221
  view: Union[
207
222
  'SystemLandscapeView',
208
223
  'SystemContextView',
@@ -212,6 +227,8 @@ class Workspace(DslWorkspaceElement):
212
227
  ]
213
228
  ) -> None:
214
229
 
230
+ self._imply_relationships()
231
+
215
232
  view._on_added(self)
216
233
 
217
234
  if not self.model.views:
@@ -269,10 +286,14 @@ class Workspace(DslWorkspaceElement):
269
286
  else:
270
287
  self.model.views.configuration.styles.relationships = style.model
271
288
 
272
- def to_json(self, path: str) -> None:
289
+ def to_json(self, path: str, pretty: bool=False) -> None:
290
+
291
+ self._imply_relationships()
292
+
273
293
  from buildzr.sinks.json_sink import JsonSink, JsonSinkConfig
274
294
  sink = JsonSink()
275
- sink.write(workspace=self.model, config=JsonSinkConfig(path=path))
295
+ sink.write(workspace=self.model, config=JsonSinkConfig(path=path, pretty=pretty))
296
+
276
297
 
277
298
  def _add_dynamic_attr(self, name: str, model: Union['Person', 'SoftwareSystem']) -> None:
278
299
  if isinstance(model, Person):
@@ -9,13 +9,15 @@ from buildzr.sinks.interfaces import Sink
9
9
  @dataclass
10
10
  class JsonSinkConfig:
11
11
  path: str
12
+ pretty: bool = False
12
13
 
13
14
  class JsonSink(Sink[JsonSinkConfig]):
14
15
 
15
16
  def write(self, workspace: Workspace, config: Optional[JsonSinkConfig]=None) -> None:
16
17
  if config is not None:
18
+ indent = 2 if config.pretty else None
17
19
  with open(config.path, 'w') as file:
18
- file.write(JsonEncoder().encode(workspace))
20
+ file.write(JsonEncoder(indent=indent).encode(workspace))
19
21
  else:
20
22
  import os
21
23
  workspace_name = workspace.name.replace(' ', '_').lower()
@@ -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,
@@ -336,7 +337,10 @@ def test_implied_relationship() -> Optional[None]:
336
337
  # relationship. For example, u -> s.database doesn't explicitly create a u
337
338
  # -> s relationship in the workspace JSON.
338
339
 
339
- # TODO: Add a way to enable/disable implied relationships in the DSL.
340
+ # Conditions to take into account:
341
+ # 1. The implied relationships method be must idempotent (e.g., if it is
342
+ # called in `to_json` twice, or in other methods like `apply_view`, it
343
+ # doesn't create duplicates)
340
344
 
341
345
  with Workspace("w", implied_relationships=True) as w:
342
346
  u = Person('u')
@@ -354,18 +358,46 @@ def test_implied_relationship() -> Optional[None]:
354
358
 
355
359
  u >> "Runs SQL queries" >> s.db # `u >> "Runs SQL queries" >> s`` should be implied
356
360
 
357
- assert isinstance(w.u, Person)
358
- assert isinstance(w.s, SoftwareSystem)
359
- assert len(w.u.model.relationships) == 2 # Should have u >> R >> s and u >> R >> s.database
361
+ # Invoke imply relationships whenever a view is called.
362
+ #
363
+ # The implied relationship ids and related elements
364
+ # should appear in the view.
365
+ SystemContextView(
366
+ software_system_selector=s,
367
+ key='s_00',
368
+ description="App system context",
369
+ )
370
+
371
+ # Invoke imply relationships more than once.
372
+ # Should be no problem.
373
+ w.to_json('workspace.test.json')
374
+ w.to_json('workspace2.test.json')
375
+
376
+ assert isinstance(w.u, Person)
377
+ assert isinstance(w.s, SoftwareSystem)
378
+ assert len(w.u.model.relationships) == 2 # Should have u >> R >> s and u >> R >> s.database
379
+
380
+ assert w.u.model.relationships[0].description == "Runs SQL queries"
381
+ assert w.u.model.relationships[0].sourceId == w.u.model.id
382
+ assert w.u.model.relationships[0].destinationId == w.s.db.model.id
383
+
384
+ assert w.u.model.relationships[1].description == "Runs SQL queries"
385
+ assert w.u.model.relationships[1].sourceId == w.u.model.relationships[0].sourceId
386
+ assert w.u.model.relationships[1].destinationId == w.s.model.id
387
+ assert w.u.model.relationships[1].linkedRelationshipId == w.u.model.relationships[0].id
360
388
 
361
- assert w.u.model.relationships[0].description == "Runs SQL queries"
362
- assert w.u.model.relationships[0].sourceId == w.u.model.id
363
- assert w.u.model.relationships[0].destinationId == w.s.db.model.id
389
+ system_context_view_elements = [x.id for x in w._m.views.systemContextViews[0].elements]
390
+ assert u.model.id in system_context_view_elements
391
+ assert s.model.id in system_context_view_elements
364
392
 
365
- assert w.u.model.relationships[1].description == "Runs SQL queries"
366
- assert w.u.model.relationships[1].sourceId == w.u.model.relationships[0].sourceId
367
- assert w.u.model.relationships[1].destinationId == w.s.model.id
368
- assert w.u.model.relationships[1].linkedRelationshipId == w.u.model.relationships[0].id
393
+ system_context_view_relationships = [x.id for x in w._m.views.systemContextViews[0].relationships]
394
+ assert w.u.model.relationships[0].id not in system_context_view_relationships
395
+ assert w.u.model.relationships[1].id in system_context_view_relationships
396
+ assert w.u.model.relationships[1].linkedRelationshipId == w.u.model.relationships[0].id
397
+
398
+ import os
399
+ os.remove('workspace.test.json')
400
+ os.remove('workspace2.test.json')
369
401
 
370
402
  def test_tags_on_elements() -> Optional[None]:
371
403
 
@@ -1072,4 +1104,82 @@ def test_json_sink_empty_views() -> Optional[None]:
1072
1104
  assert data
1073
1105
 
1074
1106
  import os
1075
- os.remove("test.json")
1107
+ os.remove("test.json")
1108
+ def test_deployment_instance_relationships_with_implied_relationships() -> Optional[None]:
1109
+ """
1110
+ Test that deployment instance relationships are created correctly when
1111
+ implied_relationships=True, without creating duplicates.
1112
+
1113
+ This test ensures:
1114
+ 1. Container relationships automatically create ContainerInstance relationships
1115
+ 2. No duplicate instance relationships are created when implied_relationships=True
1116
+ 3. Instance relationships are only created once, even with multiple view/export calls
1117
+ """
1118
+
1119
+ with Workspace('deployment-test', implied_relationships=True) as w:
1120
+ # Create containers with relationships
1121
+ ecommerce = SoftwareSystem('E-Commerce System')
1122
+ with ecommerce:
1123
+ api_gateway = Container('API Gateway', technology='Kong')
1124
+ order_svc = Container('Order Service', technology='Node.js')
1125
+ db = Container('Database', technology='MongoDB')
1126
+
1127
+ # Define container relationships
1128
+ api_gateway >> "Routes to" >> order_svc
1129
+ order_svc >> "Stores in" >> db
1130
+
1131
+ # Create deployment with container instances
1132
+ with DeploymentEnvironment('Production') as prod:
1133
+ with DeploymentNode('AWS', technology='Cloud Provider'):
1134
+ api_gw_instance = ContainerInstance(api_gateway)
1135
+ order_instance = ContainerInstance(order_svc)
1136
+ db_instance = ContainerInstance(db)
1137
+
1138
+ # Create views and export (triggers implied relationships multiple times)
1139
+ SystemContextView(
1140
+ software_system_selector=ecommerce,
1141
+ key='test-system-context',
1142
+ description="Test System Context",
1143
+ )
1144
+
1145
+ DeploymentView(
1146
+ environment=prod,
1147
+ key='test-deployment',
1148
+ )
1149
+
1150
+ # Export multiple times to ensure idempotency
1151
+ w.to_json('test_deployment1.json')
1152
+ w.to_json('test_deployment2.json')
1153
+
1154
+ # Verify instance relationships exist
1155
+ assert api_gw_instance.model.relationships is not None
1156
+ assert order_instance.model.relationships is not None
1157
+
1158
+ # Get all instance relationships
1159
+ api_gw_rels = api_gw_instance.model.relationships
1160
+ order_rels = order_instance.model.relationships
1161
+
1162
+ # Should have exactly 1 relationship from api_gw_instance to order_instance
1163
+ api_to_order_rels = [
1164
+ r for r in api_gw_rels
1165
+ if r.destinationId == order_instance.model.id
1166
+ ]
1167
+ assert len(api_to_order_rels) == 1, f"Expected 1 relationship, found {len(api_to_order_rels)}"
1168
+ assert api_to_order_rels[0].description == "Routes to"
1169
+
1170
+ # Should have exactly 1 relationship from order_instance to db_instance
1171
+ order_to_db_rels = [
1172
+ r for r in order_rels
1173
+ if r.destinationId == db_instance.model.id
1174
+ ]
1175
+ assert len(order_to_db_rels) == 1, f"Expected 1 relationship, found {len(order_to_db_rels)}"
1176
+ assert order_to_db_rels[0].description == "Stores in"
1177
+
1178
+ # Verify linkedRelationshipId is set correctly
1179
+ assert api_to_order_rels[0].linkedRelationshipId is not None
1180
+ assert order_to_db_rels[0].linkedRelationshipId is not None
1181
+
1182
+ # Clean up
1183
+ import os
1184
+ os.remove('test_deployment1.json')
1185
+ os.remove('test_deployment2.json')
@@ -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.16"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes