run-radar-run 0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- run_radar_run-0.1/PKG-INFO +60 -0
- run_radar_run-0.1/README.md +33 -0
- run_radar_run-0.1/pyproject.toml +45 -0
- run_radar_run-0.1/runradarrun/ingest.py +69 -0
- run_radar_run-0.1/runradarrun/main.py +115 -0
- run_radar_run-0.1/runradarrun/model.py +168 -0
- run_radar_run-0.1/runradarrun/output.py +53 -0
- run_radar_run-0.1/runradarrun/publishers/twbyor.py +88 -0
- run_radar_run-0.1/runradarrun/publishers/zalando.py +161 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: run-radar-run
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Run, Radar, Run - Running your own tech radar should be easy
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: tech-radar
|
|
7
|
+
Author: Alexei Znamensky
|
|
8
|
+
Author-email: russoz@gmail.com
|
|
9
|
+
Requires-Python: >=3.12,<4.0
|
|
10
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Information Technology
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Testing
|
|
20
|
+
Classifier: Topic :: Utilities
|
|
21
|
+
Requires-Dist: blessed (>=1.20.0)
|
|
22
|
+
Requires-Dist: docker (>=7.1.0)
|
|
23
|
+
Requires-Dist: gitpython (>=3.1.44)
|
|
24
|
+
Requires-Dist: pyyaml (>=5.4)
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
Run, Radar, Run
|
|
28
|
+
===============
|
|
29
|
+
|
|
30
|
+
Using run-radar-run
|
|
31
|
+
-------------------
|
|
32
|
+
|
|
33
|
+
### Requirements
|
|
34
|
+
|
|
35
|
+
* Python3.12+
|
|
36
|
+
* Docker (for the TWBYOR radar)
|
|
37
|
+
|
|
38
|
+
### Running the Radar
|
|
39
|
+
|
|
40
|
+
Clone this repository and run:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
$ ./run-radar-run
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or if you intend to see a specific radar directory, run:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
$ ./run-radar-run -i tests/radar
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Publishers
|
|
53
|
+
|
|
54
|
+
Publishers are the frameworks you can use to publish your tech radar.
|
|
55
|
+
|
|
56
|
+
As of early 2025, the publishers available are:
|
|
57
|
+
|
|
58
|
+
* Thoughtworks' Bring Your Own Radar
|
|
59
|
+
* Zalando Tech Radar
|
|
60
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Run, Radar, Run
|
|
2
|
+
===============
|
|
3
|
+
|
|
4
|
+
Using run-radar-run
|
|
5
|
+
-------------------
|
|
6
|
+
|
|
7
|
+
### Requirements
|
|
8
|
+
|
|
9
|
+
* Python3.12+
|
|
10
|
+
* Docker (for the TWBYOR radar)
|
|
11
|
+
|
|
12
|
+
### Running the Radar
|
|
13
|
+
|
|
14
|
+
Clone this repository and run:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
$ ./run-radar-run
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or if you intend to see a specific radar directory, run:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
$ ./run-radar-run -i tests/radar
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Publishers
|
|
27
|
+
|
|
28
|
+
Publishers are the frameworks you can use to publish your tech radar.
|
|
29
|
+
|
|
30
|
+
As of early 2025, the publishers available are:
|
|
31
|
+
|
|
32
|
+
* Thoughtworks' Bring Your Own Radar
|
|
33
|
+
* Zalando Tech Radar
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "run-radar-run"
|
|
3
|
+
version = "0.1"
|
|
4
|
+
description = "Run, Radar, Run - Running your own tech radar should be easy"
|
|
5
|
+
authors = ["Alexei Znamensky <russoz@gmail.com>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
10
|
+
"Environment :: Console",
|
|
11
|
+
"Intended Audience :: Information Technology",
|
|
12
|
+
"Operating System :: OS Independent",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Topic :: Software Development :: Testing",
|
|
19
|
+
"Topic :: Utilities"
|
|
20
|
+
]
|
|
21
|
+
keywords = [ "tech-radar" ]
|
|
22
|
+
|
|
23
|
+
packages = [
|
|
24
|
+
{ include = "runradarrun" }
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[tool.poetry.scripts]
|
|
28
|
+
run-radar-run = "runradarrun.main:main"
|
|
29
|
+
|
|
30
|
+
[tool.poetry.dependencies]
|
|
31
|
+
python = "^3.12"
|
|
32
|
+
pyyaml = ">=5.4"
|
|
33
|
+
docker = ">=7.1.0"
|
|
34
|
+
blessed = ">=1.20.0"
|
|
35
|
+
gitpython = ">=3.1.44"
|
|
36
|
+
|
|
37
|
+
[tool.poetry.group.dev.dependencies]
|
|
38
|
+
pytest = ">=7.3.1,<9.0.0"
|
|
39
|
+
pytest-mock = "^3.10.0"
|
|
40
|
+
pylint = ">=2.17.4,<4.0.0"
|
|
41
|
+
flake8 = ">=6,<8"
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["poetry-core"]
|
|
45
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# code: language=python tabSize=4
|
|
3
|
+
#
|
|
4
|
+
import argparse
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from .model import Blip
|
|
11
|
+
from .model import Quadrant
|
|
12
|
+
from .model import Radar
|
|
13
|
+
from .model import RadarException
|
|
14
|
+
from .model import Ring
|
|
15
|
+
from .output import Printer
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Ingester:
|
|
19
|
+
def __init__(
|
|
20
|
+
self, path: Path, options: Optional[argparse.Namespace] = None
|
|
21
|
+
) -> None:
|
|
22
|
+
self.radar_path = path
|
|
23
|
+
self.options = options
|
|
24
|
+
self.printer = Printer(options.quiet)
|
|
25
|
+
|
|
26
|
+
def parse_blip(self, quadrant: Quadrant, ring: Ring, path: Path) -> Blip:
|
|
27
|
+
with open(path) as blip_file:
|
|
28
|
+
blip_spec = yaml.safe_load(blip_file)
|
|
29
|
+
|
|
30
|
+
blip = Blip(
|
|
31
|
+
quadrant=quadrant.name,
|
|
32
|
+
ring=ring.name,
|
|
33
|
+
**{k: v for k, v in blip_spec["blip"].items() if k != "is_new"},
|
|
34
|
+
)
|
|
35
|
+
if "is_new" in blip_spec["blip"]:
|
|
36
|
+
blip.previous_ring = None if blip_spec["blip"]["is_new"] else blip.ring
|
|
37
|
+
else:
|
|
38
|
+
blip.previous_ring = blip.ring
|
|
39
|
+
return blip
|
|
40
|
+
|
|
41
|
+
def ingest(self) -> Radar:
|
|
42
|
+
if (self.radar_path / "specs.yml").exists():
|
|
43
|
+
file = self.radar_path / "specs.yml"
|
|
44
|
+
else:
|
|
45
|
+
file = self.radar_path / "specs.yaml"
|
|
46
|
+
|
|
47
|
+
with open(file) as f:
|
|
48
|
+
specs = yaml.safe_load(f)
|
|
49
|
+
|
|
50
|
+
rings = {pos: Ring(**r) for pos, r in specs["rings"].items()}
|
|
51
|
+
quadrants = {pos: Quadrant(**q) for pos, q in specs["quadrants"].items()}
|
|
52
|
+
radar = Radar(rings, quadrants)
|
|
53
|
+
count = 0
|
|
54
|
+
|
|
55
|
+
for ring in rings.values():
|
|
56
|
+
for quadrant in quadrants.values():
|
|
57
|
+
blips_dir: Path = self.radar_path / quadrant.id / ring.id
|
|
58
|
+
if not blips_dir.exists():
|
|
59
|
+
continue
|
|
60
|
+
if not blips_dir.is_dir():
|
|
61
|
+
raise RadarException(f"Path {blips_dir} must be a directory")
|
|
62
|
+
|
|
63
|
+
for path in blips_dir.iterdir():
|
|
64
|
+
if path.as_posix().endswith((".yaml", ".yml")):
|
|
65
|
+
blip = self.parse_blip(quadrant, ring, path)
|
|
66
|
+
radar.add_blip(blip)
|
|
67
|
+
count = count + 1
|
|
68
|
+
|
|
69
|
+
return radar
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# code: language=python tabSize=4
|
|
4
|
+
#
|
|
5
|
+
import argparse
|
|
6
|
+
import importlib
|
|
7
|
+
import pathlib
|
|
8
|
+
import pkgutil
|
|
9
|
+
|
|
10
|
+
import runradarrun.publishers
|
|
11
|
+
from runradarrun.ingest import Ingester
|
|
12
|
+
from runradarrun.model import RadarException
|
|
13
|
+
from runradarrun.output import Printer
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def iter_namespace(ns_pkg):
|
|
17
|
+
# Specifying the second argument (prefix) to iter_modules makes the
|
|
18
|
+
# returned name an absolute name instead of a relative one. This allows
|
|
19
|
+
# import_module to work without having to do additional modification to
|
|
20
|
+
# the name.
|
|
21
|
+
return pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + ".")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_publishers():
|
|
25
|
+
return {
|
|
26
|
+
publisher.cli_id(): publisher
|
|
27
|
+
for finder, name, ispkg in iter_namespace(runradarrun.publishers)
|
|
28
|
+
for publisher in [getattr(importlib.import_module(name), "Publisher")]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main():
|
|
33
|
+
publishers = load_publishers()
|
|
34
|
+
|
|
35
|
+
parser = argparse.ArgumentParser()
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--publisher",
|
|
38
|
+
"-P",
|
|
39
|
+
default="twbyor",
|
|
40
|
+
choices=sorted(publishers.keys()),
|
|
41
|
+
help="publisher for the data radar",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--output",
|
|
45
|
+
"-o",
|
|
46
|
+
type=pathlib.Path,
|
|
47
|
+
help="output path for the radar data, depends on publisher",
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--run",
|
|
51
|
+
"-r",
|
|
52
|
+
help="run the radar opens it in browser, depends on publisher",
|
|
53
|
+
action="store_true",
|
|
54
|
+
)
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"--run-only",
|
|
57
|
+
"-R",
|
|
58
|
+
help="only run the radar, depends on publisher",
|
|
59
|
+
action="store_true",
|
|
60
|
+
)
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"--quiet",
|
|
63
|
+
"-q",
|
|
64
|
+
help="do not print output",
|
|
65
|
+
action="store_true",
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"input",
|
|
69
|
+
type=pathlib.Path,
|
|
70
|
+
nargs="?",
|
|
71
|
+
default="./radar",
|
|
72
|
+
help="radar definition directory",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
args = parser.parse_args()
|
|
77
|
+
p = Printer(args.quiet)
|
|
78
|
+
ingester = Ingester(pathlib.Path(args.input), options=args)
|
|
79
|
+
radar = ingester.ingest()
|
|
80
|
+
p.print(
|
|
81
|
+
f"{p.align_item('Radar Path')}: {p.term.bold_yellow(str(ingester.radar_path.absolute()))}"
|
|
82
|
+
)
|
|
83
|
+
p.print(
|
|
84
|
+
f"{p.align_item('Rings')}: {', '.join(p.term.bold_green(r.name) for r in radar.rings_raw.values())}"
|
|
85
|
+
)
|
|
86
|
+
p.print(
|
|
87
|
+
f"{p.align_item('Quadrants')}: {', '.join(p.term.bold_green(q.name) for q in radar.quadrants_raw.values())}"
|
|
88
|
+
)
|
|
89
|
+
p.print(
|
|
90
|
+
f"{p.align_item('Processed')}: {p.term.bold_green}{len(radar.blips):2} blips{p.term.normal + p.term.clear_eol}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if args.publisher:
|
|
94
|
+
publisher_class = publishers[args.publisher]
|
|
95
|
+
publisher = publisher_class(radar, options=args)
|
|
96
|
+
|
|
97
|
+
if args.output:
|
|
98
|
+
publisher.write(args.output)
|
|
99
|
+
|
|
100
|
+
if args.run or args.run_only:
|
|
101
|
+
try:
|
|
102
|
+
p.print(
|
|
103
|
+
f"{p.align_item('Radar URL')}: {p.term.bold_blue}{p.term.link(publisher.url, publisher.url)}{p.term.normal}"
|
|
104
|
+
)
|
|
105
|
+
publisher.run()
|
|
106
|
+
finally:
|
|
107
|
+
publisher.cleanup()
|
|
108
|
+
except KeyboardInterrupt:
|
|
109
|
+
pass
|
|
110
|
+
except RadarException as e:
|
|
111
|
+
print(f"\n{p.term.bold_red}ERROR: {e}{p.term.normal}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
if __name__ == "__main__":
|
|
115
|
+
main()
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# code: language=python tabSize=4
|
|
3
|
+
#
|
|
4
|
+
import argparse
|
|
5
|
+
import webbrowser
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict
|
|
9
|
+
from typing import List
|
|
10
|
+
from typing import Optional
|
|
11
|
+
from typing import Tuple
|
|
12
|
+
from typing import Union
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
OptionalStrOrListStr = Optional[Union[str, List[str]]]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RadarException(Exception):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Ring:
|
|
24
|
+
id: str
|
|
25
|
+
name: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class Quadrant:
|
|
30
|
+
id: str
|
|
31
|
+
name: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Blip:
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
name: str,
|
|
38
|
+
ring: str,
|
|
39
|
+
quadrant: str,
|
|
40
|
+
previous_ring: Optional[str] = None,
|
|
41
|
+
description: OptionalStrOrListStr = None,
|
|
42
|
+
references: OptionalStrOrListStr = None,
|
|
43
|
+
tags: OptionalStrOrListStr = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
self.name = name
|
|
46
|
+
self.ring = ring
|
|
47
|
+
self.quadrant = quadrant
|
|
48
|
+
self.previous_ring = previous_ring
|
|
49
|
+
self.description = description or []
|
|
50
|
+
self.references = references or []
|
|
51
|
+
self.tags = tags or []
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def is_new(self) -> bool:
|
|
55
|
+
return self.previous_ring is None
|
|
56
|
+
|
|
57
|
+
def __str__(self) -> str:
|
|
58
|
+
return f"Blip({self.name}, r={self.ring}, q={self.quadrant}{' NEW' if self.is_new else ''})"
|
|
59
|
+
|
|
60
|
+
__repr__ = __str__
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Radar:
|
|
64
|
+
MIN_NUM_RINGS = 3
|
|
65
|
+
MAX_NUM_RINGS = 4
|
|
66
|
+
|
|
67
|
+
Q_TL = "top_left"
|
|
68
|
+
Q_TR = "top_right"
|
|
69
|
+
Q_BL = "bottom_left"
|
|
70
|
+
Q_BR = "bottom_right"
|
|
71
|
+
|
|
72
|
+
QUADRANTS_CLOCKWISE = (Q_TL, Q_TR, Q_BR, Q_BL)
|
|
73
|
+
QUADRANTS_CTR_CLOCKWISE = (Q_TL, Q_BL, Q_BR, Q_TR)
|
|
74
|
+
QUADRANTS_TL_BL_TR_BR = (Q_TL, Q_BL, Q_TR, Q_BR)
|
|
75
|
+
QUADRANTS_TL_TR_BL_BR = (Q_TL, Q_TR, Q_BL, Q_BR)
|
|
76
|
+
|
|
77
|
+
def __init__(self, rings: Dict[str, Ring], quadrants: Dict[str, Quadrant]) -> None:
|
|
78
|
+
if Radar.MIN_NUM_RINGS <= len(rings) <= Radar.MAX_NUM_RINGS:
|
|
79
|
+
self._rings = rings
|
|
80
|
+
else:
|
|
81
|
+
raise RadarException(
|
|
82
|
+
f"Radar must have between {Radar.MIN_NUM_RINGS} and {Radar.MAX_NUM_RINGS}: {rings}"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if len(quadrants) != 4:
|
|
86
|
+
raise RadarException(f"Radar must have 4 quadrants: {quadrants}")
|
|
87
|
+
self._quadrants = quadrants
|
|
88
|
+
self._blips = []
|
|
89
|
+
|
|
90
|
+
def add_blip(self, blip: Blip) -> None:
|
|
91
|
+
self._blips.append(blip)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def blips(self) -> Tuple[Blip]:
|
|
95
|
+
return tuple(self._blips)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def rings_raw(self):
|
|
99
|
+
return self._rings
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def quadrants_raw(self):
|
|
103
|
+
return self._quadrants
|
|
104
|
+
|
|
105
|
+
def rings_outward(self) -> List[Ring]:
|
|
106
|
+
return [
|
|
107
|
+
self._rings[ring]
|
|
108
|
+
for ring in ["inner", "mid_inner", "mid_outer", "outer"]
|
|
109
|
+
if self._rings.get(ring)
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
def rings_inward(self) -> List[Ring]:
|
|
113
|
+
return [
|
|
114
|
+
self._rings[ring]
|
|
115
|
+
for ring in ["outer", "mid_outer", "mid_inner", "inner"]
|
|
116
|
+
if self._rings.get(ring)
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
def quadrants(self, order: Tuple) -> List[Quadrant]:
|
|
120
|
+
return [self._quadrants[q] for q in order]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class AbstractPublisher:
|
|
124
|
+
publishing_url = None
|
|
125
|
+
|
|
126
|
+
def __init__(
|
|
127
|
+
self, radar: Radar, options: Optional[argparse.Namespace] = None
|
|
128
|
+
) -> None:
|
|
129
|
+
self.radar = radar
|
|
130
|
+
self.options = options
|
|
131
|
+
self._output = None
|
|
132
|
+
|
|
133
|
+
def make_output(self) -> str:
|
|
134
|
+
raise NotImplementedError()
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def cli_id(cls):
|
|
138
|
+
raise NotImplementedError()
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def output(self) -> str:
|
|
142
|
+
if self._output is None:
|
|
143
|
+
self._output = self.make_output()
|
|
144
|
+
return self._output
|
|
145
|
+
|
|
146
|
+
def run(self):
|
|
147
|
+
# optional implementation
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
def cleanup(self):
|
|
151
|
+
# optional implementation
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def url(self) -> str:
|
|
156
|
+
if self.publishing_url is None:
|
|
157
|
+
raise NotImplementedError()
|
|
158
|
+
return self.publishing_url
|
|
159
|
+
|
|
160
|
+
def write(self, output: Path) -> None:
|
|
161
|
+
with open(output, "w") as outputfile:
|
|
162
|
+
outputfile.write(self.output)
|
|
163
|
+
|
|
164
|
+
def open_url(self, url: Optional[str] = None) -> None:
|
|
165
|
+
if not url:
|
|
166
|
+
url = self.url
|
|
167
|
+
if not self.options.run_only:
|
|
168
|
+
webbrowser.open(url, new=1)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# code: language=python tabSize=4
|
|
3
|
+
#
|
|
4
|
+
from blessed import Terminal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Printer:
|
|
8
|
+
def __init__(self, quiet) -> None:
|
|
9
|
+
self.quiet = quiet
|
|
10
|
+
self._term = None
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def term(self) -> Terminal:
|
|
14
|
+
if not self._term:
|
|
15
|
+
self._term = Terminal()
|
|
16
|
+
return self._term
|
|
17
|
+
|
|
18
|
+
def print(self, *args, **kwargs) -> None:
|
|
19
|
+
if not self.quiet:
|
|
20
|
+
print(*args, **kwargs)
|
|
21
|
+
|
|
22
|
+
def align_item(self, item: str) -> str:
|
|
23
|
+
return self.term.rjust(item, 12)
|
|
24
|
+
|
|
25
|
+
def logger(self, stream, log_height, trigger=None):
|
|
26
|
+
try:
|
|
27
|
+
self.print(
|
|
28
|
+
self.term.hide_cursor
|
|
29
|
+
+ "\n" * (log_height + 1)
|
|
30
|
+
+ self.term.move_up(log_height)
|
|
31
|
+
+ self.term.grey,
|
|
32
|
+
end="",
|
|
33
|
+
)
|
|
34
|
+
start_row, _ = self.term.get_location()
|
|
35
|
+
|
|
36
|
+
log_lines = []
|
|
37
|
+
for line in stream:
|
|
38
|
+
line = line.decode("utf-8").strip()
|
|
39
|
+
for wrapped_line in self.term.wrap(line) or [""]:
|
|
40
|
+
log_lines.append(wrapped_line)
|
|
41
|
+
|
|
42
|
+
log_lines = log_lines[-log_height:]
|
|
43
|
+
log_lines[-1] = log_lines[-1] + self.term.clear_eol + "\n"
|
|
44
|
+
with self.term.location(0, start_row):
|
|
45
|
+
for log_line in log_lines:
|
|
46
|
+
self.print(f"{log_line}", end="", flush=True)
|
|
47
|
+
|
|
48
|
+
if trigger:
|
|
49
|
+
trigger(line)
|
|
50
|
+
finally:
|
|
51
|
+
print(
|
|
52
|
+
f"{self.term.normal}{self.term.normal_cursor}{self.term.move_up}{self.term.clear_eos}"
|
|
53
|
+
)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# code: language=python tabSize=4
|
|
3
|
+
#
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from tempfile import TemporaryDirectory
|
|
8
|
+
|
|
9
|
+
import docker
|
|
10
|
+
|
|
11
|
+
from ..model import AbstractPublisher
|
|
12
|
+
from ..output import Printer
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Publisher(AbstractPublisher):
|
|
16
|
+
container_image = "wwwthoughtworks/build-your-own-radar:latest"
|
|
17
|
+
container = None
|
|
18
|
+
publishing_url = "http://localhost:8080/"
|
|
19
|
+
run_output_file = "run-radar-run.json"
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def cli_id(cls):
|
|
23
|
+
return "twbyor"
|
|
24
|
+
|
|
25
|
+
def make_output(self):
|
|
26
|
+
results = []
|
|
27
|
+
for blip in self.radar.blips:
|
|
28
|
+
results.append(
|
|
29
|
+
dict(
|
|
30
|
+
name=blip.name,
|
|
31
|
+
ring=blip.ring,
|
|
32
|
+
quadrant=blip.quadrant,
|
|
33
|
+
isNew="TRUE" if blip.is_new else "FALSE",
|
|
34
|
+
description=blip.description,
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return json.dumps(results)
|
|
39
|
+
|
|
40
|
+
def run(self):
|
|
41
|
+
with TemporaryDirectory(dir=".", prefix=".runradarrun-") as temp_dir:
|
|
42
|
+
os.chmod(temp_dir, 0o755)
|
|
43
|
+
temp_output = Path(temp_dir) / self.run_output_file
|
|
44
|
+
self.write(temp_output)
|
|
45
|
+
|
|
46
|
+
rd = self.radar
|
|
47
|
+
client = docker.from_env()
|
|
48
|
+
self.container = client.containers.run(
|
|
49
|
+
self.container_image,
|
|
50
|
+
auto_remove=True,
|
|
51
|
+
ports={"80/tcp": 8080},
|
|
52
|
+
environment={
|
|
53
|
+
"SERVER_NAMES": "localhost 127.0.0.1",
|
|
54
|
+
"QUADRANTS": json.dumps(
|
|
55
|
+
[q.name for q in rd.quadrants(rd.QUADRANTS_TL_BL_TR_BR)]
|
|
56
|
+
),
|
|
57
|
+
"RINGS": json.dumps([r.name for r in rd.rings_outward()]),
|
|
58
|
+
},
|
|
59
|
+
volumes={
|
|
60
|
+
temp_dir: {"bind": "/opt/build-your-own-radar/files", "mode": "rw"}
|
|
61
|
+
},
|
|
62
|
+
detach=True,
|
|
63
|
+
stream=True,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
p = Printer(self.options.quiet)
|
|
67
|
+
|
|
68
|
+
def trigger_browser(line):
|
|
69
|
+
if "Starting nginx server..." in line:
|
|
70
|
+
self.open_url()
|
|
71
|
+
|
|
72
|
+
p.logger(
|
|
73
|
+
stream=self.container.logs(stream=True),
|
|
74
|
+
log_height=10,
|
|
75
|
+
trigger=trigger_browser,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def cleanup(self):
|
|
79
|
+
if self.container:
|
|
80
|
+
self.container.stop()
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def url(self):
|
|
84
|
+
return (
|
|
85
|
+
f"{self.publishing_url}"
|
|
86
|
+
"?documentId=http%3A%2F%2Flocalhost%3A8080%2Ffiles%2F"
|
|
87
|
+
f"{self.run_output_file}"
|
|
88
|
+
)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# code: language=python tabSize=4
|
|
3
|
+
#
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from tempfile import TemporaryDirectory
|
|
8
|
+
|
|
9
|
+
import docker
|
|
10
|
+
from git import Repo
|
|
11
|
+
|
|
12
|
+
from ..model import AbstractPublisher
|
|
13
|
+
from ..output import Printer
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
html_content = """\
|
|
17
|
+
<!DOCTYPE html>
|
|
18
|
+
<html lang="en">
|
|
19
|
+
|
|
20
|
+
<head>
|
|
21
|
+
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
|
|
22
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
23
|
+
<title>Zalando Tech Radar</title>
|
|
24
|
+
<link rel="shortcut icon" href="https://www.zalando.de/favicon.ico">
|
|
25
|
+
|
|
26
|
+
<script src="https://d3js.org/d3.v4.min.js"></script>
|
|
27
|
+
<script src="radar.js"></script>
|
|
28
|
+
|
|
29
|
+
<link rel="stylesheet" href="radar.css">
|
|
30
|
+
</head>
|
|
31
|
+
|
|
32
|
+
<body>
|
|
33
|
+
|
|
34
|
+
<svg id="radar"></svg>
|
|
35
|
+
|
|
36
|
+
<script>
|
|
37
|
+
fetch('./config.json').then(function(response) {
|
|
38
|
+
return response.json();
|
|
39
|
+
}).then(function(data) {
|
|
40
|
+
radar_visualization(data);
|
|
41
|
+
}).catch(function(err) {
|
|
42
|
+
console.log('Error loading config.json', err);
|
|
43
|
+
});
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
</body>
|
|
47
|
+
</html>
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Publisher(AbstractPublisher):
|
|
52
|
+
zalando_git_url = "https://github.com/zalando/tech-radar.git"
|
|
53
|
+
container_image = "gcriocloudbuilders/yarn"
|
|
54
|
+
container = None
|
|
55
|
+
publishing_url = "http://localhost:8080/"
|
|
56
|
+
ring_colors = ["#5ba300", "#009eb0", "#c7ba00", "#e09b96"]
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def quadrant_order(self):
|
|
60
|
+
rd = self.radar
|
|
61
|
+
return (rd.Q_BR, rd.Q_BL, rd.Q_TL, rd.Q_TR)
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def cli_id(cls):
|
|
65
|
+
return "zalando"
|
|
66
|
+
|
|
67
|
+
def make_output(self):
|
|
68
|
+
q_names = [q.name for q in self.radar.quadrants(self.quadrant_order)]
|
|
69
|
+
r_names = [r.name for r in self.radar.rings_outward()]
|
|
70
|
+
|
|
71
|
+
entries = [
|
|
72
|
+
dict(
|
|
73
|
+
quadrant=q_names.index(blip.quadrant),
|
|
74
|
+
ring=r_names.index(blip.ring),
|
|
75
|
+
label=blip.name,
|
|
76
|
+
active=True,
|
|
77
|
+
moved=(blip.ring != blip.previous_ring),
|
|
78
|
+
)
|
|
79
|
+
for blip in self.radar.blips
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
output = dict(
|
|
83
|
+
repo_url="https://github.com/zalando/tech-radar",
|
|
84
|
+
title="Zalando Tech Radar",
|
|
85
|
+
date="2025.01", # kind of a version
|
|
86
|
+
quadrants=[
|
|
87
|
+
{"name": q.name} for q in self.radar.quadrants(self.quadrant_order)
|
|
88
|
+
],
|
|
89
|
+
rings=[
|
|
90
|
+
dict(
|
|
91
|
+
name=r.name.upper(),
|
|
92
|
+
color=c,
|
|
93
|
+
)
|
|
94
|
+
for r, c in zip(self.radar.rings_outward(), self.ring_colors)
|
|
95
|
+
],
|
|
96
|
+
entries=entries,
|
|
97
|
+
)
|
|
98
|
+
return json.dumps(output)
|
|
99
|
+
|
|
100
|
+
def run(self):
|
|
101
|
+
with TemporaryDirectory(dir=".", prefix=".runradarrun-") as temp_dir:
|
|
102
|
+
os.chmod(temp_dir, 0o755)
|
|
103
|
+
|
|
104
|
+
repo_dir = Path(temp_dir) / "zalando"
|
|
105
|
+
Repo.clone_from(self.zalando_git_url, repo_dir, depth=1)
|
|
106
|
+
|
|
107
|
+
temp_output = repo_dir / "docs" / "config.json"
|
|
108
|
+
self.write(temp_output)
|
|
109
|
+
|
|
110
|
+
with open(repo_dir / "docs" / "index.html", "w") as index:
|
|
111
|
+
index.write(html_content)
|
|
112
|
+
|
|
113
|
+
p = Printer(self.options.quiet)
|
|
114
|
+
|
|
115
|
+
client = docker.from_env()
|
|
116
|
+
# Run bare yarn to install packages
|
|
117
|
+
self.container = client.containers.run(
|
|
118
|
+
self.container_image,
|
|
119
|
+
auto_remove=True,
|
|
120
|
+
volumes={repo_dir.as_posix(): {"bind": "/app", "mode": "rw"}},
|
|
121
|
+
working_dir="/app",
|
|
122
|
+
detach=True,
|
|
123
|
+
stream=True,
|
|
124
|
+
user=os.getuid(),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
p.logger(
|
|
128
|
+
stream=self.container.logs(stream=True),
|
|
129
|
+
log_height=10,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Run radar
|
|
133
|
+
self.container = client.containers.run(
|
|
134
|
+
self.container_image,
|
|
135
|
+
auto_remove=True,
|
|
136
|
+
ports={"3000/tcp": 8080},
|
|
137
|
+
volumes={repo_dir.as_posix(): {"bind": "/app", "mode": "rw"}},
|
|
138
|
+
working_dir="/app",
|
|
139
|
+
detach=True,
|
|
140
|
+
stream=True,
|
|
141
|
+
command=["start", "--no-open"],
|
|
142
|
+
user=os.getuid(),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def trigger_browser(line):
|
|
146
|
+
if "Watching files..." in line:
|
|
147
|
+
self.open_url()
|
|
148
|
+
|
|
149
|
+
p.logger(
|
|
150
|
+
stream=self.container.logs(stream=True),
|
|
151
|
+
log_height=10,
|
|
152
|
+
trigger=trigger_browser,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def cleanup(self):
|
|
156
|
+
if self.container:
|
|
157
|
+
self.container.stop()
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def url(self):
|
|
161
|
+
return self.publishing_url
|