]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
1 | # cobbled together from: |
2 | # https://github.com/sphinx-contrib/documentedlist/blob/master/sphinxcontrib/documentedlist.py | |
3 | # https://github.com/sphinx-doc/sphinx/blob/v1.6.3/sphinx/ext/graphviz.py | |
4 | # https://github.com/thewtex/sphinx-contrib/blob/master/exceltable/sphinxcontrib/exceltable.py | |
5 | # https://bitbucket.org/prometheus/sphinxcontrib-htsql/src/331a542c29a102eec9f8cba44797e53a49de2a49/sphinxcontrib/htsql.py?at=default&fileviewer=file-view-default | |
6 | # into the glory that follows: | |
7 | import json | |
11fdf7f2 | 8 | import yaml |
1e59de90 | 9 | import jinja2 |
11fdf7f2 TL |
10 | import sphinx |
11 | import datetime | |
12 | from docutils.parsers.rst import Directive | |
13 | from docutils import nodes | |
14 | from sphinx.util import logging | |
15 | ||
1e59de90 TL |
16 | logger = logging.getLogger(__name__) |
17 | ||
18 | ||
11fdf7f2 TL |
19 | class CephReleases(Directive): |
20 | has_content = False | |
20effc67 | 21 | required_arguments = 2 |
11fdf7f2 TL |
22 | optional_arguments = 0 |
23 | option_spec = {} | |
24 | ||
25 | def run(self): | |
26 | filename = self.arguments[0] | |
20effc67 | 27 | current = self.arguments[1] == 'current' |
11fdf7f2 TL |
28 | document = self.state.document |
29 | env = document.settings.env | |
30 | rel_filename, filename = env.relfn2path(filename) | |
31 | env.note_dependency(filename) | |
32 | try: | |
33 | with open(filename, 'r') as fp: | |
9f95a23c | 34 | releases = yaml.safe_load(fp) |
11fdf7f2 TL |
35 | releases = releases["releases"] |
36 | except Exception as e: | |
37 | return [document.reporter.warning( | |
38 | "Failed to open Ceph releases file {}: {}".format(filename, e), | |
39 | line=self.lineno)] | |
40 | ||
41 | table = nodes.table() | |
42 | tgroup = nodes.tgroup(cols=3) | |
43 | table += tgroup | |
44 | ||
45 | tgroup.extend( | |
46 | nodes.colspec(colwidth=30, colname='c'+str(idx)) | |
47 | for idx, _ in enumerate(range(4))) | |
48 | ||
49 | thead = nodes.thead() | |
50 | tgroup += thead | |
51 | row_node = nodes.row() | |
52 | thead += row_node | |
20effc67 TL |
53 | row_node.extend( |
54 | nodes.entry(h, nodes.paragraph(text=h)) | |
55 | for h in ["Name", "Initial release", "Latest", | |
56 | "End of life (estimated)" if current else "End of life"]) | |
11fdf7f2 | 57 | |
f67539c2 | 58 | releases = releases.items() |
11fdf7f2 TL |
59 | releases = sorted(releases, key=lambda t: t[0], reverse=True) |
60 | ||
61 | tbody = nodes.tbody() | |
62 | tgroup += tbody | |
63 | ||
64 | rows = [] | |
65 | for code_name, info in releases: | |
66 | actual_eol = info.get("actual_eol", None) | |
20effc67 TL |
67 | |
68 | if current: | |
69 | if actual_eol and actual_eol <= datetime.datetime.now().date(): | |
70 | continue | |
71 | else: | |
72 | if not actual_eol: | |
73 | continue | |
74 | ||
11fdf7f2 TL |
75 | trow = nodes.row() |
76 | ||
77 | entry = nodes.entry() | |
20effc67 | 78 | para = nodes.paragraph(text=f"`{code_name.title()} <{code_name}>`_") |
11fdf7f2 TL |
79 | sphinx.util.nodes.nested_parse_with_titles( |
80 | self.state, para, entry) | |
81 | #entry += para | |
82 | trow += entry | |
83 | ||
84 | sorted_releases = sorted(info["releases"], | |
85 | key=lambda t: [t["released"]] + list(map(lambda v: int(v), t["version"].split(".")))) | |
86 | oldest_release = sorted_releases[0] | |
87 | newest_release = sorted_releases[-1] | |
88 | ||
89 | entry = nodes.entry() | |
90 | para = nodes.paragraph(text="{}".format( | |
20effc67 | 91 | oldest_release["released"])) |
11fdf7f2 TL |
92 | entry += para |
93 | trow += entry | |
94 | ||
95 | entry = nodes.entry() | |
96 | if newest_release.get("skip_ref", False): | |
97 | para = nodes.paragraph(text="{}".format( | |
98 | newest_release["version"])) | |
99 | else: | |
100 | para = nodes.paragraph(text="`{}`_".format( | |
101 | newest_release["version"])) | |
102 | sphinx.util.nodes.nested_parse_with_titles( | |
103 | self.state, para, entry) | |
104 | #entry += para | |
105 | trow += entry | |
106 | ||
107 | entry = nodes.entry() | |
20effc67 TL |
108 | if current: |
109 | para = nodes.paragraph(text=info.get("target_eol", '--')) | |
110 | else: | |
111 | para = nodes.paragraph(text=info.get('actual_eol', '--')) | |
11fdf7f2 TL |
112 | entry += para |
113 | trow += entry | |
114 | ||
115 | rows.append(trow) | |
116 | ||
117 | tbody.extend(rows) | |
118 | ||
119 | return [table] | |
120 | ||
1e59de90 TL |
121 | |
122 | RELEASES_TEMPLATE = ''' | |
123 | .. mermaid:: | |
124 | ||
125 | gantt | |
126 | dateFormat YYYY-MM-DD | |
127 | axisFormat %Y | |
128 | section Active Releases | |
129 | {% for release in active_releases %} | |
130 | {{ release.code_name }} (latest {{ release.last_version }}): done, {{ release.debute_date }},{{ release.lifetime }}d | |
131 | {% endfor %} | |
132 | section Archived Releases | |
133 | {% for release in archived_releases %} | |
134 | {{ release.code_name }} (latest {{ release.last_version }}): done, {{ release.debute_date }},{{ release.lifetime }}d | |
135 | {% endfor %} | |
136 | ''' | |
137 | ||
138 | ||
139 | class ReleasesGantt(Directive): | |
140 | has_content = True | |
141 | required_arguments = 1 | |
142 | optional_arguments = 0 | |
143 | final_argument_whitespace = False | |
144 | ||
145 | template = jinja2.Environment().from_string(RELEASES_TEMPLATE) | |
146 | ||
147 | def _render_time_line(self, filename): | |
148 | try: | |
149 | with open(filename) as f: | |
150 | releases = yaml.safe_load(f)['releases'] | |
151 | except Exception as e: | |
152 | message = f'Unable read release file: "{filename}": {e}' | |
153 | self.error(message) | |
154 | ||
155 | active_releases = [] | |
156 | archived_releases = [] | |
157 | # just update `releases` with extracted info | |
158 | for code_name, info in releases.items(): | |
159 | last_release = info['releases'][0] | |
160 | first_release = info['releases'][-1] | |
161 | last_version = last_release['version'] | |
162 | debute_date = first_release['released'] | |
163 | if 'actual_eol' in info: | |
164 | lifetime = info['actual_eol'] - first_release['released'] | |
165 | else: | |
166 | lifetime = info['target_eol'] - first_release['released'] | |
167 | release = dict(code_name=code_name, | |
168 | last_version=last_version, | |
169 | debute_date=debute_date, | |
170 | lifetime=lifetime.days) | |
171 | if 'actual_eol' in info: | |
172 | archived_releases.append(release) | |
173 | else: | |
174 | active_releases.append(release) | |
175 | rendered = self.template.render(active_releases=active_releases, | |
176 | archived_releases=archived_releases) | |
177 | return rendered.splitlines() | |
178 | ||
179 | def run(self): | |
180 | filename = self.arguments[0] | |
181 | document = self.state.document | |
182 | env = document.settings.env | |
183 | rel_filename, filename = env.relfn2path(filename) | |
184 | env.note_dependency(filename) | |
185 | lines = self._render_time_line(filename) | |
186 | lineno = self.lineno - self.state_machine.input_offset - 1 | |
187 | source = self.state_machine.input_lines.source(lineno) | |
188 | self.state_machine.insert_input(lines, source) | |
189 | return [] | |
190 | ||
191 | ||
11fdf7f2 TL |
192 | class CephTimeline(Directive): |
193 | has_content = False | |
1e59de90 | 194 | required_arguments = 3 |
11fdf7f2 TL |
195 | optional_arguments = 0 |
196 | option_spec = {} | |
197 | ||
198 | def run(self): | |
199 | filename = self.arguments[0] | |
200 | document = self.state.document | |
201 | env = document.settings.env | |
202 | rel_filename, filename = env.relfn2path(filename) | |
203 | env.note_dependency(filename) | |
204 | try: | |
205 | with open(filename, 'r') as fp: | |
9f95a23c | 206 | releases = yaml.safe_load(fp) |
11fdf7f2 TL |
207 | except Exception as e: |
208 | return [document.reporter.warning( | |
209 | "Failed to open Ceph releases file {}: {}".format(filename, e), | |
210 | line=self.lineno)] | |
211 | ||
212 | display_releases = self.arguments[1:] | |
213 | ||
214 | timeline = [] | |
f67539c2 | 215 | for code_name, info in releases["releases"].items(): |
11fdf7f2 TL |
216 | if code_name in display_releases: |
217 | for release in info.get("releases", []): | |
218 | released = release["released"] | |
219 | timeline.append((released, code_name, release["version"], | |
220 | release.get("skip_ref", False))) | |
221 | ||
222 | assert "development" not in releases["releases"] | |
223 | if "development" in display_releases: | |
224 | for release in releases["development"]["releases"]: | |
225 | released = release["released"] | |
226 | timeline.append((released, "development", release["version"], | |
227 | release.get("skip_ref", False))) | |
228 | ||
229 | timeline = sorted(timeline, key=lambda t: t[0], reverse=True) | |
230 | ||
231 | table = nodes.table() | |
232 | tgroup = nodes.tgroup(cols=3) | |
233 | table += tgroup | |
234 | ||
235 | columns = ["Date"] + display_releases | |
236 | tgroup.extend( | |
237 | nodes.colspec(colwidth=30, colname='c'+str(idx)) | |
238 | for idx, _ in enumerate(range(len(columns)))) | |
239 | ||
240 | thead = nodes.thead() | |
241 | tgroup += thead | |
242 | row_node = nodes.row() | |
243 | thead += row_node | |
244 | for col in columns: | |
245 | entry = nodes.entry() | |
246 | if col.lower() in ["date", "development"]: | |
20effc67 | 247 | para = nodes.paragraph(text=col.title()) |
11fdf7f2 | 248 | else: |
20effc67 | 249 | para = nodes.paragraph(text=f"`{col.title()} <{col}>`_".format(col)) |
11fdf7f2 TL |
250 | sphinx.util.nodes.nested_parse_with_titles( |
251 | self.state, para, entry) | |
252 | row_node += entry | |
253 | ||
254 | tbody = nodes.tbody() | |
255 | tgroup += tbody | |
256 | ||
257 | rows = [] | |
258 | for row_info in timeline: | |
259 | trow = nodes.row() | |
260 | ||
261 | entry = nodes.entry() | |
20effc67 | 262 | para = nodes.paragraph(text=row_info[0]) |
11fdf7f2 TL |
263 | entry += para |
264 | trow += entry | |
265 | ||
266 | for release in display_releases: | |
267 | entry = nodes.entry() | |
268 | if row_info[1] == release: | |
269 | if row_info[3]: # if skip ref | |
270 | para = nodes.paragraph(text=row_info[2]) | |
271 | else: | |
272 | para = nodes.paragraph(text="`{}`_".format(row_info[2])) | |
273 | sphinx.util.nodes.nested_parse_with_titles( | |
274 | self.state, para, entry) | |
275 | else: | |
276 | para = nodes.paragraph(text="--") | |
277 | entry += para | |
278 | trow += entry | |
279 | rows.append(trow) | |
280 | ||
281 | tbody.extend(rows) | |
282 | ||
283 | return [table] | |
284 | ||
1e59de90 TL |
285 | |
286 | TIMELINE_TEMPLATE = ''' | |
287 | .. mermaid:: | |
288 | ||
289 | gantt | |
290 | dateFormat YYYY-MM-DD | |
291 | axisFormat %Y-%m | |
292 | {% if title %} | |
293 | title {{title}} | |
294 | {% endif %} | |
295 | {% for display_release in display_releases %} | |
296 | section {{ display_release }} | |
297 | {%if releases[display_release].actual_eol %} | |
298 | End of life: crit, {{ releases[display_release].actual_eol }},4d | |
299 | {% else %} | |
300 | End of life (estimated): crit, {{ releases[display_release].target_eol }},4d | |
301 | {% endif %} | |
302 | {% for release in releases[display_release].releases | sort(attribute='released', reverse=True) %} | |
303 | {{ release.version }}: milestone, done, {{ release.released }},0d | |
304 | {% endfor %} | |
305 | {% endfor %} | |
306 | ''' | |
307 | ||
308 | ||
309 | class TimeLineGantt(Directive): | |
310 | has_content = True | |
311 | required_arguments = 2 | |
312 | optional_arguments = 0 | |
313 | final_argument_whitespace = True | |
314 | ||
315 | template = jinja2.Environment().from_string(TIMELINE_TEMPLATE) | |
316 | ||
317 | def _render_time_line(self, filename, display_releases): | |
318 | try: | |
319 | with open(filename) as f: | |
320 | releases = yaml.safe_load(f)['releases'] | |
321 | except Exception as e: | |
322 | message = f'Unable read release file: "{filename}": {e}' | |
323 | self.error(message) | |
324 | ||
325 | rendered = self.template.render(display_releases=display_releases, | |
326 | releases=releases) | |
327 | return rendered.splitlines() | |
328 | ||
329 | def run(self): | |
330 | filename = self.arguments[0] | |
331 | display_releases = self.arguments[1].split() | |
332 | document = self.state.document | |
333 | env = document.settings.env | |
334 | rel_filename, filename = env.relfn2path(filename) | |
335 | env.note_dependency(filename) | |
336 | lines = self._render_time_line(filename, display_releases) | |
337 | lineno = self.lineno - self.state_machine.input_offset - 1 | |
338 | source = self.state_machine.input_lines.source(lineno) | |
339 | self.state_machine.insert_input(lines, source) | |
340 | return [] | |
341 | ||
342 | ||
11fdf7f2 TL |
343 | def setup(app): |
344 | app.add_directive('ceph_releases', CephReleases) | |
1e59de90 | 345 | app.add_directive('ceph_releases_gantt', ReleasesGantt) |
11fdf7f2 | 346 | app.add_directive('ceph_timeline', CephTimeline) |
1e59de90 | 347 | app.add_directive('ceph_timeline_gantt', TimeLineGantt) |
20effc67 TL |
348 | return { |
349 | 'parallel_read_safe': True, | |
350 | 'parallel_write_safe': True | |
351 | } |