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