buildzr 0.0.8__py3-none-any.whl → 0.0.9__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 +1 -1
- buildzr/dsl/__init__.py +4 -1
- buildzr/dsl/color.py +121 -0
- buildzr/dsl/dsl.py +350 -19
- buildzr/dsl/explorer.py +4 -25
- buildzr/dsl/expression.py +10 -0
- buildzr/dsl/interfaces/interfaces.py +21 -0
- buildzr/dsl/relations.py +4 -0
- {buildzr-0.0.8.dist-info → buildzr-0.0.9.dist-info}/METADATA +1 -1
- {buildzr-0.0.8.dist-info → buildzr-0.0.9.dist-info}/RECORD +12 -11
- {buildzr-0.0.8.dist-info → buildzr-0.0.9.dist-info}/WHEEL +0 -0
- {buildzr-0.0.8.dist-info → buildzr-0.0.9.dist-info}/licenses/LICENSE.md +0 -0
buildzr/__about__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
VERSION = "0.0.
|
1
|
+
VERSION = "0.0.9"
|
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
|
-
|
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
|
-
|
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
|
-
|
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,14 +1,15 @@
|
|
1
|
-
buildzr/__about__.py,sha256=
|
1
|
+
buildzr/__about__.py,sha256=DgAnctESS7a-r7bKUfHvaK2XtP5BpQpkO_wLESIfHjc,17
|
2
2
|
buildzr/__init__.py,sha256=hY-cOdjBQcz0v2m8cBF1oEJFIbcR3sWI-xww--0RKSo,99
|
3
|
-
buildzr/dsl/__init__.py,sha256=
|
4
|
-
buildzr/dsl/
|
5
|
-
buildzr/dsl/
|
6
|
-
buildzr/dsl/
|
7
|
-
buildzr/dsl/
|
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=
|
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.
|
21
|
-
buildzr-0.0.
|
22
|
-
buildzr-0.0.
|
23
|
-
buildzr-0.0.
|
21
|
+
buildzr-0.0.9.dist-info/METADATA,sha256=qjmMwahyMKpFXfFQZj4p_j_X0VVC-j3Lknwm_7NRJLQ,6578
|
22
|
+
buildzr-0.0.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
23
|
+
buildzr-0.0.9.dist-info/licenses/LICENSE.md,sha256=e8e6W6tL4MbBY-c-gXMgDbaMf_BnaQDQv4Yoy42b-CI,1070
|
24
|
+
buildzr-0.0.9.dist-info/RECORD,,
|
File without changes
|
File without changes
|