buildzr 0.0.8__py3-none-any.whl → 0.0.10__py3-none-any.whl

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.
buildzr/__about__.py CHANGED
@@ -1 +1 @@
1
- VERSION = "0.0.8"
1
+ VERSION = "0.0.10"
buildzr/dsl/__init__.py CHANGED
@@ -9,10 +9,13 @@ from .dsl import (
9
9
  SystemContextView,
10
10
  ContainerView,
11
11
  ComponentView,
12
+ StyleElements,
13
+ StyleRelationships,
12
14
  )
13
15
  from .relations import (
14
16
  desc,
15
17
  With,
16
18
  )
17
19
  from .explorer import Explorer
18
- from .expression import Expression
20
+ from .expression import Expression
21
+ from .color import Color
buildzr/dsl/color.py ADDED
@@ -0,0 +1,121 @@
1
+ import re
2
+ from typing import Tuple, Optional, Union, Literal
3
+
4
+ class Color:
5
+ r: int
6
+ g: int
7
+ b: int
8
+
9
+ _ENGLISH_COLORS = {
10
+ 'black': '#000000',
11
+ 'white': '#ffffff',
12
+ 'red': '#ff0000',
13
+ 'green': '#00ff00',
14
+ 'blue': '#0000ff',
15
+ 'yellow': '#ffff00',
16
+ 'cyan': '#00ffff',
17
+ 'magenta': '#ff00ff',
18
+ 'gray': '#808080',
19
+ 'grey': '#808080',
20
+ 'orange': '#ffa500',
21
+ 'purple': '#800080',
22
+ 'pink': '#ffc0cb',
23
+ 'brown': '#a52a2a',
24
+ 'lime': '#00ff00',
25
+ 'navy': '#000080',
26
+ 'teal': '#008080',
27
+ 'olive': '#808000',
28
+ 'maroon': '#800000',
29
+ 'silver': '#c0c0c0',
30
+ 'gold': '#ffd700',
31
+ }
32
+
33
+ def __init__(
34
+ self,
35
+ value: Union[
36
+ 'Color',
37
+ str,
38
+ Tuple[int, int, int],
39
+ Literal[
40
+ 'black', 'white', 'red', 'green', 'blue', 'yellow', 'cyan', 'magenta', 'gray', 'grey', 'orange', 'purple', 'pink', 'brown', 'lime', 'navy', 'teal', 'olive', 'maroon', 'silver', 'gold'
41
+ ]
42
+ ]
43
+ ):
44
+ if isinstance(value, Color):
45
+ self.r, self.g, self.b = value.r, value.g, value.b
46
+ elif isinstance(value, tuple):
47
+ if len(value) == 3:
48
+ self.r, self.g, self.b = value
49
+ else:
50
+ raise ValueError("Tuple must be (r, g, b)")
51
+ elif isinstance(value, str):
52
+ self.r, self.g, self.b = self._parse_color(value)
53
+ else:
54
+ raise TypeError("Invalid type for Color constructor")
55
+
56
+ @classmethod
57
+ def is_valid_color(cls, value: Union[str, Tuple[int, int, int], 'Color']) -> bool:
58
+ try:
59
+ if isinstance(value, tuple):
60
+ if len(value) == 3 and all(isinstance(x, int) and 0 <= x <= 255 for x in value):
61
+ return True
62
+ return False
63
+ elif isinstance(value, str):
64
+ v = value.strip().lower()
65
+ if v in cls._ENGLISH_COLORS:
66
+ return True
67
+ if v.startswith('#'):
68
+ try:
69
+ cls._parse_hex(v)
70
+ return True
71
+ except Exception:
72
+ return False
73
+ if v.startswith('rgb'):
74
+ try:
75
+ cls._parse_rgb(v)
76
+ return True
77
+ except Exception:
78
+ return False
79
+ return False
80
+ return False
81
+ except Exception:
82
+ return False
83
+
84
+ @classmethod
85
+ def _parse_color(cls, value: str) -> Tuple[int, int, int]:
86
+ value = value.strip().lower()
87
+ if value in cls._ENGLISH_COLORS:
88
+ value = cls._ENGLISH_COLORS[value]
89
+ if value.startswith('#'):
90
+ return cls._parse_hex(value)
91
+ if value.startswith('rgb'):
92
+ return cls._parse_rgb(value)
93
+ raise ValueError(f"Unknown color format: {value}")
94
+
95
+ @classmethod
96
+ def _parse_hex(cls, value: str) -> Tuple[int, int, int]:
97
+ value = value.lstrip('#')
98
+ if len(value) == 3:
99
+ value = ''.join([c*2 for c in value])
100
+ if len(value) == 6:
101
+ r, g, b = value[0:2], value[2:4], value[4:6]
102
+ return int(r, 16), int(g, 16), int(b, 16)
103
+ raise ValueError(f"Invalid hex color: #{value}")
104
+
105
+ @classmethod
106
+ def _parse_rgb(cls, value: str) -> Tuple[int, int, int]:
107
+ # Match rgb(r, g, b)
108
+ match = re.match(r"rgb\(([^)]+)\)", value)
109
+ if not match:
110
+ raise ValueError(f"Invalid rgb color: {value}")
111
+ parts = [x.strip() for x in match.group(1).split(',')]
112
+ if len(parts) == 3:
113
+ r, g, b = map(int, parts)
114
+ return r, g, b
115
+ raise ValueError(f"Invalid rgb color: {value}")
116
+
117
+ def to_hex(self) -> str:
118
+ return f"#{self.r:02x}{self.g:02x}{self.b:02x}"
119
+
120
+ def __str__(self) -> str:
121
+ return f"rgb({self.r}, {self.g}, {self.b})"
buildzr/dsl/dsl.py CHANGED
@@ -33,7 +33,10 @@ from buildzr.dsl.interfaces import (
33
33
  )
34
34
  from buildzr.dsl.relations import (
35
35
  DslElementRelationOverrides,
36
+ DslRelationship,
37
+ _Relationship,
36
38
  )
39
+ from buildzr.dsl.color import Color
37
40
 
38
41
  def _child_name_transform(name: str) -> str:
39
42
  return name.lower().replace(' ', '_')
@@ -186,6 +189,30 @@ class Workspace(DslWorkspaceElement):
186
189
  ) -> None:
187
190
  Views(self).add_views(*views)
188
191
 
192
+ def apply_style( self,
193
+ style: Union['StyleElements', 'StyleRelationships'],
194
+ ) -> None:
195
+
196
+ style._parent = self
197
+
198
+ if not self.model.views:
199
+ self.model.views = buildzr.models.Views()
200
+ if not self.model.views.configuration:
201
+ self.model.views.configuration = buildzr.models.Configuration()
202
+ if not self.model.views.configuration.styles:
203
+ self.model.views.configuration.styles = buildzr.models.Styles()
204
+
205
+ if isinstance(style, StyleElements):
206
+ if self.model.views.configuration.styles.elements:
207
+ self.model.views.configuration.styles.elements.extend(style.model)
208
+ else:
209
+ self.model.views.configuration.styles.elements = style.model
210
+ elif isinstance(style, StyleRelationships):
211
+ if self.model.views.configuration.styles.relationships:
212
+ self.model.views.configuration.styles.relationships.extend(style.model)
213
+ else:
214
+ self.model.views.configuration.styles.relationships = style.model
215
+
189
216
  def to_json(self, path: str) -> None:
190
217
  from buildzr.sinks.json_sink import JsonSink, JsonSinkConfig
191
218
  sink = JsonSink()
@@ -245,6 +272,10 @@ class SoftwareSystem(DslElementRelationOverrides[
245
272
  def destinations(self) -> List[DslElement]:
246
273
  return self._destinations
247
274
 
275
+ @property
276
+ def relationships(self) -> Set[_Relationship]:
277
+ return self._relationships
278
+
248
279
  @property
249
280
  def tags(self) -> Set[str]:
250
281
  return self._tags
@@ -256,6 +287,7 @@ class SoftwareSystem(DslElementRelationOverrides[
256
287
  self._children: Optional[List['Container']] = []
257
288
  self._sources: List[DslElement] = []
258
289
  self._destinations: List[DslElement] = []
290
+ self._relationships: Set[_Relationship] = set()
259
291
  self._tags = {'Element', 'Software System'}.union(tags)
260
292
  self._dynamic_attrs: Dict[str, 'Container'] = {}
261
293
  self._label: Optional[str] = None
@@ -354,6 +386,10 @@ class Person(DslElementRelationOverrides[
354
386
  def destinations(self) -> List[DslElement]:
355
387
  return self._destinations
356
388
 
389
+ @property
390
+ def relationships(self) -> Set[_Relationship]:
391
+ return self._relationships
392
+
357
393
  @property
358
394
  def tags(self) -> Set[str]:
359
395
  return self._tags
@@ -363,6 +399,7 @@ class Person(DslElementRelationOverrides[
363
399
  self._parent: Optional[Workspace] = None
364
400
  self._sources: List[DslElement] = []
365
401
  self._destinations: List[DslElement] = []
402
+ self._relationships: Set[_Relationship] = set()
366
403
  self._tags = {'Element', 'Person'}.union(tags)
367
404
  self._label: Optional[str] = None
368
405
  self.model.id = GenerateId.for_element()
@@ -420,6 +457,10 @@ class Container(DslElementRelationOverrides[
420
457
  def destinations(self) -> List[DslElement]:
421
458
  return self._destinations
422
459
 
460
+ @property
461
+ def relationships(self) -> Set[_Relationship]:
462
+ return self._relationships
463
+
423
464
  @property
424
465
  def tags(self) -> Set[str]:
425
466
  return self._tags
@@ -431,6 +472,7 @@ class Container(DslElementRelationOverrides[
431
472
  self._children: Optional[List['Component']] = []
432
473
  self._sources: List[DslElement] = []
433
474
  self._destinations: List[DslElement] = []
475
+ self._relationships: Set[_Relationship] = set()
434
476
  self._tags = {'Element', 'Container'}.union(tags)
435
477
  self._dynamic_attrs: Dict[str, 'Component'] = {}
436
478
  self._label: Optional[str] = None
@@ -527,6 +569,10 @@ class Component(DslElementRelationOverrides[
527
569
  def destinations(self) -> List[DslElement]:
528
570
  return self._destinations
529
571
 
572
+ @property
573
+ def relationships(self) -> Set[_Relationship]:
574
+ return self._relationships
575
+
530
576
  @property
531
577
  def tags(self) -> Set[str]:
532
578
  return self._tags
@@ -536,6 +582,7 @@ class Component(DslElementRelationOverrides[
536
582
  self._parent: Optional[Container] = None
537
583
  self._sources: List[DslElement] = []
538
584
  self._destinations: List[DslElement] = []
585
+ self._relationships: Set[_Relationship] = set()
539
586
  self._tags = {'Element', 'Component'}.union(tags)
540
587
  self._label: Optional[str] = None
541
588
  self.model.id = GenerateId.for_element()
@@ -567,9 +614,32 @@ class Group:
567
614
  def __init__(
568
615
  self,
569
616
  name: str,
617
+ workspace: Optional[Workspace]=None,
570
618
  ) -> None:
619
+
620
+ if not workspace:
621
+ workspace = _current_workspace.get()
622
+ if workspace is not None:
623
+ self._group_separator = workspace._group_separator
624
+
625
+ self._group_separator = workspace._group_separator
571
626
  self._name = name
572
627
 
628
+ if len(self._group_separator) > 1:
629
+ raise ValueError('Group separator must be a single character.')
630
+
631
+ if self._group_separator in self._name:
632
+ raise ValueError('Group name cannot contain the group separator.')
633
+
634
+ stack = _current_group_stack.get()
635
+ new_stack = stack.copy()
636
+ new_stack.extend([self])
637
+
638
+ self._full_name = self._group_separator.join([group._name for group in new_stack])
639
+
640
+ def full_name(self) -> str:
641
+ return self._full_name
642
+
573
643
  def add_element(
574
644
  self,
575
645
  model: Union[
@@ -577,27 +647,11 @@ class Group:
577
647
  'SoftwareSystem',
578
648
  'Container',
579
649
  'Component',
580
- ],
581
- group_separator: str="/",
650
+ ]
582
651
  ) -> None:
583
652
 
584
- separator = group_separator
585
-
586
- workspace = _current_workspace.get()
587
- if workspace is not None:
588
- separator = workspace._group_separator
589
-
590
- if len(separator) > 1:
591
- raise ValueError('Group separator must be a single character.')
592
-
593
- if separator in self._name:
594
- raise ValueError('Group name cannot contain the group separator.')
595
-
596
- stack = _current_group_stack.get()
597
653
 
598
- index = next((i for i, group in enumerate(stack) if group._name == self._name), -1)
599
- if index >= 0:
600
- model.model.group = separator.join([group._name for group in stack[:index + 1]])
654
+ model.model.group = self._full_name
601
655
 
602
656
  def __enter__(self) -> Self:
603
657
  stack = _current_group_stack.get() # stack: a/b
@@ -1073,6 +1127,9 @@ class ComponentView(DslViewElement):
1073
1127
 
1074
1128
  class Views(DslViewsElement):
1075
1129
 
1130
+ # TODO: Make this view a "hidden" class -- it's not a "first class citizen"
1131
+ # in buildzr DSL.
1132
+
1076
1133
  @property
1077
1134
  def model(self) -> buildzr.models.Views:
1078
1135
  return self._m
@@ -1130,4 +1187,278 @@ class Views(DslViewsElement):
1130
1187
  """
1131
1188
  Get the `Workspace` which contain this views definition.
1132
1189
  """
1133
- return self._parent
1190
+ return self._parent
1191
+
1192
+ class StyleElements:
1193
+
1194
+ from buildzr.dsl.expression import Element
1195
+
1196
+ Shapes = Union[
1197
+ Literal['Box'],
1198
+ Literal['RoundedBox'],
1199
+ Literal['Circle'],
1200
+ Literal['Ellipse'],
1201
+ Literal['Hexagon'],
1202
+ Literal['Cylinder'],
1203
+ Literal['Pipe'],
1204
+ Literal['Person'],
1205
+ Literal['Robot'],
1206
+ Literal['Folder'],
1207
+ Literal['WebBrowser'],
1208
+ Literal['MobileDevicePortrait'],
1209
+ Literal['MobileDeviceLandscape'],
1210
+ Literal['Component'],
1211
+ ]
1212
+
1213
+ @property
1214
+ def model(self) -> List[buildzr.models.ElementStyle]:
1215
+ return self._m
1216
+
1217
+ @property
1218
+ def parent(self) -> Optional[Workspace]:
1219
+ return self._parent
1220
+
1221
+ # TODO: Validate arguments with pydantic.
1222
+ def __init__(
1223
+ self,
1224
+ on: List[Union[
1225
+ DslElement,
1226
+ Group,
1227
+ Callable[[Workspace, Element], bool],
1228
+ Type[Union['Person', 'SoftwareSystem', 'Container', 'Component']],
1229
+ str
1230
+ ]],
1231
+ shape: Optional[Shapes]=None,
1232
+ icon: Optional[str]=None,
1233
+ width: Optional[int]=None,
1234
+ height: Optional[int]=None,
1235
+ background: Optional[Union['str', Tuple[int, int, int], Color]]=None,
1236
+ color: Optional[Union['str', Tuple[int, int, int], Color]]=None,
1237
+ stroke: Optional[Union[str, Tuple[int, int, int], Color]]=None,
1238
+ stroke_width: Optional[int]=None,
1239
+ font_size: Optional[int]=None,
1240
+ border: Optional[Literal['solid', 'dashed', 'dotted']]=None,
1241
+ opacity: Optional[int]=None,
1242
+ metadata: Optional[bool]=None,
1243
+ description: Optional[bool]=None,
1244
+ ) -> None:
1245
+
1246
+ # How the tag is populated depends on each element type in the
1247
+ # `elemenets`.
1248
+ # - If the element is a `DslElement`, then we create a unique tag
1249
+ # specifically to help the stylizer identify that specific element.
1250
+ # For example, if the element has an id `3`, then we should create a
1251
+ # tag, say, `style-element-3`.
1252
+ # - If the element is a `Group`, then we simply make create the tag
1253
+ # based on the group name and its nested path. For example,
1254
+ # `Group:Company 1/Department 1`.
1255
+ # - If the element is a `Callable[[Workspace, Element], bool]`, we just
1256
+ # run the function to filter out all the elements that matches the
1257
+ # description, and create a unique tag for all of the filtered
1258
+ # elements.
1259
+ # - If the element is a `Type[Union['Person', 'SoftwareSystem', 'Container', 'Component']]`,
1260
+ # we create a tag based on the class name. This is based on the fact
1261
+ # that the default tag for each element is the element's type.
1262
+ # - If the element is a `str`, we just use the string as the tag.
1263
+ # This is useful for when you want to apply a style to all elements
1264
+ # with a specific tag, just like in the original Structurizr DSL.
1265
+ #
1266
+ # Note that a new `buildzr.models.ElementStyle` is created for each
1267
+ # item, not for each of `StyleElements` instance. This makes the styling
1268
+ # makes more concise and flexible.
1269
+
1270
+ from buildzr.dsl.expression import Element
1271
+ from uuid import uuid4
1272
+
1273
+ if background:
1274
+ assert Color.is_valid_color(background), "Invalid background color: {}".format(background)
1275
+ if color:
1276
+ assert Color.is_valid_color(color), "Invalid color: {}".format(color)
1277
+ if stroke:
1278
+ assert Color.is_valid_color(stroke), "Invalid stroke color: {}".format(stroke)
1279
+
1280
+ self._m: List[buildzr.models.ElementStyle] = []
1281
+ self._parent: Optional[Workspace] = None
1282
+
1283
+ workspace = _current_workspace.get()
1284
+ if workspace is not None:
1285
+ self._parent = workspace
1286
+
1287
+ self._elements = on
1288
+
1289
+ border_enum: Dict[str, buildzr.models.Border] = {
1290
+ 'solid': buildzr.models.Border.Solid,
1291
+ 'dashed': buildzr.models.Border.Dashed,
1292
+ 'dotted': buildzr.models.Border.Dotted,
1293
+ }
1294
+
1295
+ shape_enum: Dict[str, buildzr.models.Shape] = {
1296
+ 'Box': buildzr.models.Shape.Box,
1297
+ 'RoundedBox': buildzr.models.Shape.RoundedBox,
1298
+ 'Circle': buildzr.models.Shape.Circle,
1299
+ 'Ellipse': buildzr.models.Shape.Ellipse,
1300
+ 'Hexagon': buildzr.models.Shape.Hexagon,
1301
+ 'Cylinder': buildzr.models.Shape.Cylinder,
1302
+ 'Pipe': buildzr.models.Shape.Pipe,
1303
+ 'Person': buildzr.models.Shape.Person,
1304
+ 'Robot': buildzr.models.Shape.Robot,
1305
+ 'Folder': buildzr.models.Shape.Folder,
1306
+ 'WebBrowser': buildzr.models.Shape.WebBrowser,
1307
+ 'MobileDevicePortrait': buildzr.models.Shape.MobileDevicePortrait,
1308
+ 'MobileDeviceLandscape': buildzr.models.Shape.MobileDeviceLandscape,
1309
+ 'Component': buildzr.models.Shape.Component,
1310
+ }
1311
+
1312
+ # A single unique element to be applied to all elements
1313
+ # affected by this style.
1314
+ element_tag = "buildzr-styleelements-{}".format(uuid4().hex)
1315
+
1316
+ for element in self._elements:
1317
+
1318
+ element_style = buildzr.models.ElementStyle()
1319
+ element_style.shape = shape_enum[shape] if shape else None
1320
+ element_style.icon = icon
1321
+ element_style.width = width
1322
+ element_style.height = height
1323
+ element_style.background = Color(background).to_hex() if background else None
1324
+ element_style.color = Color(color).to_hex() if color else None
1325
+ element_style.stroke = Color(stroke).to_hex() if stroke else None
1326
+ element_style.strokeWidth = stroke_width
1327
+ element_style.fontSize = font_size
1328
+ element_style.border = border_enum[border] if border else None
1329
+ element_style.opacity = opacity
1330
+ element_style.metadata = metadata
1331
+ element_style.description = description
1332
+
1333
+ if isinstance(element, DslElement) and not isinstance(element.model, buildzr.models.Workspace):
1334
+ element_style.tag = element_tag
1335
+ element.add_tags(element_tag)
1336
+ elif isinstance(element, Group):
1337
+ element_style.tag = f"Group:{element.full_name()}"
1338
+ elif isinstance(element, type):
1339
+ element_style.tag = f"{element.__name__}"
1340
+ elif isinstance(element, str):
1341
+ element_style.tag = element
1342
+ elif callable(element):
1343
+ from buildzr.dsl.expression import Element, Expression
1344
+ if self._parent:
1345
+ matched_elems = Expression(include_elements=[element]).elements(self._parent)
1346
+ for e in matched_elems:
1347
+ element_style.tag = element_tag
1348
+ e.add_tags(element_tag)
1349
+ else:
1350
+ raise ValueError("Cannot use callable to select elements to style without a Workspace.")
1351
+ self._m.append(element_style)
1352
+
1353
+ workspace = _current_workspace.get()
1354
+ if workspace is not None:
1355
+ workspace.apply_style(self)
1356
+
1357
+ class StyleRelationships:
1358
+
1359
+ from buildzr.dsl.expression import Relationship
1360
+
1361
+ @property
1362
+ def model(self) -> List[buildzr.models.RelationshipStyle]:
1363
+ return self._m
1364
+
1365
+ @property
1366
+ def parent(self) -> Optional[Workspace]:
1367
+ return self._parent
1368
+
1369
+ def __init__(
1370
+ self,
1371
+ on: Optional[List[Union[
1372
+ DslRelationship,
1373
+ Group,
1374
+ Callable[[Workspace, Relationship], bool],
1375
+ str
1376
+ ]]]=None,
1377
+ thickness: Optional[int]=None,
1378
+ color: Optional[Union[str, Tuple[int, int, int], Color]]=None,
1379
+ routing: Optional[Literal['Direct', 'Orthogonal', 'Curved']]=None,
1380
+ font_size: Optional[int]=None,
1381
+ width: Optional[int]=None,
1382
+ dashed: Optional[bool]=None,
1383
+ position: Optional[int]=None,
1384
+ opacity: Optional[int]=None,
1385
+ ) -> None:
1386
+
1387
+ from uuid import uuid4
1388
+
1389
+ if color is not None:
1390
+ assert Color.is_valid_color(color), "Invalid color: {}".format(color)
1391
+
1392
+ routing_enum: Dict[str, buildzr.models.Routing1] = {
1393
+ 'Direct': buildzr.models.Routing1.Direct,
1394
+ 'Orthogonal': buildzr.models.Routing1.Orthogonal,
1395
+ 'Curved': buildzr.models.Routing1.Curved,
1396
+ }
1397
+
1398
+ self._m: List[buildzr.models.RelationshipStyle] = []
1399
+ self._parent: Optional[Workspace] = None
1400
+
1401
+ workspace = _current_workspace.get()
1402
+ if workspace is not None:
1403
+ self._parent = workspace
1404
+
1405
+ # A single unique tag to be applied to all relationships
1406
+ # affected by this style.
1407
+ relation_tag = "buildzr-stylerelationships-{}".format(uuid4().hex)
1408
+
1409
+ if on is None:
1410
+ self._m.append(buildzr.models.RelationshipStyle(
1411
+ thickness=thickness,
1412
+ color=Color(color).to_hex() if color else None,
1413
+ routing=routing_enum[routing] if routing else None,
1414
+ fontSize=font_size,
1415
+ width=width,
1416
+ dashed=dashed,
1417
+ position=position,
1418
+ opacity=opacity,
1419
+ tag="Relationship",
1420
+ ))
1421
+ else:
1422
+ for relationship in on:
1423
+
1424
+ relationship_style = buildzr.models.RelationshipStyle()
1425
+ relationship_style.thickness = thickness
1426
+ relationship_style.color = Color(color).to_hex() if color else None
1427
+ relationship_style.routing = routing_enum[routing] if routing else None
1428
+ relationship_style.fontSize = font_size
1429
+ relationship_style.width = width
1430
+ relationship_style.dashed = dashed
1431
+ relationship_style.position = position
1432
+ relationship_style.opacity = opacity
1433
+
1434
+ if isinstance(relationship, DslRelationship):
1435
+ relationship.add_tags(relation_tag)
1436
+ relationship_style.tag = relation_tag
1437
+ elif isinstance(relationship, Group):
1438
+ from buildzr.dsl.expression import Expression
1439
+ if self._parent:
1440
+ rels = Expression(include_relationships=[
1441
+ lambda w, r: r.source.group == relationship.full_name() and \
1442
+ r.destination.group == relationship.full_name()
1443
+ ]).relationships(self._parent)
1444
+ for r in rels:
1445
+ r.add_tags(relation_tag)
1446
+ relationship_style.tag = relation_tag
1447
+ else:
1448
+ raise ValueError("Cannot use callable to select elements to style without a Workspace.")
1449
+ elif isinstance(relationship, str):
1450
+ relationship_style.tag = relationship
1451
+ elif callable(relationship):
1452
+ from buildzr.dsl.expression import Expression
1453
+ if self._parent:
1454
+ matched_rels = Expression(include_relationships=[relationship]).relationships(self._parent)
1455
+ for matched_rel in matched_rels:
1456
+ matched_rel.add_tags(relation_tag)
1457
+ relationship_style.tag = relation_tag
1458
+ else:
1459
+ raise ValueError("Cannot use callable to select elements to style without a Workspace.")
1460
+ self._m.append(relationship_style)
1461
+
1462
+ workspace = _current_workspace.get()
1463
+ if workspace is not None:
1464
+ workspace.apply_style(self)
buildzr/dsl/explorer.py CHANGED
@@ -33,35 +33,14 @@ class Explorer:
33
33
  yield from explorer
34
34
 
35
35
  def walk_relationships(self) -> Generator[_Relationship, None, None]:
36
- import buildzr
37
- from buildzr.dsl.factory.gen_id import GenerateId
38
36
 
39
37
  if self._workspace_or_element.children:
40
- for child in self._workspace_or_element.children:
41
- # Relationships aren't materialized in the `Workspace` or in any
42
- # of the `DslElement`s. As such, we need to recreate the `_Relationship` objects
43
- # from the Structurizr model.
44
38
 
45
- if child.model.relationships and child.destinations:
46
- for relationship, destination in zip(child.model.relationships, child.destinations):
47
- fake_relationship = _Relationship(
48
- _UsesData(
49
- relationship=buildzr.models.Relationship(
50
- id=relationship.id,
51
- description=relationship.description,
52
- properties=relationship.properties,
53
- technology=relationship.technology,
54
- tags=relationship.tags,
55
- sourceId=relationship.sourceId,
56
- ),
57
- source=child,
58
- ),
59
- destination=destination,
60
- _include_in_model=False,
61
- )
62
- fake_relationship._tags = set(relationship.tags.split(','))
39
+ for child in self._workspace_or_element.children:
63
40
 
64
- yield fake_relationship
41
+ if child.relationships:
42
+ for relationship in child.relationships:
43
+ yield relationship
65
44
 
66
45
  explorer = Explorer(child).walk_relationships()
67
46
  yield from explorer
buildzr/dsl/expression.py CHANGED
@@ -88,6 +88,16 @@ class Element:
88
88
  def properties(self) -> Dict[str, Any]:
89
89
  return self._element.model.properties
90
90
 
91
+ @property
92
+ def group(self) -> Optional[str]:
93
+ """
94
+ Returns the group of the element. The group is a string that is used to
95
+ group elements in the Structurizr DSL.
96
+ """
97
+ if not isinstance(self._element.model, buildzr.models.Workspace):
98
+ return self._element.model.group
99
+ return None
100
+
91
101
  def __eq__(self, element: object) -> bool:
92
102
  return isinstance(element, type(self._element)) and\
93
103
  element.model.id == self._element.model.id
@@ -11,6 +11,7 @@ from typing import (
11
11
  Callable,
12
12
  overload,
13
13
  Sequence,
14
+ MutableSet,
14
15
  cast,
15
16
  )
16
17
  from typing_extensions import (
@@ -125,11 +126,24 @@ class DslElement(BindRight[TSrc, TDst]):
125
126
  def destinations(self) -> List['DslElement']:
126
127
  pass
127
128
 
129
+ @property
130
+ @abstractmethod
131
+ def relationships(self) -> MutableSet['DslRelationship']:
132
+ pass
133
+
128
134
  @property
129
135
  @abstractmethod
130
136
  def tags(self) -> Set[str]:
131
137
  pass
132
138
 
139
+ def add_tags(self, *tags: str) -> None:
140
+ """
141
+ Add tags to the element.
142
+ """
143
+ self.tags.update(tags)
144
+ if not isinstance(self.model, buildzr.models.Workspace):
145
+ self.model.tags = ','.join(self.tags)
146
+
133
147
  def uses(
134
148
  self,
135
149
  other: 'DslElement',
@@ -167,6 +181,13 @@ class DslRelationship(ABC, Generic[TSrc, TDst]):
167
181
  def destination(self) -> DslElement:
168
182
  pass
169
183
 
184
+ def add_tags(self, *tags: str) -> None:
185
+ """
186
+ Adds tags to the relationship.
187
+ """
188
+ self.tags.update(tags)
189
+ self.model.tags = ','.join(self.tags)
190
+
170
191
  def __contains__(self, other: 'DslElement') -> bool:
171
192
  return self.source.model.id == other.model.id or self.destination.model.id == other.model.id
172
193
 
buildzr/dsl/relations.py CHANGED
@@ -102,6 +102,10 @@ class _Relationship(DslRelationship[TSrc, TDst]):
102
102
  if not any([self._src.model.id == src.model.id for src in self._dst.sources]):
103
103
  self._dst.sources.append(self._src)
104
104
 
105
+ # Make this relationship accessible from the source element.
106
+ if not any([r.model.id == self.model.id for r in self._src.relationships]):
107
+ self._src.relationships.add(self)
108
+
105
109
  if _include_in_model:
106
110
  if uses_data.source.model.relationships:
107
111
  uses_data.source.model.relationships.append(uses_data.relationship)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: buildzr
3
- Version: 0.0.8
3
+ Version: 0.0.10
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
@@ -1,14 +1,15 @@
1
- buildzr/__about__.py,sha256=4c61OddVWS6L9IjCPX5Ldepb6zkgj8z9bKRq9nu3ESs,17
1
+ buildzr/__about__.py,sha256=wEjCYQzurEzDt1Qp6SwlCKMqyhpgH57_IpRlf0zm9fk,18
2
2
  buildzr/__init__.py,sha256=hY-cOdjBQcz0v2m8cBF1oEJFIbcR3sWI-xww--0RKSo,99
3
- buildzr/dsl/__init__.py,sha256=paxuMCCDuOs1eSvBPyuW5pv5j1UZD6TxRZcCzC2iKss,307
4
- buildzr/dsl/dsl.py,sha256=gjYExKxrbv3QyJFNknkW5lPpleYysvDDlllM5NSB0Xg,40395
5
- buildzr/dsl/explorer.py,sha256=numMPqD3RYJ1oeMgX5wYnT6aHRHmBN2EsFZFYRuFffA,2523
6
- buildzr/dsl/expression.py,sha256=k5R98lW15TYepWV0P-2AYCmUw_gOceGGiLJwynm-NMI,7913
7
- buildzr/dsl/relations.py,sha256=lypzwpBuK9IHtz60T_oI8l0pZh4sLp_ylorW_7I7QJo,11679
3
+ buildzr/dsl/__init__.py,sha256=k0G9blhA3NSzCBs1dSfBNzCh0iDS_ylkzS_5a3pqrSY,375
4
+ buildzr/dsl/color.py,sha256=at5lo3WgLEDCjrnbu37ra1p1TjzdB51sxeW7pBMC_7U,4019
5
+ buildzr/dsl/dsl.py,sha256=47_O2OqN6XR7MNZnmJK2EaX8JPZrGu-h5YEVEaLUwdM,54154
6
+ buildzr/dsl/explorer.py,sha256=vLAgQEYd0h22QuVfWfBdk4zyDpGaE1T67Pn9V7P1C-I,1238
7
+ buildzr/dsl/expression.py,sha256=EMAtaZQA0O_zwENCZs3l4u_w9wUuO09XA0LgnvsomfA,8256
8
+ buildzr/dsl/relations.py,sha256=GBs5epr9uuExU_H6VcP4XY76iJPQ__rz_d8tZlhhWQ4,11891
8
9
  buildzr/dsl/factory/__init__.py,sha256=niaYqvNPUWJejoPyRyABUtzVsoxaV8eSjzS9dta4bMI,30
9
10
  buildzr/dsl/factory/gen_id.py,sha256=LnaeOCMngSvYkcGnuARjQYoUVWdcOoNHO2EHe6PMGco,538
10
11
  buildzr/dsl/interfaces/__init__.py,sha256=ncYARIPB4ARcCCRObgV1b4jluEubpm2s46mp1ZC8Urk,197
11
- buildzr/dsl/interfaces/interfaces.py,sha256=sYBi887fLr6Nrw_dTkvwOjoIfzjo7VcMY5O_wshv0Jk,4994
12
+ buildzr/dsl/interfaces/interfaces.py,sha256=j8UCPCRegmFZ2Jl4qCz0uW0OxBS2mRe0VjPcNExH8mc,5553
12
13
  buildzr/encoders/__init__.py,sha256=suID63Ay-023T0uKD25EAoGYmAMTa9AKxIjioccpiPM,32
13
14
  buildzr/encoders/encoder.py,sha256=n8WLVMrisykBTLTr1z6PAxgqhqW2dFRZhSupOuMdx_A,3235
14
15
  buildzr/models/__init__.py,sha256=SRfF7oDVlOOAi6nGKiJIUK6B_arqYLO9iSMp-2IZZps,21
@@ -17,7 +18,7 @@ buildzr/models/models.py,sha256=0LhLG1wmbt4dvROV5MEBZLLoxPbMpkUsOqNz525cynE,4248
17
18
  buildzr/sinks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
19
  buildzr/sinks/interfaces.py,sha256=LOZekP4WNjomD5J5f3FnZTwGj0aXMr6RbrvyFV5zn0E,383
19
20
  buildzr/sinks/json_sink.py,sha256=onKOZTpwOQfeMEj1ONkuIEHBAQhx4yQSqqI_lgZBaP8,777
20
- buildzr-0.0.8.dist-info/METADATA,sha256=83EbUbu1-_Amvjh2oweVgAy6NpRop3eM_K-CiDfkDqs,6578
21
- buildzr-0.0.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
22
- buildzr-0.0.8.dist-info/licenses/LICENSE.md,sha256=e8e6W6tL4MbBY-c-gXMgDbaMf_BnaQDQv4Yoy42b-CI,1070
23
- buildzr-0.0.8.dist-info/RECORD,,
21
+ buildzr-0.0.10.dist-info/METADATA,sha256=CWSrdAVndP4HsBW00aeGNVqDmngxJqEE1VqKa_97E8k,6579
22
+ buildzr-0.0.10.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
+ buildzr-0.0.10.dist-info/licenses/LICENSE.md,sha256=e8e6W6tL4MbBY-c-gXMgDbaMf_BnaQDQv4Yoy42b-CI,1070
24
+ buildzr-0.0.10.dist-info/RECORD,,