buildzr 0.0.1__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 -0
- buildzr/__init__.py +4 -0
- buildzr/dsl/__init__.py +18 -0
- buildzr/dsl/dsl.py +990 -0
- buildzr/dsl/explorer.py +67 -0
- buildzr/dsl/expression.py +208 -0
- buildzr/dsl/factory/__init__.py +1 -0
- buildzr/dsl/factory/gen_id.py +23 -0
- buildzr/dsl/interfaces/__init__.py +14 -0
- buildzr/dsl/interfaces/interfaces.py +207 -0
- buildzr/dsl/relations.py +367 -0
- buildzr/encoders/__init__.py +1 -0
- buildzr/encoders/encoder.py +61 -0
- buildzr/models/__init__.py +1 -0
- buildzr/models/generate.sh +21 -0
- buildzr/models/models.py +1739 -0
- buildzr-0.0.1.dist-info/METADATA +140 -0
- buildzr-0.0.1.dist-info/RECORD +20 -0
- buildzr-0.0.1.dist-info/WHEEL +4 -0
- buildzr-0.0.1.dist-info/licenses/LICENSE.md +21 -0
buildzr/dsl/relations.py
ADDED
@@ -0,0 +1,367 @@
|
|
1
|
+
from typing import (
|
2
|
+
Any,
|
3
|
+
List,
|
4
|
+
Tuple,
|
5
|
+
Union,
|
6
|
+
Set,
|
7
|
+
Generic,
|
8
|
+
Optional,
|
9
|
+
Callable,
|
10
|
+
Sequence,
|
11
|
+
Dict,
|
12
|
+
cast,
|
13
|
+
overload,
|
14
|
+
)
|
15
|
+
from typing_extensions import Self, TypeIs
|
16
|
+
from dataclasses import dataclass
|
17
|
+
from buildzr.dsl.interfaces import (
|
18
|
+
BindLeft,
|
19
|
+
BindLeftLate,
|
20
|
+
DslRelationship,
|
21
|
+
DslFluentRelationship,
|
22
|
+
DslElement,
|
23
|
+
DslWorkspaceElement,
|
24
|
+
TSrc, TDst,
|
25
|
+
TParent, TChild,
|
26
|
+
)
|
27
|
+
from buildzr.dsl.factory import GenerateId
|
28
|
+
import buildzr
|
29
|
+
|
30
|
+
def _is_software_fluent_relationship(
|
31
|
+
obj: '_FluentRelationship[Any]'
|
32
|
+
) -> TypeIs['_FluentRelationship[buildzr.dsl.SoftwareSystem]']:
|
33
|
+
return isinstance(obj._parent, buildzr.dsl.SoftwareSystem)
|
34
|
+
|
35
|
+
def _is_container_fluent_relationship(
|
36
|
+
obj: '_FluentRelationship[Any]'
|
37
|
+
) -> TypeIs['_FluentRelationship[buildzr.dsl.Container]']:
|
38
|
+
return isinstance(obj._parent, buildzr.dsl.Container)
|
39
|
+
|
40
|
+
@dataclass
|
41
|
+
class With:
|
42
|
+
tags: Optional[Set[str]] = None
|
43
|
+
properties: Optional[Dict[str, str]] = None
|
44
|
+
url: Optional[str] = None
|
45
|
+
|
46
|
+
@dataclass
|
47
|
+
class _UsesData(Generic[TSrc]):
|
48
|
+
relationship: buildzr.models.Relationship
|
49
|
+
source: TSrc
|
50
|
+
|
51
|
+
def desc(value: str, tech: Optional[str]=None) -> '_RelationshipDescription[DslElement]':
|
52
|
+
if tech is None:
|
53
|
+
return _RelationshipDescription(value)
|
54
|
+
else:
|
55
|
+
return _RelationshipDescription(value, tech)
|
56
|
+
|
57
|
+
class _UsesFrom(BindLeft[TSrc, TDst]):
|
58
|
+
|
59
|
+
def __init__(self, source: TSrc, description: str="", technology: str="") -> None:
|
60
|
+
self.uses_data = _UsesData(
|
61
|
+
relationship=buildzr.models.Relationship(
|
62
|
+
id=GenerateId.for_relationship(),
|
63
|
+
description=description,
|
64
|
+
technology=technology,
|
65
|
+
sourceId=str(source.model.id),
|
66
|
+
),
|
67
|
+
source=source,
|
68
|
+
)
|
69
|
+
|
70
|
+
def __rshift__(self, destination: TDst) -> '_Relationship[TSrc, TDst]':
|
71
|
+
if isinstance(destination, DslWorkspaceElement):
|
72
|
+
raise TypeError(f"Unsupported operand type for >>: '{type(self).__name__}' and {type(destination).__name__}")
|
73
|
+
return _Relationship(self.uses_data, destination)
|
74
|
+
|
75
|
+
class _Relationship(DslRelationship[TSrc, TDst]):
|
76
|
+
|
77
|
+
@property
|
78
|
+
def model(self) -> buildzr.models.Relationship:
|
79
|
+
return self._m
|
80
|
+
|
81
|
+
@property
|
82
|
+
def tags(self) -> Set[str]:
|
83
|
+
return self._tags
|
84
|
+
|
85
|
+
@property
|
86
|
+
def source(self) -> DslElement:
|
87
|
+
return self._src
|
88
|
+
|
89
|
+
@property
|
90
|
+
def destination(self) -> DslElement:
|
91
|
+
return self._dst
|
92
|
+
|
93
|
+
def __init__(
|
94
|
+
self,
|
95
|
+
uses_data: _UsesData[TSrc],
|
96
|
+
destination: TDst,
|
97
|
+
tags: Set[str]=set(),
|
98
|
+
_include_in_model: bool=True,
|
99
|
+
) -> None:
|
100
|
+
|
101
|
+
self._m = uses_data.relationship
|
102
|
+
self._tags = {'Relationship'}.union(tags)
|
103
|
+
self._src = uses_data.source
|
104
|
+
self._dst = destination
|
105
|
+
self.model.tags = ','.join(self._tags)
|
106
|
+
|
107
|
+
uses_data.relationship.destinationId = str(destination.model.id)
|
108
|
+
|
109
|
+
if not isinstance(uses_data.source.model, buildzr.models.Workspace):
|
110
|
+
uses_data.source.destinations.append(self._dst)
|
111
|
+
self._dst.sources.append(self._src)
|
112
|
+
if _include_in_model:
|
113
|
+
if uses_data.source.model.relationships:
|
114
|
+
uses_data.source.model.relationships.append(uses_data.relationship)
|
115
|
+
else:
|
116
|
+
uses_data.source.model.relationships = [uses_data.relationship]
|
117
|
+
|
118
|
+
# Used to pass the `_UsesData` object as reference to the `__or__`
|
119
|
+
# operator overloading method.
|
120
|
+
self._ref: Tuple[_UsesData] = (uses_data,)
|
121
|
+
|
122
|
+
def __or__(self, _with: With) -> Self:
|
123
|
+
return self.has(
|
124
|
+
tags=_with.tags,
|
125
|
+
properties=_with.properties,
|
126
|
+
url=_with.url,
|
127
|
+
)
|
128
|
+
|
129
|
+
def has(
|
130
|
+
self,
|
131
|
+
tags: Optional[Set[str]]=None,
|
132
|
+
properties: Optional[Dict[str, str]]=None,
|
133
|
+
url: Optional[str]=None,
|
134
|
+
) -> Self:
|
135
|
+
"""
|
136
|
+
Adds extra info to the relationship.
|
137
|
+
|
138
|
+
This can also be achieved using the syntax sugar `DslRelationship |
|
139
|
+
With(...)`.
|
140
|
+
"""
|
141
|
+
if tags:
|
142
|
+
self._tags = self._tags.union(tags)
|
143
|
+
self._ref[0].relationship.tags = ",".join(self._tags)
|
144
|
+
if properties:
|
145
|
+
self._ref[0].relationship.properties = properties
|
146
|
+
if url:
|
147
|
+
self._ref[0].relationship.url = url
|
148
|
+
return self
|
149
|
+
|
150
|
+
class _FluentRelationship(DslFluentRelationship[TParent]):
|
151
|
+
|
152
|
+
"""
|
153
|
+
A hidden class used in the fluent DSL syntax after specifying a model (i.e.,
|
154
|
+
Person, Software System, Container) to define relationship(s) within the
|
155
|
+
specified model.
|
156
|
+
"""
|
157
|
+
|
158
|
+
def __init__(self, parent: TParent) -> None:
|
159
|
+
self._parent: TParent = parent
|
160
|
+
|
161
|
+
def where(
|
162
|
+
self,
|
163
|
+
func: Callable[
|
164
|
+
[TParent],
|
165
|
+
Sequence[
|
166
|
+
Union[
|
167
|
+
DslRelationship,
|
168
|
+
Sequence[DslRelationship]
|
169
|
+
]
|
170
|
+
]
|
171
|
+
], implied: bool=False) -> TParent:
|
172
|
+
|
173
|
+
relationships: Sequence[DslRelationship] = []
|
174
|
+
|
175
|
+
func = cast(Callable[[TParent], Sequence[Union[DslRelationship, Sequence[DslRelationship]]]], func)
|
176
|
+
|
177
|
+
# Flatten the resulting relationship list.
|
178
|
+
relationships = [
|
179
|
+
rel for sublist in func(self._parent)
|
180
|
+
for rel in (
|
181
|
+
sublist if isinstance(sublist, list) else [sublist]
|
182
|
+
)
|
183
|
+
]
|
184
|
+
|
185
|
+
# If we have relationship s >> do >> a.b, then create s >> do >> a.
|
186
|
+
# If we have relationship s.ss >> do >> a.b.c, then create s.ss >> do >> a.b and s.ss >> do >> a.
|
187
|
+
# And so on...
|
188
|
+
if implied:
|
189
|
+
for relationship in relationships:
|
190
|
+
source = relationship.source
|
191
|
+
parent = relationship.destination.parent
|
192
|
+
while parent is not None and not isinstance(parent, DslWorkspaceElement):
|
193
|
+
r = source.uses(parent, description=relationship.model.description, technology=relationship.model.technology)
|
194
|
+
r.model.linkedRelationshipId = relationship.model.id
|
195
|
+
parent = parent.parent
|
196
|
+
|
197
|
+
return self._parent
|
198
|
+
|
199
|
+
def get(self) -> TParent:
|
200
|
+
return self._parent
|
201
|
+
|
202
|
+
class _RelationshipDescription(Generic[TDst]):
|
203
|
+
|
204
|
+
def __init__(self, description: str, technology: Optional[str]=None) -> None:
|
205
|
+
self._description = description
|
206
|
+
self._technology = technology
|
207
|
+
|
208
|
+
def __rshift__(self, destination: TDst) -> '_UsesFromLate[TDst]':
|
209
|
+
if self._technology:
|
210
|
+
return _UsesFromLate(
|
211
|
+
description=(self._description, self._technology),
|
212
|
+
destination=destination,
|
213
|
+
)
|
214
|
+
return _UsesFromLate(
|
215
|
+
description=self._description,
|
216
|
+
destination=destination,
|
217
|
+
)
|
218
|
+
|
219
|
+
class _UsesFromLate(BindLeftLate[TDst]):
|
220
|
+
"""
|
221
|
+
This method is used to create a relationship between one source element with
|
222
|
+
multiple destination elements, like so:
|
223
|
+
|
224
|
+
```python
|
225
|
+
u = Person("user")
|
226
|
+
s1 = SoftwareSystem("software1")
|
227
|
+
s2 = SoftwareSystem("software2")
|
228
|
+
|
229
|
+
# Each element in the following list is a `_UsesFromLate` object.
|
230
|
+
u >> [
|
231
|
+
"Uses" >> s1 | With(tags={"linux", "rules"}),
|
232
|
+
("Reads from", "SQL") >> s1,
|
233
|
+
]
|
234
|
+
```
|
235
|
+
|
236
|
+
This requires late left binding (i.e., the source element is bound after the
|
237
|
+
the destination elements in the list are bounded. This is in contrast to how
|
238
|
+
`_UsesFrom` works, where `u >> "Uses >> s1` binds the source element `u`
|
239
|
+
first (i.e., in `u >> "Uses"` into `_UsesFrom` before finally binding `s1`
|
240
|
+
in `((u >> "Uses) >> s2)`).
|
241
|
+
"""
|
242
|
+
|
243
|
+
PossibleSourceType = Optional[DslElement]
|
244
|
+
|
245
|
+
@dataclass
|
246
|
+
class _LateBindData:
|
247
|
+
tags: Optional[Set[str]] = None
|
248
|
+
properties: Optional[Dict[str, str]] = None
|
249
|
+
url: Optional[str] = None
|
250
|
+
|
251
|
+
def __init__(self, description: Union[str, Tuple[str, str]], destination: TDst) -> None:
|
252
|
+
if isinstance(description, str):
|
253
|
+
self._description = description
|
254
|
+
self._technology: Optional[str] = None
|
255
|
+
elif isinstance(description, tuple) and len(description) == 2:
|
256
|
+
self._description = description[0]
|
257
|
+
self._technology = description[1]
|
258
|
+
self._source: Optional[_UsesFromLate.PossibleSourceType] = None
|
259
|
+
self._destination = destination
|
260
|
+
self._relationship: Optional[_Relationship[_UsesFromLate.PossibleSourceType, TDst]] = None
|
261
|
+
self._late_bind_data: _UsesFromLate._LateBindData = _UsesFromLate._LateBindData()
|
262
|
+
|
263
|
+
def set_source(self, source: PossibleSourceType) -> None:
|
264
|
+
self._source = source
|
265
|
+
self._relationship = _Relationship(
|
266
|
+
uses_data=_UsesData(
|
267
|
+
relationship=buildzr.models.Relationship(
|
268
|
+
id=GenerateId.for_relationship(),
|
269
|
+
description=self._description,
|
270
|
+
technology=self._technology,
|
271
|
+
sourceId=str(self._source.model.id),
|
272
|
+
),
|
273
|
+
source=self._source,
|
274
|
+
),
|
275
|
+
destination=self._destination,
|
276
|
+
)
|
277
|
+
self._late_bind_with()
|
278
|
+
|
279
|
+
def get_relationship(self) -> Optional['_Relationship[PossibleSourceType, TDst]']:
|
280
|
+
return self._relationship
|
281
|
+
|
282
|
+
def _late_bind_with(self) -> None:
|
283
|
+
"""
|
284
|
+
Binds tags, properties, url to the relationship.
|
285
|
+
Called once the relationship is set.
|
286
|
+
"""
|
287
|
+
self._relationship = self._relationship.has(
|
288
|
+
tags=self._late_bind_data.tags,
|
289
|
+
properties=self._late_bind_data.properties,
|
290
|
+
url=self._late_bind_data.url,
|
291
|
+
)
|
292
|
+
|
293
|
+
|
294
|
+
def __or__(self, other: With) -> Self:
|
295
|
+
self._late_bind_data.tags = other.tags
|
296
|
+
self._late_bind_data.properties = other.properties
|
297
|
+
self._late_bind_data.url = other.url
|
298
|
+
|
299
|
+
return self
|
300
|
+
|
301
|
+
class DslElementRelationOverrides(DslElement):
|
302
|
+
|
303
|
+
"""
|
304
|
+
Base class meant to be derived from to override the `__rshift__` method to
|
305
|
+
allow for the `>>` operator to be used to create relationships between
|
306
|
+
elements.
|
307
|
+
"""
|
308
|
+
|
309
|
+
@overload # type: ignore[override]
|
310
|
+
def __rshift__(self, description_and_technology: Tuple[str, str]) -> _UsesFrom[Self, DslElement]:
|
311
|
+
...
|
312
|
+
|
313
|
+
@overload
|
314
|
+
def __rshift__(self, description: str) -> _UsesFrom[Self, DslElement]:
|
315
|
+
...
|
316
|
+
|
317
|
+
@overload
|
318
|
+
def __rshift__(self, _RelationshipDescription: _RelationshipDescription[DslElement]) -> _UsesFrom[Self, DslElement]:
|
319
|
+
...
|
320
|
+
|
321
|
+
@overload
|
322
|
+
def __rshift__(self, multiple_destinations: List[_UsesFromLate[DslElement]]) -> List[_Relationship[Self, DslElement]]:
|
323
|
+
...
|
324
|
+
|
325
|
+
def __rshift__(
|
326
|
+
self,
|
327
|
+
other: Union[
|
328
|
+
str,
|
329
|
+
Tuple[str, str],
|
330
|
+
_RelationshipDescription[DslElement],
|
331
|
+
List[_UsesFromLate[DslElement]]
|
332
|
+
]) -> Union[_UsesFrom[Self, DslElement], List[_Relationship[Self, DslElement]]]:
|
333
|
+
if isinstance(other, str):
|
334
|
+
return _UsesFrom(self, other)
|
335
|
+
elif isinstance(other, tuple):
|
336
|
+
return _UsesFrom(self, description=other[0], technology=other[1])
|
337
|
+
elif isinstance(other, _RelationshipDescription):
|
338
|
+
return _UsesFrom(self, description=other._description, technology=other._technology)
|
339
|
+
elif isinstance(other, list):
|
340
|
+
relationships = []
|
341
|
+
for dest in other:
|
342
|
+
dest.set_source(self)
|
343
|
+
relationships.append(dest.get_relationship())
|
344
|
+
return cast(List[_Relationship[Self, DslElement]], relationships)
|
345
|
+
else:
|
346
|
+
raise TypeError(f"Unsupported operand type for >>: '{type(self).__name__}' and {type(other).__name__}")
|
347
|
+
|
348
|
+
def uses(
|
349
|
+
self,
|
350
|
+
other: DslElement,
|
351
|
+
description: Optional[str]=None,
|
352
|
+
technology: Optional[str]=None,
|
353
|
+
tags: Set[str]=set()) -> _Relationship[Self, DslElement]:
|
354
|
+
|
355
|
+
source = self
|
356
|
+
|
357
|
+
uses_from = _UsesFrom[Self, DslElement](
|
358
|
+
source=source,
|
359
|
+
description=description,
|
360
|
+
technology=technology
|
361
|
+
)
|
362
|
+
|
363
|
+
return _Relationship(
|
364
|
+
uses_from.uses_data,
|
365
|
+
destination=other,
|
366
|
+
tags=tags,
|
367
|
+
)
|
@@ -0,0 +1 @@
|
|
1
|
+
from .encoder import JsonEncoder
|
@@ -0,0 +1,61 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
import dataclasses, json
|
3
|
+
import enum
|
4
|
+
import humps
|
5
|
+
from buildzr.dsl.interfaces import DslElement, DslWorkspaceElement
|
6
|
+
from typing import Union, List, TYPE_CHECKING, Type, Any
|
7
|
+
from typing_extensions import TypeGuard
|
8
|
+
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from _typeshed import DataclassInstance
|
11
|
+
JsonEncodable = Union[DslElement, DslWorkspaceElement, DataclassInstance, enum.Enum]
|
12
|
+
else:
|
13
|
+
# Need this so that when we're not type checking with mypy, we're still on
|
14
|
+
# the clear.
|
15
|
+
JsonEncodable = Union[Any, None]
|
16
|
+
|
17
|
+
def _is_dataclass(obj: JsonEncodable) -> TypeGuard['DataclassInstance']:
|
18
|
+
"""
|
19
|
+
Make mypy happy by ensuring that `obj` is indeed a `DataclassInstance`, and
|
20
|
+
not merely its `Type[DataclassInstance]`.
|
21
|
+
"""
|
22
|
+
|
23
|
+
return dataclasses.is_dataclass(obj) and not isinstance(obj, type)
|
24
|
+
|
25
|
+
def _remove_nones(d: Union[dict[str, Any], List[Any]]) -> Union[dict[str, Any], List[Any]]:
|
26
|
+
|
27
|
+
"""
|
28
|
+
Remove the `null` valued JSON objects, as they're causing problem when read
|
29
|
+
by Structurizr.
|
30
|
+
|
31
|
+
I'm lazy. This code is stolen and modified from https://stackoverflow.com/a/66127889.
|
32
|
+
"""
|
33
|
+
|
34
|
+
if isinstance(d, dict):
|
35
|
+
for key, value in list(d.items()):
|
36
|
+
if isinstance(value, (list, dict)):
|
37
|
+
d[key] = _remove_nones(value)
|
38
|
+
elif value is None or key is None:
|
39
|
+
del d[key]
|
40
|
+
elif isinstance(d, list):
|
41
|
+
if isinstance(d, list):
|
42
|
+
d = [_remove_nones(item) for item in d if item is not None]
|
43
|
+
|
44
|
+
return d
|
45
|
+
|
46
|
+
class JsonEncoder(json.JSONEncoder):
|
47
|
+
def default(self, obj: JsonEncodable) -> Union[str, list, dict]:
|
48
|
+
# Handle the default encoder the nicely wrapped DSL elements.
|
49
|
+
if isinstance(obj, DslElement) or isinstance(obj, DslWorkspaceElement):
|
50
|
+
return humps.camelize(_remove_nones(dataclasses.asdict(obj.model)))
|
51
|
+
|
52
|
+
# Handle the default encoder for those `dataclass`es models generated in
|
53
|
+
# `buildzr.model`
|
54
|
+
elif _is_dataclass(obj):
|
55
|
+
return humps.camelize(_remove_nones(dataclasses.asdict(obj)))
|
56
|
+
|
57
|
+
# Handle the enums
|
58
|
+
elif isinstance(obj, enum.Enum):
|
59
|
+
return str(obj.value)
|
60
|
+
|
61
|
+
return super().default(obj) #type: ignore[no-any-return]
|
@@ -0,0 +1 @@
|
|
1
|
+
from .models import *
|
@@ -0,0 +1,21 @@
|
|
1
|
+
schema_url=https://raw.githubusercontent.com/structurizr/json/master/structurizr.yaml
|
2
|
+
|
3
|
+
curl $schema_url > structurizr.yaml
|
4
|
+
|
5
|
+
# Change from 'long' (unsupported) to 'integer'
|
6
|
+
yq -i -y '.components.schemas.Workspace.properties.id.type = "integer"' structurizr.yaml
|
7
|
+
|
8
|
+
# Type 'integer' doesn't support 'number' type, but supports the following:
|
9
|
+
# int32, int64, default, date-time, unix-time
|
10
|
+
# yq -i 'select(.components.schemas.*.properties.*.format=="integer" and .components.schemas.*.properties.*.type=="number") .components.schemas.*.properties.*.format="default"' structurizr.yaml
|
11
|
+
|
12
|
+
# Format 'url' isn't supported. Change the format to 'string' and type to 'uri'.
|
13
|
+
# yq -i 'select(.components.schemas.*.properties.*.format=="url" and .components.schemas.*.properties.*.type=="string") .components.schemas.*.properties.*.format="uri"' structurizr.yaml
|
14
|
+
|
15
|
+
datamodel-codegen \
|
16
|
+
--input-file-type openapi \
|
17
|
+
--output-model-type dataclasses.dataclass \
|
18
|
+
--input structurizr.yaml \
|
19
|
+
--output models.py \
|
20
|
+
--use-schema-description \
|
21
|
+
--use-field-description
|