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.
@@ -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