Coverage for lib/lib_github.py: 6%

1471 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2026-02-18 02:40 +0100

1__author__ = 'moilerat' 

2 

3# --job=git.user -v --with_comment --check_issue_re --list_repos=fotonower/projects 

4 

5 

6import sys 

7 

8def print_mem(): 

9 import logging 

10 logger = logging.getLogger() 

11 local_vars = list(locals().items()) 

12 for var, obj in local_vars: 

13 print(var, sys.getsizeof(obj)) 

14 logger.warning(" var : " + str(sys.getsizeof(obj))) 

15 

16 

17import datetime 

18import sys 

19 

20# TODO move mainly to dev_test ou alpha 

21 

22# pip3 install PyGithub d'après https://github.com/FlorianMarckmann/Test_stage_ftn/ 

23from github import Github 

24import json 

25 

26## Usage 

27 

28## Append git. to these job for use from prompt 

29 

30# VR 25-12-21 : excluding issues with label 

31# --job=count --excluded_labels="project:rubbia,bug,enhancement,business,client,documentation,api,project:cod,raspi,it_maintenance,reporting work,model:supervised:classification,ready_for_qa,project:qualipapia,refacto,rh,project:broca,compte_rendu,project:datou,model:gan,model:unsupervised:vse,project:datou_loco,model:statistics,recherche,duplicate" 

32# --nb_days_tolerance=0 --excluded_labels="project:rubbia,bug,enhancement,business,client,documentation,api,project:cod,raspi,it_maintenance,reporting work,model:supervised:classification,ready_for_qa,project:qualipapia,refacto,rh,project:broca,compte_rendu,project:datou,model:gan,model:unsupervised:vse,project:datou_loco,model:statistics,recherche,duplicate" 

33 

34 

35# --repos=fotonower/API,fotonower/raspi-fotonower-x,fotonower/Metolour,fotonower/Velours,fotonower/projects,fotonower/Admin 

36 

37# Just for one repo 

38# -v --repos=fotonower/Metolour 

39 

40# Check reporting automatically 

41# --job=report 

42 

43# Job normal 

44# --nb_days_tolerance=20 

45 

46# List of all projects 

47# project:cod,project:adenes,project:broca,project:circpack,project:qualipapia,project:mpa,project:datou,documentation,bug,it_maintenance,reporting work,business,commercial,recherche,rh,refacto 

48 

49 

50# <!DOCTYPE NETSCAPE-Bookmark-file-1> 

51# <!-- This is an automatically generated file. 

52# It will be read and overwritten. 

53# DO NOT EDIT! --> 

54# <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"> 

55# <TITLE>Bookmarks</TITLE> 

56# <H1>Bookmarks</H1> 

57# <DL><p> 

58# <DT><H3 ADD_DATE="1368544705" LAST_MODIFIED="1644685392" PERSONAL_TOOLBAR_FOLDER="true">Barre de favoris</H3> 

59# <DL><p> 

60# <DT><H3 ADD_DATE="1509912547" LAST_MODIFIED="1644103407">perso</H3> 

61# <DL><p> 

62# <DT><H3 ADD_DATE="1580984692" LAST_MODIFIED="1644685336">louve</H3> 

63# <DL><p> 

64# <DT><A HREF="https://wiki.supermarches-cooperatifs.fr/doku.php?id=thematiques:informatique" ADD_DATE="1580984683" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACKElEQVQ4jW2SO48dRRCFv1PdM3P37rXXFjL7HwiRkACRkRAS8ovgJ5CSgwh4SIBIIOBN4MAPJJCQsAUBIJ7W4pmqQzD33sUSFXWru07Vd6p0enp6frLbPdUAeodlgdbE/0RmurWmrKrm6A+Su32z2z3dW38dW4Fc0SVAEpLwIdtGIQREtATGbfBK74AgDC0k97GzLIuWeSZzARsb+jAwbTYGmOdZ0lqnL60xCrvsiObMJDM5e+wGZ4+fIwWS+PXH+/z12y8exwkJS3JI7j0TWkcS8zKDxHxxwbMvvsQTzzzHvW/vMk4nfP7Om9z54ieiDdgAVtl0tybbkiTbK7etabvlqw/e5f3XXmV3dp2qYhgnqorVIhGSYn/jkCxWU+wil5l8+JBl/gc798au/w/RD4eDCHvmWhaefP4Frt04p48jX773Fj98c4thnI7FHhG4DINBEfx8/x7f3fyaabvlwR+/E9H2/JdxFLhEWDuJaHx/6yafvf0Gp9eu04eR1jsY+A9CHIoeEMwqVDZ9mtheucr2ytV9srHWYscOlGm3tmau+thFH0daNKoKl49w8n63bMp2X1rTKKmqGIaBqlIfRm5/8hEXf/9JHyfKxR5erTcqE9ZFUldm0YcF1CQ8DB2M7nz6MYpg2OxnLxjb4AiRuRRGZWdX1Yg9rQ9JZoHMsJmOhq7LYbIWsgRob7hPembejl4vA4SCqiQiVlAenVlVERGUq1yaKD78FyIYKx9nIeZ/AAAAAElFTkSuQmCC">TEST thematiques:informatique [Supermarchés Coopératifs : le wiki]</A> 

65# <DT><A HREF="https://forum.supermarches-cooperatifs.fr/t/le-choix-de-loutil-de-gestion-des-cooperatives/1287/8" ADD_DATE="1580984695" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACKElEQVQ4jW2SO48dRRCFv1PdM3P37rXXFjL7HwiRkACRkRAS8ovgJ5CSgwh4SIBIIOBN4MAPJJCQsAUBIJ7W4pmqQzD33sUSFXWru07Vd6p0enp6frLbPdUAeodlgdbE/0RmurWmrKrm6A+Su32z2z3dW38dW4Fc0SVAEpLwIdtGIQREtATGbfBK74AgDC0k97GzLIuWeSZzARsb+jAwbTYGmOdZ0lqnL60xCrvsiObMJDM5e+wGZ4+fIwWS+PXH+/z12y8exwkJS3JI7j0TWkcS8zKDxHxxwbMvvsQTzzzHvW/vMk4nfP7Om9z54ieiDdgAVtl0tybbkiTbK7etabvlqw/e5f3XXmV3dp2qYhgnqorVIhGSYn/jkCxWU+wil5l8+JBl/gc798au/w/RD4eDCHvmWhaefP4Frt04p48jX773Fj98c4thnI7FHhG4DINBEfx8/x7f3fyaabvlwR+/E9H2/JdxFLhEWDuJaHx/6yafvf0Gp9eu04eR1jsY+A9CHIoeEMwqVDZ9mtheucr2ytV9srHWYscOlGm3tmau+thFH0daNKoKl49w8n63bMp2X1rTKKmqGIaBqlIfRm5/8hEXf/9JHyfKxR5erTcqE9ZFUldm0YcF1CQ8DB2M7nz6MYpg2OxnLxjb4AiRuRRGZWdX1Yg9rQ9JZoHMsJmOhq7LYbIWsgRob7hPembejl4vA4SCqiQiVlAenVlVERGUq1yaKD78FyIYKx9nIeZ/AAAAAElFTkSuQmCC">TEST Le choix de l&#39;outil de gestion des coopératives - Informatique / ERP / ODOO - Supermarchés Coopératifs : le forum</A> 

66# <DL><p> 

67# <DL><p> 

68# <DT><A HREF="https://github.com/issues?q=is%3Aopen+is%3Aissue+org%3Afotonower+archived%3Afalse+label%3Aready_for_qa" ADD_DATE="1644685336" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACKElEQVQ4jW2SO48dRRCFv1PdM3P37rXXFjL7HwiRkACRkRAS8ovgJ5CSgwh4SIBIIOBN4MAPJJCQsAUBIJ7W4pmqQzD33sUSFXWru07Vd6p0enp6frLbPdUAeodlgdbE/0RmurWmrKrm6A+Su32z2z3dW38dW4Fc0SVAEpLwIdtGIQREtATGbfBK74AgDC0k97GzLIuWeSZzARsb+jAwbTYGmOdZ0lqnL60xCrvsiObMJDM5e+wGZ4+fIwWS+PXH+/z12y8exwkJS3JI7j0TWkcS8zKDxHxxwbMvvsQTzzzHvW/vMk4nfP7Om9z54ieiDdgAVtl0tybbkiTbK7etabvlqw/e5f3XXmV3dp2qYhgnqorVIhGSYn/jkCxWU+wil5l8+JBl/gc798au/w/RD4eDCHvmWhaefP4Frt04p48jX773Fj98c4thnI7FHhG4DINBEfx8/x7f3fyaabvlwR+/E9H2/JdxFLhEWDuJaHx/6yafvf0Gp9eu04eR1jsY+A9CHIoeEMwqVDZ9mtheucr2ytV9srHWYscOlGm3tmau+thFH0daNKoKl49w8n63bMp2X1rTKKmqGIaBqlIfRm5/8hEXf/9JHyfKxR5erTcqE9ZFUldm0YcF1CQ8DB2M7nz6MYpg2OxnLxjb4AiRuRRGZWdX1Yg9rQ9JZoHMsJmOhq7LYbIWsgRob7hPembejl4vA4SCqiQiVlAenVlVERGUq1yaKD78FyIYKx9nIeZ/AAAAAElFTkSuQmCC">TEST Issues label:ready_for_qa</A> 

69# </DL><p> 

70# </DL><p> 

71 

72head_favoris = """<!DOCTYPE NETSCAPE-Bookmark-file-1> 

73<!-- This is an automatically generated file. 

74 It will be read and overwritten. 

75 DO NOT EDIT! --> 

76<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"> 

77<TITLE>Bookmarks</TITLE> 

78<H1>Bookmarks</H1> 

79""" 

80image_ftn = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACKElEQVQ4jW2SO48dRRCFv1PdM3P37rXXFjL7HwiRkACRkRAS8ovgJ5CSgwh4SIBIIOBN4MAPJJCQsAUBIJ7W4pmqQzD33sUSFXWru07Vd6p0enp6frLbPdUAeodlgdbE/0RmurWmrKrm6A+Su32z2z3dW38dW4Fc0SVAEpLwIdtGIQREtATGbfBK74AgDC0k97GzLIuWeSZzARsb+jAwbTYGmOdZ0lqnL60xCrvsiObMJDM5e+wGZ4+fIwWS+PXH+/z12y8exwkJS3JI7j0TWkcS8zKDxHxxwbMvvsQTzzzHvW/vMk4nfP7Om9z54ieiDdgAVtl0tybbkiTbK7etabvlqw/e5f3XXmV3dp2qYhgnqorVIhGSYn/jkCxWU+wil5l8+JBl/gc798au/w/RD4eDCHvmWhaefP4Frt04p48jX773Fj98c4thnI7FHhG4DINBEfx8/x7f3fyaabvlwR+/E9H2/JdxFLhEWDuJaHx/6yafvf0Gp9eu04eR1jsY+A9CHIoeEMwqVDZ9mtheucr2ytV9srHWYscOlGm3tmau+thFH0daNKoKl49w8n63bMp2X1rTKKmqGIaBqlIfRm5/8hEXf/9JHyfKxR5erTcqE9ZFUldm0YcF1CQ8DB2M7nz6MYpg2OxnLxjb4AiRuRRGZWdX1Yg9rQ9JZoHMsJmOhq7LYbIWsgRob7hPembejl4vA4SCqiQiVlAenVlVERGUq1yaKD78FyIYKx9nIeZ/AAAAAElFTkSuQmCC" 

81 

82 

83 

84# - [ ] add date in name 

85# - [ ] add date in link 

86 

87def create_favoris(map_user_issues, file="favoris.html"): 

88 with open(file, "w") as f: 

89 f.write(head_favoris) 

90 f.write("<DL><p>\n") 

91 f.write(" <DT><H3>Listes de tickets ouvert depuis trop longtemps</H3>\n") 

92 f.write(" <DL><p>\n") 

93 

94 for user in map_user_issues: 

95 f.write(" <DT><H3>" + str(user) + "</H3>\n") 

96 f.write(" <DL><p>\n") 

97 # print("<a name='" + str(datestr) + "_" + str(user) + "' ></a>") 

98 if len(map_user_issues[user]) > 0: 

99 # print ("For user : " + str(user)) 

100 if "list_inactiv" in map_user_issues[user]: 

101 for issue in map_user_issues[user]["list_inactiv"]: 

102 # print(issue["link"] + " for " + str(issue["nb_days_inactiv"]) + " days") 

103 f.write( 

104 " <DT><A HREF=\"" + issue["link"] + "\" ICON=\"" + image_ftn + "\">" + issue[ 

105 "link"] + " for " + str(issue["nb_days_inactiv"]) + "</A>\n") 

106 f.write(" </DL><p>\n") 

107 

108 f.write(" </DL><p>\n") 

109 f.write("</DL><p>\n") 

110 

111 return 0 

112 

113 

114 

115def issue_atmo_has_label(issue, list_labels): 

116 found = False 

117 if "labels" not in issue: 

118 return found 

119 if issue["labels"] == None: 

120 return found 

121 for label in issue["labels"]: 

122 if label != None and "name" in label and label["name"] in list_labels: 

123 found = True 

124 

125 return found 

126 

127 

128 

129def issue_has_label(issue, list_labels): 

130 found = False 

131 for label in issue.labels: 

132 if label.name.lower() in list_labels: 

133 found = True 

134 

135 return found 

136 

137 

138def check_issue_body(issue, list_comments, parse_link_wiki, link_issue): 

139 if issue["concat_body"] != None: 

140 has_up_anchor = issue["concat_body"].lower().find("<a name=up></a>") 

141 if has_up_anchor < 0: 

142 print( 

143 "Missing anchor up (now using top) in body of issue " + str(issue["name"]) + " " + str(issue["title"])) 

144 

145 map_wiki_issue_link = {} 

146 

147 import re 

148 # Check if the issue body has a link to a wiki page starting with https://github.com/fotonower/Fotonower/wiki 

149 wiki_links = re.findall(r'(https\:\/\/github\.com\/fotonower\/Fotonower\/wiki[\/]?[^\s]*)', 

150 issue["concat_body"]) 

151 # m = re.search(r'\[\[([^\]]+)\]\]', issue.body) 

152 if len(wiki_links) > 0: 

153 print("Found a link to a wiki page in the body of issue " + str(issue)[:50] + " wiki_link : " + str(wiki_links)) 

154 map_wiki_issue_link = {link_issue: wiki_links} 

155 

156 nbi = issue["number"] 

157 link_issue 

158 

159 # internal_comment_links = re.findall(r'(https\:\/\/www\.github\.com\/fotonower\/saxia\/issues\/11\/[^\s]*)', issue.body) 

160 link_issue_for_regexp = link_issue.replace("/", "\/").replace(":", "\:").replace("www.", "[www.]?").replace(".", 

161 "\.") 

162 search_regex = r"({0}\#issuecomment\-[\d]*)".format(link_issue_for_regexp) 

163 print(" search_regex : " + str(search_regex)) 

164 internal_comment_links = re.findall(search_regex, issue["concat_body"]) 

165 # VR TODO on pourrait aussi chercher les lien vers des anchor contenu dans les commentaires, mais c'est beaucoup plus compliqué ! 

166 # VR TODO on pourrait aussi récupérer l'id du comment pour lister plus facilement tous les commentaires qui ne sont pas lister 

167 

168 present_comment_links = [] 

169 for m in internal_comment_links: 

170 link = None 

171 if type(m) == str: 

172 link = m 

173 if link != None and link not in present_comment_links: 

174 present_comment_links.append(link) 

175 

176 missing_comment_links = [] 

177 nb_total_comments = 0 

178 map_id_comment_missing_link_body_to_comment = {} 

179 for c in list_comments: 

180 nb_total_comments = nb_total_comments + 1 

181 c_id = str(c["git_io"]["id"] if "git_io" in c and "id" in c["git_io"] else c["name"].split("#")[-1]) 

182 link_comment = link_issue + "#issuecomment-" + c_id 

183 label_missing_link_body_to_comment = False 

184 if link_comment.replace("www.", 

185 "") not in present_comment_links and link_comment not in missing_comment_links and "issue-" not in link_comment and "DEFAULTFISTR" not in link_comment: 

186 print("Missing link to comment " + str(c["name"]) + " in issue " + str(issue["name"]) + " " + str( 

187 issue["title"]) + " link : " + str(link_comment)) 

188 missing_comment_links.append(link_comment.replace(".www", "")) 

189 label_missing_link_body_to_comment = True 

190 map_id_comment_missing_link_body_to_comment[c_id] = label_missing_link_body_to_comment 

191 if c["body"] != None: 

192 wiki_links_comments = re.findall(r'(https\:\/\/github\.com\/fotonower\/Fotonower\/wiki[\/]?[^\s]*)', 

193 c["body"]) 

194 if len(wiki_links_comments) > 0: 

195 print("Found a link to a wiki page in the comment of issue " + str( 

196 issue["name"]) + " wiki_link : " + str(wiki_links_comments)) 

197 map_wiki_issue_link[link_comment] = wiki_links_comments 

198 

199 ret = {"has_up_anchor": has_up_anchor >= 0, "wiki_links": map_wiki_issue_link} 

200 

201 ret["internal_comment_links"] = internal_comment_links 

202 ret["missing_comment_links"] = missing_comment_links 

203 ret["draft_reason"] = (str(len(missing_comment_links)) + "/" + str(nb_total_comments - 1) + " MISSING\n") if len( 

204 missing_comment_links) > 0 else "OK\n" 

205 

206 ret["map_id_comment_missing_link_body_to_comment"] = map_id_comment_missing_link_body_to_comment 

207 

208 return ret 

209 

210 map_id_comment_missing_link_body_to_comment = {} 

211 for c in list_comments: 

212 c_id = str(c["git_io"]["id"] if "git_io" in c and "id" in c["git_io"] else c["name"].split("#")[-1]) 

213 map_id_comment_missing_link_body_to_comment[c_id] = True 

214 

215 return {"map_id_comment_missing_link_body_to_comment" : map_id_comment_missing_link_body_to_comment} 

216 

217 

218 

219def from_label_to_map_color_name(labels): 

220 list_map_labels = [] 

221 for label in labels: 

222 map_label = {} 

223 map_label["name"] = label.name 

224 map_label["color"] = label.color 

225 list_map_labels.append(map_label) 

226 return list_map_labels 

227 

228 

229def check_rate(headers): 

230 # print("TODO") 

231 

232 xratelimitremaining = headers['x-ratelimit-remaining'] if "x-ratelimit-remaining" in headers else 0 

233 xratelimitlimit = headers['x-ratelimit-limit'] if "x-ratelimit-limit" in headers else 0 

234 if int(xratelimitlimit) > 0 and int(xratelimitremaining) * 5 < int(xratelimitlimit): 

235 print(" x-ratelimit-remaining : " + str(xratelimitremaining)) 

236 print(" x-ratelimit-limit : " + str(xratelimitlimit)) 

237 print("Now we aren't blind anymore, to avoid rate limit issues, we wait for 1 minute before continuing !") 

238 import time 

239 time.sleep(60) 

240 

241 

242# 'x-ratelimit-limit' (140650522216608) '5000' 

243# 'x-ratelimit-remaining' (140650522216528) '4993' 

244# 'x-ratelimit-reset' (140650522216368) '1684285562' 

245# 'x-ratelimit-resource' (140650522216288) 'core' 

246# 'x-ratelimit-used' (140650522216448) '7' 

247 

248def util_is_naive(dated_at): 

249 is_not_naive = dated_at.tzinfo is not None and dated_at.tzinfo.utcoffset(dated_at) is not None 

250 return is_not_naive 

251 

252# f1 gag 3-8-25 

253def load_github_to_json_with_type(list_repos=["fotonower/Velours", "fotonower/projects", "fotonower/Admin"], 

254 # override by default argument_parser 

255 list_excluded_labels=["project:rubbia", "project:cod", "project:adenes", "project:broca", 

256 "project:circpack", "project:qualipapia", "project:mpa", "project:datou", 

257 "documentation", "bug", "it_maintenance", "reporting work", "business", 

258 "commercial", "recherche", "rh", "refacto"], 

259 token="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", 

260 verbose=False, only_count=False, 

261 with_comment=False, 

262 check_issue_re=False, 

263 load_all=False, 

264 list_required_labels=[], 

265 last_number_days = None, 

266 previous_number_days = None, 

267 limit=0, 

268 offset=0, 

269 list_issues=[], 

270 parse_link_wiki=True, 

271 cache_data_file=None, 

272 find_detailed_update_at = False, 

273 list_labels_to_parse = [], 

274 config_atmo_for_name = "default"): 

275 from datetime import datetime 

276 start = datetime.now() 

277 

278 g = Github(token) 

279 

280 reponame = list_repos[0].replace("/", "_") if len(list_repos) == 1 else "all_repos_" + str(len(list_repos)) 

281 

282 from tzlocal import get_localzone 

283 print(" start cached_at : " + str(start) + " local timezone : " + str(get_localzone())) 

284 data_load = {"issues": [], "type": "repo", 

285 "name" : reponame, 

286 "title" : reponame, "cached_at" : start, "cache_local_timezome" : get_localzone()} 

287 

288 count_issue = 0 

289 

290 for repo_name in list_repos: 

291 

292 repo = g.get_repo(repo_name) 

293 state_to_load = 'all' if (load_all) else 'open' # or closed 

294 

295 if list_required_labels == []: 

296 open_issues = repo.get_issues(state=state_to_load) 

297 else: 

298 if "audit" in list_required_labels: 

299 with_comment = True 

300 find_detailed_update_at = True 

301 open_issues = repo.get_issues(state=state_to_load, labels=list_required_labels) 

302 

303 for issue in open_issues: 

304 import sys 

305 sys.stdout.write(";") 

306 sys.stdout.flush() 

307 if len(list_issues) > 0 and (repo_name.replace("fotonower/", "") + "/" + str(issue.number)) not in list_issues: 

308 continue 

309 if count_issue < offset: 

310 count_issue += 1 

311 continue 

312 if count_issue == (limit + offset) and limit > 0: 

313 print("Limit number issue to parse") 

314 break 

315 check_rate(issue._headers) 

316 

317 # Exclusion is more powerful 

318 if not issue_has_label(issue, list_excluded_labels) and ( 

319 len(list_required_labels) == 0 or issue_has_label(issue, list_required_labels) or "issue" in list_required_labels): 

320 if issue.body == None: 

321 print("issue.body is None, but I want this issue !") 

322 # continue 

323 

324 if util_is_naive(issue.updated_at): 

325 print(" Issue NOTNAIVE " + str(issue.number) + " has a timezone, we convert it to UTC : " + str( 

326 issue.updated_at) + " ! ") 

327 else: 

328 print(" Issue NAIVE " + str(issue.number) + " has no timezone, we assume it is in UTC : " + str( 

329 issue.updated_at) + " ! ") 

330 

331 if last_number_days or previous_number_days: 

332 from datetime import date, datetime 

333 aujourdhui = datetime.now() 

334 delta = aujourdhui - issue.updated_at 

335 

336 if last_number_days and delta.days > last_number_days: 

337 print("Issue has not been modified recently, we don't use it ! ") 

338 continue 

339 if previous_number_days and delta.days < previous_number_days: 

340 print("Issue has not been modified recently, we don't use it ! ") 

341 continue 

342 

343 concat_body = issue.body if issue.body is not None else "" 

344 size_body = len(issue.body.lower()) if issue.body is not None else 0 

345 size_total = 0 

346 all_comments = [] 

347 if with_comment: 

348 all_comments_as_git = issue.get_comments() 

349 for comment_git in all_comments_as_git: 

350 comment = {"body": comment_git.body, 

351 "updated_at": comment_git.updated_at, 

352 "name": issue.repository.name + "/" + str(issue.number) + "#issuecomment-" + str( 

353 comment_git.id), 

354 "type": "comment", 

355 "git_io": {"id": comment_git.id}} 

356 all_comments.append(comment) 

357 

358 data_one_issue = {"id": issue.id, 

359 "title": issue.title, 

360 "number": issue.number, 

361 "repo": issue.repository.name, 

362 "assignees": issue.assignees, 

363 "size_body": size_body, 

364 "updated_at": issue.updated_at, 

365 "created_at": issue.created_at, 

366 "concat_body": concat_body, 

367 "labels": from_label_to_map_color_name(issue.labels) + [{"name": "issue", "color": "9b0f3d"}], 

368 "comments_data": all_comments, 

369 "name": issue.repository.name + "/" + str(issue.number), 

370 "type": "issue"} 

371 data_load["issues"].append(data_one_issue) 

372 count_issue = count_issue + 1 

373 else: 

374 print("WARNING This issue has some of the excluded label, we don't treat it ! " + str(issue.number) + " " + str( 

375 issue.title) + " labels : " + str(issue.labels)) 

376 

377 print("Time to load issues from github : " + str(datetime.now() - start)) 

378 

379 if len(list_issues) == 1: 

380 # .repository.name 

381 print(" WE EXPECT type list[str] : " + str(type(list_issues)) + " type(list_issues[0]) " + str(type(list_issues[0]))) 

382 montcucq_manque_pas_par = list_issues[0].replace("/", "_") 

383 reponame = f"{montcucq_manque_pas_par}_single_issue" 

384 elif config_atmo_for_name != "default": 

385 reponame = f"{reponame}_{config_atmo_for_name}_atmo_config" 

386 elif limit > 0: 

387 reponame = f"{reponame}_limit_{limit}_offset_{offset}" 

388 

389 if cache_data_file is None: 

390 cache_data_file = f"data_{reponame}.json" 

391 print("CACHEDATAFILE We save the data in cache file : " + str(cache_data_file)) 

392 print("CACHEDATAFILE TIME cached_at : " + str(data_load["cached_at"] if "cached_at" in data_load else "unknown") + " local timezone : " + str(data_load["cache_local_timezone"] if "cache_local_timezone" in data_load else "unknown")) 

393 with open(cache_data_file, "w") as outfile: 

394 json.dump(data_load, outfile, indent=4, default=str) 

395 

396 ret_atmo_data = parse_atmo_from_json_cache(data_load=data_load, 

397 list_repos=list_repos, # override by default argument_parser 

398 list_excluded_labels=list_excluded_labels, 

399 verbose=verbose, only_count=only_count, 

400 with_comment=with_comment, 

401 check_issue_re=check_issue_re, 

402 list_required_labels=list_required_labels, 

403 last_number_days=last_number_days, 

404 limit=limit, 

405 parse_link_wiki=parse_link_wiki, 

406 list_labels_to_parse = list_labels_to_parse, 

407 list_issues=list_issues) 

408 return ret_atmo_data, reponame # as repo_name_with_cond 

409 

410def compute_info_edit_atmo(content_this_comment, content, checkbox_name = "", title = ""): 

411 list_find_begin_end = content_this_comment.split(content) 

412 if len(list_find_begin_end) > 2: 

413 print("WARNING : content_this_comment.split(content) has more than 2 elements, we take the first one ! we don't accept edition from ATMO then : " + str(checkbox_name) + " " + str(title) + " for this content : " + content) 

414 return {} 

415 else : 

416 if "\n" not in list_find_begin_end[0]: 

417 begin_char = 0 

418 else : 

419 len_until_before_last = 0 

420 begin_char = len(list_find_begin_end[0]) - len(list_find_begin_end[0].split("\n")[-1]) 

421 

422 # begin_char = len(list_find_begin_end[0]) 

423 end_char = len(list_find_begin_end[0]) + len(content) 

424 len_content = end_char - begin_char 

425 info_edit_atmo = {"begin_char": begin_char, "end_char": end_char, "len_content": len_content} 

426 return info_edit_atmo 

427 

428 

429def parse_checkbox_as_atmo_doc_func(comment_name, content_this_comment, c_updated_at, 

430 c_label, list_excluded_labels = [], 

431 list_required_labels = [], 

432 list_labels_to_parse = []): 

433 import re 

434 checkbox_data = [] 

435 search_regex = r"\[([x| ])\](.+)\n" 

436 content_this_comment = content_this_comment.replace("NBSP", " ") 

437 content_this_comment = content_this_comment.replace("\xa0", " ") 

438 content_this_comment = content_this_comment.replace("\u00a0", " ") 

439 

440 all_checkboxs = re.findall(search_regex, content_this_comment) 

441 count_checkbox = 0 

442 for checkbox in all_checkboxs: 

443 if len(checkbox) != 2: 

444 print("Unexpected format parsed checkboxes ") 

445 

446 done_or_not, content = checkbox 

447 title = content 

448 

449 # or DDB328 

450 if done_or_not == " ": 

451 label_todone = "todo" 

452 color_todone = "f46aff" 

453 else : 

454 label_todone = "done" 

455 color_todone = "20fe32" 

456 labels = [{"name" : label_todone, "color" : color_todone}] 

457 from copy import deepcopy 

458 labels = deepcopy(c_label) if c_label is not None else [] 

459 labels.insert(0, {"name" : label_todone, "color" : color_todone}) 

460 

461 checkbox_name = comment_name + "/CB" + str(count_checkbox) 

462 one_checkbox_as_atmo_data = {"name" : checkbox_name, 

463 "title" : title, 

464 "content" : content, 

465 "labels" : labels, 

466 "links" : [{"label":"hierarchical", "target":comment_name}], 

467 "type" : "checkbox", 

468 "updated_at" : c_updated_at} 

469 

470 info_edit_atmo = compute_info_edit_atmo(content_this_comment, content) 

471 

472 one_checkbox_as_atmo_data["info_edit_atmo"] = info_edit_atmo 

473 

474 if len(list_excluded_labels) > 0 and issue_atmo_has_label(one_checkbox_as_atmo_data, list_excluded_labels): 

475 print("WARNING This checkbox has some of the excluded label, we don't treat it ! " + str(one_checkbox_as_atmo_data["name"]) + " " + str( 

476 one_checkbox_as_atmo_data["title"]) + " labels : " + str(one_checkbox_as_atmo_data["labels"])) 

477 continue 

478 

479 checkbox_data.append(one_checkbox_as_atmo_data) 

480 count_checkbox += 1 

481 

482 return checkbox_data 

483 

484# f4 gag 3-8-25 

485def parse_atmo_from_json_cache(data_load={}, 

486 list_repos=["fotonower/Velours", "fotonower/projects", "fotonower/Admin"], 

487 # override by default argument_parser 

488 list_excluded_labels=[], # "project:rubbia", "project:cod", "project:adenes", 

489 #"project:broca", "project:circpack", "project:qualipapia", 

490 # "project:mpa", "project:datou", "documentation", "bug", 

491 # "it_maintenance", "reporting work", "business", 

492 # "commercial", "recherche", "rh", "refacto" 

493 verbose=False, only_count=False, 

494 with_comment=False, 

495 check_issue_re=False, 

496 list_required_labels=[], 

497 last_number_days=None, 

498 previous_number_days=None, 

499 limit=0, 

500 parse_link_wiki=True, 

501 parse_checkbox_as_atmo_doc = True, 

502 config_parse_wip = {}, 

503 list_labels_to_parse = [], 

504 list_issues = []): 

505 import datetime 

506 from datetime import timezone 

507 start = datetime.datetime.now() 

508 todaynn = datetime.datetime.now(timezone.utc) 

509 today = datetime.datetime.now() 

510 

511 data = {"issues_data": []} 

512 if "name" in data_load: 

513 data["name"] = data_load["name"] 

514 if "title" in data_load: 

515 data["title"] = data_load["title"] 

516 if "type" in data_load: 

517 data["type"] = data_load["type"] 

518 

519 content_recently_updated = "" 

520 map_wiki_issue_link = {} # For check_issue_re 

521 

522 count_issue = 0 

523 for data_one_issue in (data_load["issues"] if "issues" in data_load else []): 

524 

525 if list_required_labels != [] and not issue_atmo_has_label(data_one_issue, list_required_labels) and not "issue" in list_required_labels: 

526 print("WARNING This issue has not the required label, we don't treat it ! " + str(data_one_issue["number"]) + " " + str( 

527 data_one_issue["title"]) + " labels : " + str(data_one_issue["labels"])) 

528 continue 

529 

530 if list_excluded_labels != [] and issue_atmo_has_label(data_one_issue, list_excluded_labels): 

531 print("WARNING This issue has some of the excluded label, we don't treat it ! " + str(data_one_issue["number"]) + " " + str( 

532 data_one_issue["title"]) + " labels : " + str(data_one_issue["labels"])) 

533 continue 

534 

535 updated_at_str = data_one_issue["updated_at"] if type(data_one_issue["updated_at"]) == str else data_one_issue[ 

536 "updated_at"].strftime("%Y-%m-%d %H:%M:%S") 

537 

538 content_recently_updated += "Repo : " + data_one_issue["repo"] + " issue : " + data_one_issue[ 

539 "title"] + " updated at " + " id : " + str(data_one_issue["id"]) + " Last Update : " + updated_at_str + "\n" 

540 content_recently_updated += "\nbody :\n" + str(data_one_issue["concat_body"]) 

541 

542 concat_body = data_one_issue["concat_body"] if "concat_body" in data_one_issue else "" 

543 size_body = len(concat_body) 

544 

545 size_total = 0 

546 if with_comment: 

547 

548 count_to_do_this_comment = len( 

549 concat_body.split("[ ]")) - 1 if concat_body is not None else 0 

550 count_done_this_comment = len( 

551 concat_body.lower().split("[x]")) - 1 if concat_body is not None else 0 

552 

553 title = (data_one_issue["title"] if "title" in data_one_issue else "") + " " + \ 

554 ("TODO : " + str(count_to_do_this_comment) + "/" + str( 

555 count_to_do_this_comment + count_done_this_comment) + " \n size : " + str( 

556 len(concat_body))) if parse_checkbox_as_atmo_doc else data_one_issue["title"] 

557 

558 fake_first_comment = {"body": concat_body, "title" : title, 

559 "name" : data_one_issue["name"] + "#DEFAULTFISTR", 

560 "git_io": {"nb": data_one_issue["id"]}, 

561 "type" : "comment", 

562 "labels" : [{"color" : "a2eeef", "name" : "comment"}], 

563 "links" : [{"label":"hierarchical", "target":data_one_issue["name"]}]} 

564 info_from_first_comment = parse_comment_with_config_parse(concat_body, context_node_parent=fake_first_comment, 

565 list_labels_to_parse=list_labels_to_parse, 

566 verbose=verbose) 

567 fake_first_comment["checkboxs_data"] = info_from_first_comment 

568 

569 all_comments = data_one_issue["comments_data"] if "comments_data" in data_one_issue else [] 

570 

571 for c in all_comments: 

572 c["links"] = [{"label":"hierarchical", "target":data_one_issue["name"]}] 

573 # Conversion git to json 

574 comment_name = "comment_default_name_todo" if "name" not in c else c["name"] 

575 c_body = c["body"] 

576 

577 title = c_body.strip() 

578 

579 if title.startswith("[up](#"): 

580 title = title[6:] # remove [up](# and before 

581 if "#" in title: 

582 title = title.replace("##", "#").replace("##", "#").replace("##", "#") # remove double # 

583 title = title.split("#")[1] 

584 if "\n" in title: 

585 # 100 c'est déjà trop long => je ne comprends pas pourquoi ce superbug arrive parfois et parfois non ! 

586 title = title.split("\n")[0].strip()[:400] 

587 else : 

588 title = title[:400] #c["name"] 

589 else: 

590 print("Missing info title !") 

591 title = c["name"] 

592 

593 c["title"] = title # comment_name 

594 c_updated_at = c["updated_at"] if "updated_at" in c else datetime.datetime.now() 

595 # print(str(c.body)) 

596 c_updated_at_str = c_updated_at if type(c_updated_at) == str else c_updated_at.strftime("%Y-%m-%d %H:%M:%S") 

597# c_updated_at = datetime.datetime.strptime(c_updated_at_str, "%Y-%m-%d %H:%M:%S") 

598 

599 if "+" in c_updated_at_str: # or "-" in updated_at_str.split(" ")[-1]: 

600 c_updated_at = datetime.datetime.strptime(c_updated_at_str, "%Y-%m-%d %H:%M:%S%z") 

601 else: 

602 c_updated_at = datetime.datetime.strptime(c_updated_at_str, "%Y-%m-%d %H:%M:%S") 

603 

604# content_this_comment = "\nComment last modified at " + c_updated_at_str + "\n" 

605 content_this_comment = "" 

606 content_this_comment += c_body if c_body is not None else "" 

607 if last_number_days: 

608 if util_is_naive(c_updated_at): 

609 nb_day_aux = todaynn - c_updated_at 

610 else: 

611 nb_day_aux = today - c_updated_at 

612 

613 if nb_day_aux.days <= last_number_days: 

614 print(" This comment has been modified after aujourdhui minus last_number_days : " + str( 

615 last_number_days)) 

616 content_recently_updated += content_this_comment 

617 if parse_checkbox_as_atmo_doc: 

618 print("TODO parse checkbox as atmo doc => première version done") 

619 

620 c["labels"] = [ d for d in data_one_issue["labels"] if d["name"] != "issue"] if "labels" in data_one_issue else [] 

621 c["labels"].insert(0, {"name": "comment", "color": "0075ca"}) 

622 

623 # VR SANDBOX use marko to parse markdown 

624 test = parse_comment_with_config_parse(content_this_comment, context_node_parent=c, 

625 list_labels_to_parse = list_labels_to_parse, 

626 verbose = verbose) 

627 

628 checkboxs_data = [] 

629# checkboxs_data = parse_checkbox_as_atmo_doc_func(comment_name, content_this_comment, 

630# c_updated_at, c["labels"], 

631# list_excluded_labels=list_excluded_labels) 

632 checkboxs_data.extend(test) 

633 

634 count_to_do_this_comment = len(content_this_comment.split("[ ]")) - 1 if content_this_comment is not None else 0 

635 count_done_this_comment = len(content_this_comment.lower().split("[x]")) - 1 if content_this_comment is not None else 0 

636 c["title"] += " TODO : " + str(count_to_do_this_comment) + "/" + str( 

637 count_to_do_this_comment + count_done_this_comment) + " \n size : " + str( 

638 len(content_this_comment)) # + " \n updated at : " + c_updated_at_str 

639 

640 c["checkboxs_data"] = checkboxs_data 

641 concat_body += content_this_comment 

642 # print(c_body) 

643 size_total += len(c_body) if c_body is not None else 0 

644 

645 all_comments.append(fake_first_comment) 

646 

647 else: 

648 all_comments = [] 

649 

650 print(" l502 updated_at_str : " + str(updated_at_str)) 

651 print("+" in updated_at_str) 

652 if "+" in updated_at_str: # or "-" in updated_at_str.split(" ")[-1]: 

653 updated_at_as_date = datetime.datetime.strptime(updated_at_str, "%Y-%m-%d %H:%M:%S%z") 

654 else : 

655 updated_at_as_date = datetime.datetime.strptime(updated_at_str, "%Y-%m-%d %H:%M:%S") 

656 if util_is_naive(updated_at_as_date): 

657 nb_day_since_activ = todaynn - updated_at_as_date 

658 else: 

659 nb_day_since_activ = today - updated_at_as_date 

660 

661 if last_number_days != None and nb_day_since_activ.days > last_number_days: 

662 print("WARNING This issue has not been modified after aujourdhui minus last_number_days : " + str( 

663 last_number_days) + ", we don't use it ! ") 

664 continue 

665 if previous_number_days != None and nb_day_since_activ.days < previous_number_days: 

666 print("WARNING This issue has not been modified before aujourdhui minus previous_number_days : " + str( 

667 previous_number_days) + ", we don't use it ! ") 

668 continue 

669 

670 count_to_do = len(concat_body.split("[ ]")) - 1 if concat_body is not None else 0 

671 count_done = len(concat_body.lower().split("[x]")) - 1 if concat_body is not None else 0 

672 

673 # if verbose: 

674 # print(" count_to_do : " + str(count_to_do) + " count_done : " + str(count_done) + " size_body : " + str(size_body) + " size_total : " + str(size_total)) 

675 

676 link_issue = "https://www.github.com/fotonower/" + data_one_issue["repo"] + "/issues/" + str( 

677 data_one_issue["number"]) 

678 

679 links = find_link_issue(data_one_issue["concat_body"]) 

680 

681# VR TODO 5-8-25 : this makes stuff not very visible ! 

682 if "name" in data: 

683 links.append({"target" : data["name"], "label" : "hierarchical"}) 

684 else: 

685 print("WARNING data has no name ! : " + str(data.keys())) 

686 

687 res_check_issue = {} 

688 if check_issue_re: 

689 res_check_issue = check_issue_body(data_one_issue, all_comments, parse_link_wiki, link_issue) 

690 

691 if "wiki_links" in res_check_issue: 

692 for issue_link in res_check_issue["wiki_links"]: 

693 for wiki_link in res_check_issue["wiki_links"][issue_link]: 

694 if wiki_link not in map_wiki_issue_link: 

695 map_wiki_issue_link[wiki_link] = [] 

696 if issue_link not in map_wiki_issue_link[wiki_link]: 

697 map_wiki_issue_link[wiki_link].append(issue_link) 

698 if "missing_comment_links" in res_check_issue: 

699 for missing_link in res_check_issue["missing_comment_links"]: 

700 print("Missing link to comment : " + missing_link) 

701 

702 if "map_id_comment_missing_link_body_to_comment" in res_check_issue: 

703 map_id_comment_missing_link_body_to_comment = res_check_issue["map_id_comment_missing_link_body_to_comment"] 

704 for comment in all_comments: 

705 c_id = str(comment["git_io"]["id"] if "git_io" in comment and "id" in comment["git_io"] else comment["name"].split("#")[-1]) 

706 if c_id in map_id_comment_missing_link_body_to_comment: 

707 comment["label_missing_link_body_to_comment"] = map_id_comment_missing_link_body_to_comment[c_id] 

708 if map_id_comment_missing_link_body_to_comment[c_id] == False: 

709 comment["labels"].append({"name": "missing_link_body_to_comment", "color": "ff0000"}) 

710 else: 

711 comment["label_missing_link_body_to_comment"] = "unknown" 

712 

713 draft_reason = res_check_issue[ 

714 "draft_reason"] if check_issue_re and "draft_reason" in res_check_issue else "OK\n" 

715 complete_info = (draft_reason) + " nb_day_since_activ : " + str( 

716 nb_day_since_activ.days) 

717 title = data_one_issue["title"] + " " + str(count_to_do) + "/" + str( 

718 count_to_do + count_done) + " \n size_total : " + str( 

719 size_body + size_total) + " \n size_body : " + str(size_body) + " " + complete_info 

720 

721 if not only_count: 

722 print("ISSUE:" + str(data_one_issue["title"]) + " TODO " + str(count_to_do) + " DONE " + str( 

723 count_done) + " " \ 

724 + str(data_one_issue["number"]) + " " + ",".join( 

725 map(lambda x: x["name"], data_one_issue["labels"])) \ 

726 + " " + link_issue + " body : " + str(size_body) + " and total comments : " + str(size_total)) 

727 

728 # if (size_body + size_total < 16000): 

729 # print("TO PARSE") 

730 # print(concat_body) 

731 # print("TO CONTINUE") 

732 

733 # VR TODO REFACTO 30-7-25 : on ne veut pas de git_issue mais plutot faire un extend ? 

734 data_issue_form_comment_and_parsing = { 

735 "count_done": count_done, 

736 "count_to_do": count_to_do, 

737 "size_total": size_total, 

738 "link": link_issue, 

739 "links" : links, 

740 "title" : title} 

741# "comments_data" : all_comments} # a priori dans le data_one_issue 

742 data_one_issue.update(data_issue_form_comment_and_parsing) 

743 data_one_issue["title"] = title 

744 data["issues_data"].append(data_one_issue) 

745 count_issue += 1 

746 

747 data["recently_updated"] = content_recently_updated 

748 

749 print("Number issues : " + str(count_issue)) 

750 

751 if parse_link_wiki: 

752 data["wiki_links"] = map_wiki_issue_link 

753 if len(map_wiki_issue_link) > 0: 

754 print("Found " + str(len(map_wiki_issue_link)) + " links to wiki pages in issues and comments") 

755 for wiki_link in map_wiki_issue_link: 

756 print( 

757 "Wiki link : " + wiki_link.rstrip(")") + "/_edit\n# Lien Retour :\n<details>\n\n - " + "\n - ".join( 

758 map_wiki_issue_link[wiki_link]) + "\n</details>") 

759 

760 ## Broken, maybe due to date added or something else 

761 # with open("data.json", "r+") as outfile: 

762 # outfile.seek(0) 

763 # json.dump(data, outfile) 

764 

765 print("Time to parse issues : " + str(datetime.datetime.now() - start)) 

766 

767 return data 

768 

769def append_node_and_edge(issue, map_node_to_node, list_nb_days_since_activ_for_darken, 

770 map_node_to_father_hierarchical, 

771 config_gag = {"list_link_display" : ["hierarchical", "allow"], "criteria_size" : "nb_days_activity"}, 

772 config_filter = {"required_labels" : [], 

773 "excluded_labels" : [], 

774 "list_issues" : []}): 

775 

776 max_size_title = 256 if "max_size_title" not in config_gag else config_gag["max_size_title"] 

777 

778 import datetime 

779 from datetime import timezone 

780 todaynn = datetime.datetime.now(timezone.utc) 

781 today = datetime.datetime.now() 

782 

783 try : 

784 expected_fields = set(ATMODataFTN.model_fields.keys()) 

785 submitted_fields = set(issue.keys()) 

786 unknown_fields = submitted_fields - expected_fields 

787 

788 from copy import deepcopy 

789 issue_copy = deepcopy(issue) 

790 

791 # VR 5-8-25 : on a pas besoin de faire de deepcopy car la recursion portent en dernier sur les objets externes qui contiennet le reste dans des champs inattendu en attendant le refacto qui changera les data_issues en data 

792 # mais on en a besoin pour updated_at 

793 for d in unknown_fields: # and d != "updated_at": 

794 del issue_copy[d] 

795 

796 one_atmo_data = ATMODataFTN(**issue_copy) 

797 if len(one_atmo_data.title) > max_size_title: 

798 one_atmo_data.title = one_atmo_data.title[:max_size_title] 

799 print(f"WARNING : title is {max_size_title} chars, maybe truncated : " + one_atmo_data.title) 

800 one_atmo_data.labels.append({"name" : "OBJECTION", "color" : "ff0000"}) 

801 #if ATMODataFTN.parse_raw(json.dumps(issue, default=str)): # and not validate_json 

802# if ATMODataFTN.parse_raw(**issue): => ca ca marche pas 

803 # print("Issue is valid") 

804# one_atmo_data = ATMODataFTN(**json.loads(json.dumps(issue, default=str))) 

805 

806 # if unknown_fields: 

807 # print(f"Log these unexpected fields: {unknown_fields}") 

808 

809 

810 except Exception as e: 

811 print(str(e)) 

812 print("Issue is NOT valid") 

813 print(json.dumps(issue, indent=2, default=str)[:1000]) 

814 print(" Expected : ") 

815 print(json.dumps(ATMODataFTN.model_json_schema(), indent=2)) 

816 print("ERROR DATA NOT TREATED") 

817 print(str(e)) 

818 return 

819 

820 index = one_atmo_data.name 

821 

822 # print(issue["concat_body"]) 

823 

824 links = one_atmo_data.links 

825 

826# if len(links) > 0: 

827# print(str(links)) 

828 

829 criteria_size = config_gag.get("criteria_size", "nb_days_activity") 

830 if criteria_size not in ["none", "size_total", "nb_days_activity", "nb_todo", "nb_checkboxs", "nb_done"]: 

831 print("Invalid criteria_size: " + criteria_size) 

832 return 

833 

834 value_criteria_for_size = None 

835 

836 if criteria_size == "nb_days_activity": # and "updated_at" not in issue: 

837 if "updated_at" in issue and type(issue["updated_at"]) == datetime.datetime: 

838 updated_at_as_date = issue["updated_at"] 

839 elif "updated_at" in issue and type(issue["updated_at"]) == str: 

840 if "+" in issue["updated_at"]: # or "-" in updated_at_str.split(" ")[-1]: 

841 updated_at_as_date = datetime.datetime.strptime(issue["updated_at"], "%Y-%m-%d %H:%M:%S%z") 

842 else : 

843 updated_at_as_date = datetime.datetime.strptime(issue["updated_at"], "%Y-%m-%d %H:%M:%S") 

844 else: 

845 if "updated_at" in issue: 

846 print("Type not expected " + str(type(issue["updated_at"])) + " for updated_at : ") 

847 else: 

848 print("updated_at missing in issue ") 

849 updated_at_as_date = None 

850 

851 if updated_at_as_date != None: 

852 # print(" updated_at_as_date is not naive " + str(util_is_naive(updated_at_as_date))) 

853 # print(" todaynn is not naive " + str(util_is_naive(todaynn))) 

854 # print(" today is not naive " + str(util_is_naive(today))) 

855 if util_is_naive(updated_at_as_date): 

856 nb_day_since_activ = todaynn - updated_at_as_date 

857 else: 

858 nb_day_since_activ = today - updated_at_as_date 

859# nb_day_since_activ_days = nb_day_since_activ.days 

860 value_criteria_for_size = nb_day_since_activ.days 

861 else: 

862# nb_day_since_activ_days = -1 

863 value_criteria_for_size = -1 

864 elif criteria_size == "size_total": 

865 if "size_total" in issue and type(issue["size_total"]) == int: 

866 value_criteria_for_size = -issue["size_total"] 

867 elif "body" in issue and type(issue["body"]) == str: 

868 issue["size_total"] = len(issue["body"]) 

869 value_criteria_for_size = -len(issue["body"]) 

870 else: 

871 issue["size_total"] = 1 

872 value_criteria_for_size = -1 

873 else: 

874 print("Invalid criteria_size: " + criteria_size + " for now") 

875 value_criteria_for_size = -1 

876 

877 title = one_atmo_data.title 

878 

879 # labels = one_atmo_data.labels 

880 labels = issue["labels"] if "labels" in issue else [] 

881 # VR TODO 5-8-25 replace by one_atmo_data.labels 

882 issue_copy["labels"] = one_atmo_data.labels 

883 

884 list_excluded_labels = config_filter["excluded_labels"] if "excluded_labels" in config_filter else [] 

885 list_required_labels = config_filter["required_labels"] if "required_labels" in config_filter else [] 

886 if len(list_excluded_labels) > 0 and issue_atmo_has_label(issue_copy, list_excluded_labels): 

887 return None 

888 if len(list_required_labels) > 0 and not issue_atmo_has_label(issue_copy, list_required_labels): 

889 return None 

890 

891 if value_criteria_for_size != None: 

892 list_nb_days_since_activ_for_darken.append(value_criteria_for_size) 

893 else: 

894 print("TO DEBUG") 

895 

896 # Search label hierarchical 

897 for link in links: 

898 if link == None: 

899 continue 

900 if "label" in link and link["label"].lower() == "hierarchical": 

901 if "target" in link and link["target"] != "": 

902 target = link["target"] 

903 if index not in map_node_to_father_hierarchical: 

904 map_node_to_father_hierarchical[index] = target 

905 else : 

906 if map_node_to_father_hierarchical[index].lower() != target.lower(): 

907 # print("OBJECTION WARNING : " + str(index) + " already has a target which is " + map_node_to_father_hierarchical[index] + " in hierarchical link, we don't update it !") 

908 # print("We add an OBJECTION label to the node " + str(index)) 

909 one_atmo_data.labels.append({"name" : "OBJECTION", "color" : "ff0000"}) 

910 print("OBJECTION WARNING : " + str(index) + " already has a target which is " + map_node_to_father_hierarchical[index] + " and not " + target + " in hierarchical link, we don't update it !") 

911 else: 

912 print("dummy duplicate code") 

913 

914 if index in map_node_to_node: 

915 if map_node_to_node[index] != {"links": links, 

916 "title": title, 

917 "labels": one_atmo_data.labels, 

918 "assigned": len(one_atmo_data.assignee), 

919 criteria_size: value_criteria_for_size, 

920 "type": one_atmo_data.type}: 

921 print("OBJECTION WARNING : index " + str(index) + " already in map_node_to_node, we don't overwrite it !") 

922 else: 

923 print("dummy duplicate code") 

924# map_node_to_node[index]["labels"].append({"name" : "OBJECTION", "color" : "ff0000"}) 

925 else: 

926 map_node_to_node[index] = {"links": links, 

927 "title": title, 

928 "labels": one_atmo_data.labels, 

929 "assigned": len(one_atmo_data.assignee), 

930 criteria_size: value_criteria_for_size, 

931 "type": one_atmo_data.type} # one_atmo_data.type 

932 return issue 

933 

934def build_gag_data_from_dict_rec(data, map_node_to_node, 

935 list_nb_days_since_activ_for_darken, 

936 map_node_to_father_hierarchical, 

937 config_gag = {"list_link_display" : [], "criteria_size" : "nb_days_activity"}, # possible value for criteria_size : "none", "size_total", "nb_days_activity", "nb_todo", "nb_checkboxs", "nb_done" 

938 config_filter = {"required_labels" : [], 

939 "excluded_labels" : [], 

940 "list_issues" : []}): 

941 data_loop = [] 

942 used_data = [] # va contenir des dupliquer 

943 if data == None: 

944 print("Due to add generic parsing of md ") 

945 return map_node_to_node, [] 

946 if "issues_data" in data: 

947 data_loop = data["issues_data"] 

948 elif "comments_data" in data : 

949 data_loop = data["comments_data"] 

950 elif "checkboxs_data" in data: 

951 data_loop = data["checkboxs_data"] 

952 else : 

953 print("Pas utile celui-ci mais l'autre et on enleve celui la") 

954# ret = append_node_and_edge(data, map_node_to_node, list_nb_days_since_activ_for_darken, map_node_to_father_hierarchical, config_gag=config_gag, 

955# config_filter=config_filter) 

956 # will avoid useless append to the tree, which by the way made warning for the unique hierarchical tree building 

957 # Since this is an internal node, we manage the stat in this special usecase doomly 

958 # VR TO TEST 

959# if ret != None: 

960# used_data.append(ret) 

961# return map_node_to_node, [0] 

962 print("Looping") 

963 

964 for d in data_loop: 

965 ret = build_gag_data_from_dict_rec(d, map_node_to_node, list_nb_days_since_activ_for_darken, 

966 map_node_to_father_hierarchical, config_gag=config_gag, 

967 config_filter=config_filter) 

968 if ret != None: 

969 used_data.extend(ret) 

970 

971 ret = append_node_and_edge(data, map_node_to_node, list_nb_days_since_activ_for_darken, map_node_to_father_hierarchical, config_gag=config_gag, 

972 config_filter=config_filter) 

973 if ret != None: 

974 # En fait ici il faudrait enlever l'internal loop si on veut applaitir la donner, mais je crois qu'il faut faire un deepcopy, ca ne me plait pas, faut mieux faire dans l'autre sens et tout copier sauf le data_loop 

975 used_data.append(ret) 

976 

977 return used_data 

978 

979 

980 

981def util_check_data_inside_parent_node_tree(map_node_to_father_hierarchical, current_node_name, potential_parent_node_name): 

982 count_height = 0 

983 while current_node_name in map_node_to_father_hierarchical: 

984 if count_height > 100: 

985 print("There seems to be a cycle in the supposed hierarchical tree, so there could be an infinite loop too bad " + str(current_node_name)) 

986 break 

987 count_height += 1 

988 if current_node_name.lower() == potential_parent_node_name.lower(): 

989 return True 

990 current_node_name = map_node_to_father_hierarchical[current_node_name] 

991 

992 # Ca c'est juste pour gérer le cas où on met tous les repo, mais en meme temps on s'en fout car on devrait juste pas mettre de condition (je crois) 

993 if current_node_name.lower() == potential_parent_node_name.lower(): 

994 return True 

995 return False 

996 

997 

998 

999# f16 gag 3-8-25 

1000def build_gag_data_from_dict(data, 

1001 config_gag = {"node": {"type" : ["issue", "comment"], 

1002 "color": "blue"}, 

1003 "edge": {"type": ["https"]}}, 

1004 config_filter = {"required_labels" : [], 

1005 "excluded_labels" : [], 

1006 "list_issues" : []}): 

1007 # works without concat_body 

1008 

1009 # J'ai du remplacer 

1010 # - \" par " 

1011 # - les débuts de concat_body \\' par " 

1012 # works with concat_body 

1013 

1014 

1015 

1016 # if len(data["issues"]) > 0 and util_is_naive(data["issues"][0]["updated_at"]): 

1017 # print("FORCE NOT NAIVE") 

1018 # today = datetime.datetime.now(timezone.utc) 

1019 

1020 list_nb_days_since_activ_for_darken = [] 

1021 

1022 map_node_to_node = {} # - [x] VR 3-8-25 renamed from map_issue_link_to_issues 

1023 map_node_to_father_hierarchical = {} 

1024 

1025 used_data = build_gag_data_from_dict_rec(data, map_node_to_node, 

1026 list_nb_days_since_activ_for_darken, 

1027 map_node_to_father_hierarchical, 

1028 config_gag=config_gag, 

1029 config_filter=config_filter) 

1030 

1031# for issue in data["issues_data"]: 

1032# append_node_and_edge(issue, map_node_to_node, list_nb_days_since_activ_for_darken) 

1033 

1034 

1035 

1036 if "list_issues" in config_filter and len(config_filter["list_issues"]) > 0: 

1037 list_to_delete = [] 

1038 for current_node_name in map_node_to_node: 

1039 found_issue = False 

1040 for potential_parent_node in config_filter["list_issues"]: 

1041 found_issue = util_check_data_inside_parent_node_tree(map_node_to_father_hierarchical, current_node_name, potential_parent_node) 

1042 if found_issue: 

1043 break 

1044 if not found_issue: 

1045 list_to_delete.append(current_node_name) 

1046 

1047 for current_node_name in list_to_delete: 

1048 del map_node_to_node[current_node_name] 

1049 if current_node_name in map_node_to_father_hierarchical: 

1050 del map_node_to_father_hierarchical[current_node_name] 

1051 

1052 # Je veux aussi supprimer dans l'autre sens 

1053 list_link_hierarchy_to_delete = [] 

1054 for current_node_name in map_node_to_father_hierarchical: 

1055 if map_node_to_father_hierarchical[current_node_name] in list_to_delete or map_node_to_father_hierarchical[current_node_name] not in map_node_to_father_hierarchical: 

1056 list_link_hierarchy_to_delete.append(current_node_name) 

1057 for current_node_name in list_link_hierarchy_to_delete: 

1058 del map_node_to_father_hierarchical[current_node_name] 

1059 

1060 list_distrib_nb_day_activ = stat_nb_day_activ(list_nb_days_since_activ_for_darken) 

1061 

1062 return map_node_to_node, list_distrib_nb_day_activ, map_node_to_father_hierarchical, used_data 

1063 

1064 

1065 

1066def init_user_count_activ(): 

1067 return {"nb_activ": 0, "nb_inactiv": 0, "list_inactiv": [], "size_body": 0, "size_total": 0, "count_to_do": 0, 

1068 "count_done": 0} 

1069 

1070 

1071def append_inactiv_issue(issue): 

1072 # VR 14-02-2022 : if one wants more legend in issue, it should be here ! 

1073 return {"link": issue["link"], "nb_days_inactiv": issue["nb_days_inactiv"]} 

1074 

1075 

1076def compute_display_user_activ(map_user_issues): 

1077 map_user_issues_resume = {} 

1078 for user in map_user_issues: 

1079 nb_issue_activ = map_user_issues[user]["nb_activ"] 

1080 nb_issue_inactiv = map_user_issues[user]["nb_inactiv"] 

1081 prop = int(100 * nb_issue_activ / (nb_issue_activ + nb_issue_inactiv)) if ( 

1082 nb_issue_activ + nb_issue_inactiv > 0) else 0 

1083 map_user_issues_resume[user] = {"user": user, "nb_issue_activ": nb_issue_activ, 

1084 "nb_issue_inactiv": nb_issue_inactiv, "prop": prop, 

1085 "count_to_do": map_user_issues[user]["count_to_do"], 

1086 "count_done": map_user_issues[user]["count_done"], 

1087 "size_body": map_user_issues[user]["size_body"], 

1088 "size_total": map_user_issues[user]["size_total"]} 

1089 

1090 return map_user_issues_resume 

1091 

1092 

1093def display_user_activ_inactiv(list_resume_to_display_sorted): 

1094 for data in list_resume_to_display_sorted: 

1095 nb_issue_activ = data["nb_issue_activ"] 

1096 nb_issue_inactiv = data["nb_issue_inactiv"] 

1097 prop = data["prop"] 

1098 user = data["user"] 

1099 begin_message = "For user " + str(user) + " activ : " + str(nb_issue_activ) + "/" + str( 

1100 nb_issue_activ + nb_issue_inactiv) 

1101 complete_to_tab = max(0, 45 - len(begin_message)) 

1102 percent_achieve = str(round(100. * data["count_done"] / float(data["count_to_do"] + data["count_done"]))) if ( 

1103 data[ 

1104 "count_to_do"] + 

1105 data[ 

1106 "count_done"]) > 0 else "0" 

1107 print(begin_message + (" " * complete_to_tab) + str(prop) + "% " + " size_body : " + str( 

1108 data["size_body"]) + " size_total : " + str(data["size_total"]) + 

1109 " count_to_do : " + str(data["count_to_do"]) + " count_done : " + str( 

1110 data["count_done"]) + " prop : " + str(percent_achieve) + "%") 

1111 

1112 # Marseille 

1113 from datetime import datetime, timedelta 

1114 original_date = datetime(2023, 8, 26, 13, 0) 

1115 nb_hour = int(float(percent_achieve) * 10.08) 

1116 # print(" nb hour : " + str(nb_hour)) 

1117 # new_date = original_date + timedelta(hours = nb_hour) # Affichez la 

1118 # print("La nouvelle date et heure est: ", new_date) # Utilisez la fonction add_hours(10) 

1119 

1120 # ORga 10-23 

1121 date1 = datetime(2024, 1, 21) 

1122 date2 = datetime(2023, 6, 8) 

1123 # Compter les jours entre les deux dates 

1124 diff = date1 - date2 

1125 total_hours = diff.days * 24. 

1126 nb_hour = int(float(percent_achieve) * total_hours / 100) 

1127 # Calculer la date et l'heure correspondant à la proportion p de temps passé 

1128 new_date = date2 + timedelta(hours=nb_hour) 

1129 

1130 print("La date et l'heure correspondant à la proportion", percent_achieve, "de temps passé est:", new_date) 

1131 

1132 

1133def list_inactiv_issue_per_user(list_issues): 

1134 for issue in list_issues: 

1135 print(issue["link"] + " for " + str(issue["nb_days_inactiv"]) + " days") 

1136 

1137 

1138def sum_for_user(map_user, map_issue, list_to_sum=["count_to_do", "count_done", "size_body", "size_total"]): 

1139 for s in list_to_sum: 

1140 if s in map_user and s in map_issue and str(map_issue[s]).isdigit() and type(map_user[s]) == type(1): 

1141 map_user[s] += int(map_issue[s]) 

1142 

1143def group_by_user_from_versatile_data(data, nb_days_tolerance = 30): 

1144 # Add data and list_users 

1145 # list_users = ["unassigned"] 

1146 

1147 list_issues = [] 

1148 

1149 import datetime 

1150 from datetime import timezone 

1151 todaynn = datetime.datetime.now(timezone.utc) 

1152 today = datetime.datetime.now() 

1153 

1154 datestr = today.strftime("%d%m%Y") 

1155 

1156 map_user_issues = {"unassigned": init_user_count_activ()} 

1157 

1158 nb_total_activ = 0 

1159 nb_total_inactiv = 0 

1160 

1161 map_user_issues["ALL"] = init_user_count_activ() 

1162 map_user_issues["ALLu"] = init_user_count_activ() 

1163 for issue in data["issues_data"]: 

1164 if list_issues == [] or (issue["repo"] + "/" + str(issue["number"])) in list_issues: 

1165 updated_at = issue["updated_at"] 

1166 if util_is_naive(updated_at): 

1167 inactiv_period = todaynn - updated_at 

1168 else: 

1169 inactiv_period = today - updated_at 

1170 

1171 nb_days_inactiv = inactiv_period.days 

1172 

1173 # c = a - b 

1174 # seconds = c.total_seconds() 

1175 

1176 activ = nb_days_inactiv < nb_days_tolerance 

1177 

1178 issue["nb_days_inactiv"] = nb_days_inactiv 

1179 

1180 if activ: 

1181 map_user_issues["ALL"]["nb_activ"] += 1 

1182 else: 

1183 map_user_issues["ALL"]["nb_inactiv"] += 1 

1184 # map_user_issues["ALL"]["list_inactiv"].append(append_inactiv_issue(issue)) 

1185 

1186 if len(issue["assignees"]) == 0: 

1187 if activ: 

1188 map_user_issues["unassigned"]["nb_activ"] += 1 

1189 # nb_total_activ += 1 

1190 else: 

1191 map_user_issues["unassigned"]["nb_inactiv"] += 1 

1192 # nb_total_inactiv += 1 

1193 map_user_issues["unassigned"]["list_inactiv"].append(append_inactiv_issue(issue)) 

1194 

1195 map_user_issues["unassigned"]["size_body"] += issue["size_body"] 

1196 sum_for_user(map_user_issues["unassigned"], issue) 

1197 

1198 for user in issue["assignees"]: 

1199 if user.login not in map_user_issues: 

1200 map_user_issues[user.login] = init_user_count_activ() 

1201 

1202 for user in issue["assignees"]: 

1203 username = user.login 

1204 sum_for_user(map_user_issues[username], issue) 

1205 if activ: 

1206 nb_total_activ += 1 

1207 map_user_issues[username]["nb_activ"] += 1 

1208 else: 

1209 nb_total_inactiv += 1 

1210 map_user_issues[username]["nb_inactiv"] += 1 

1211 inactiv_link = append_inactiv_issue(issue) 

1212 map_user_issues[username]["list_inactiv"].append(inactiv_link) 

1213 

1214 sum_for_user(map_user_issues["ALLu"], issue) 

1215 sum_for_user(map_user_issues["ALL"], issue) 

1216 

1217 map_user_issues["ALLu"]["nb_activ"] = nb_total_activ 

1218 map_user_issues["ALLu"]["nb_inactiv"] = nb_total_inactiv 

1219 

1220 print("For tolerance nb of days of : " + str(nb_days_tolerance)) 

1221 resume_to_display = compute_display_user_activ(map_user_issues) 

1222 list_resume_to_display_sorted = sorted(resume_to_display.values(), key=lambda x: -x["prop"]) 

1223 display_user_activ_inactiv(list_resume_to_display_sorted) 

1224 

1225 for user in map_user_issues: 

1226 print("<a name='" + str(datestr) + "_" + str(user) + "' ></a>") 

1227 if len(map_user_issues[user]) > 0: 

1228 print("For user : " + str(user)) 

1229 if "list_inactiv" in map_user_issues[user]: 

1230 list_inactiv_issue_per_user(map_user_issues[user]["list_inactiv"]) 

1231 

1232 create_favoris(map_user_issues) 

1233 

1234 return map_user_issues["ALL"] 

1235 

1236def load_github_to_json_prepare_gag_and_group_by_user(list_repos, list_excluded_labels, 

1237 list_issues, 

1238 token, nb_days_tolerance=30, verbose=False, 

1239 with_comment=False, check_issue_re=False, 

1240 limit=0, 

1241 offset=0, 

1242 load_all=False, 

1243 list_labels_to_parse=[], 

1244 config_atmo_for_name = "default"): 

1245 data, repo_name_with_cond = \ 

1246 load_github_to_json_with_type(list_repos, list_excluded_labels, token, 

1247 verbose, with_comment=with_comment, 

1248 check_issue_re=check_issue_re, 

1249 limit=limit, offset=offset, 

1250 list_issues=list_issues, 

1251 load_all=load_all, 

1252 list_labels_to_parse=list_labels_to_parse, 

1253 config_atmo_for_name = config_atmo_for_name) 

1254 

1255 data_by_user = group_by_user_from_versatile_data(data, nb_days_tolerance = nb_days_tolerance) 

1256 

1257 return data_by_user, data, repo_name_with_cond 

1258 

1259 

1260def find_link_issue(text): 

1261 import re 

1262 # Je pense qu'il n'y a jamais de www.github.com 

1263 pattern = r'https://github\.com/fotonower/([\-\w]+)/issues/(\d+)' 

1264 matches = re.findall(pattern, text) 

1265 

1266 list_found = [] 

1267 

1268 for match in matches: 

1269 repo, issue_number = match 

1270 index = repo + "/" + str(issue_number) 

1271 if index not in list_found: 

1272 list_found.append(index) 

1273 

1274 list_with_label = list(map(lambda x : {"label" : "http", "target" : x}, list_found)) 

1275 return list_with_label 

1276 

1277 

1278def stat_nb_day_activ(list_nb_days_since_activ_for_darken): 

1279 if len(list_nb_days_since_activ_for_darken) == 0: 

1280 return [0] 

1281 list_decil_nb_day_activ = [] 

1282 

1283 nb_decile = 8 

1284 list_nb_days_since_activ_for_darken.append(-30) 

1285 list_nb_days_since_activ_for_darken.sort(reverse=True) 

1286 # for i in range(nb_decile - 1, 0, -1): 

1287 for i in range(nb_decile): 

1288 subscrit = int(i * float(len(list_nb_days_since_activ_for_darken)) / float(nb_decile)) 

1289 if subscrit < len(list_nb_days_since_activ_for_darken): 

1290 list_decil_nb_day_activ.append(list_nb_days_since_activ_for_darken[subscrit]) 

1291 list_decil_nb_day_activ.append(min(list_nb_days_since_activ_for_darken)) # => ca pourrait tout faire foirer pour le type decil ! 

1292 

1293 return list_decil_nb_day_activ 

1294 

1295 

1296def convert_activ_to_size(nb_day_activ, list_decil_nb_day_activ, 

1297 type_scale = "decil", # decil,linear,log()not done) 

1298 verbose = False): 

1299 

1300 if type_scale == "decil": 

1301 i = 0 

1302 while i < len(list_decil_nb_day_activ) and nb_day_activ < list_decil_nb_day_activ[i]: 

1303 i = i + 1 

1304 

1305 i = i + 1 # pas sur enfin bon 

1306 elif type_scale == "linear": 

1307 min_l = min(list_decil_nb_day_activ) if len(list_decil_nb_day_activ) > 0 else 0 

1308 max_l = max(list_decil_nb_day_activ) if len(list_decil_nb_day_activ) > 0 else 0 

1309 

1310 surface_linear_square = len(list_decil_nb_day_activ) * len(list_decil_nb_day_activ) * (nb_day_activ * nb_day_activ - max_l * max_l) / (max_l * max_l - min_l * min_l) if max_l != min_l else 25 

1311 

1312 import math 

1313 if surface_linear_square > 0: 

1314 val = int((math.sqrt(surface_linear_square))) 

1315 else: 

1316 val = int((math.sqrt(-surface_linear_square))) 

1317 if val < 0: 

1318 i = -val 

1319 else: 

1320 i = val 

1321 

1322 else : 

1323 print ("Unexpected type_scale " + str(type_scale) + " using decil") 

1324 i = 2 

1325 

1326 if verbose: 

1327 print(" value_criteria_size : " + str(nb_day_activ) + " height : " + str(i)) 

1328 

1329 return i 

1330 

1331 

1332# en fait non je court-ciruite, il faut juste rajouter un # 

1333def hex_to_rgb(value): 

1334 value = value.lstrip('#') 

1335 lv = len(value) 

1336 color_as_tuple = tuple(int(value[i:i + lv // 3], 16) for i in range(0, lv, lv // 3)) 

1337 color_as_csv = "".join(map(str, color_as_tuple)) 

1338 return "#" + value 

1339 

1340 

1341 

1342def mise_en_forme_text_replace_some_voila_par_voila(input_text): 

1343 max_length_line = 32 

1344 

1345 list_part = input_text.split(" ") 

1346# max_length_no_space = max(list_part, key=len) 

1347 new_list_part = [] 

1348 for part in list_part: 

1349 if len(part) > max_length_line: 

1350 new_list_part.append(part[:max_length_line]) 

1351 new_list_part.append(part[max_length_line:]) 

1352 else: 

1353 new_list_part.append(part) 

1354 

1355 if len(input_text)/4 < max_length_line: 

1356 max_length_line = int(len(input_text)/4) 

1357 

1358 new_text_with_new_line = "" 

1359 nb_char_from_start_line = 0 

1360 for part in new_list_part: 

1361 if nb_char_from_start_line + len(part) > max_length_line: 

1362 new_text_with_new_line += "\n" 

1363 nb_char_from_start_line = 0 

1364 else : 

1365 new_text_with_new_line += " " 

1366 new_text_with_new_line += part 

1367 nb_char_from_start_line += len(part) 

1368 

1369# while max_length_no_space > 64: 

1370# input_text = input_text[:64] + " " + input_text[64:] 

1371 

1372 return new_text_with_new_line 

1373 

1374 

1375 

1376def mise_en_forme_text(input_text, 

1377 format_gag_mise_en_forme_text_tooltip = "default"): # VR 20-8-25 TO DOC en CDC : one_short_line 

1378 return mise_en_forme_text_replace_some_voila_par_voila(input_text[:256]) 

1379 

1380 if len(input_text.split("\n")) > 3: 

1381 print("VR 20-8-25 : ununderstood case !") 

1382 if format_gag_mise_en_forme_text_tooltip == "one_short_line": 

1383 return input_text.split("\n")[0] 

1384 return input_text 

1385 input_text_bak = input_text 

1386 # Une ligne de deux caractère fais la largeur egale à la hauteur d'une ligne : on egalise les deux 

1387 # nb_line * size_split = nb_char 

1388 # Egaliser les hauteur : nb_line = size_split / 2 

1389 displayed_text_with_endline = "" 

1390 import math 

1391 size_split = int(math.sqrt(2 * len(input_text))) + 1 

1392 while len(input_text) > 0: 

1393 displayed_text_with_endline += input_text[:size_split] + "\n" 

1394 input_text = input_text[size_split:] 

1395 

1396 # VR 20-8-25 : for debugging purposse to avoid wrong counting of line number in dot .gv 

1397 if format_gag_mise_en_forme_text_tooltip == "one_short_line": 

1398 displayed_text_with_endline = input_text_bak[:size_split].split("\n")[0] 

1399 

1400 return displayed_text_with_endline 

1401 

1402def highest(issue_link, map_issue_link_to_issues): 

1403 if issue_link.startswith("http"): 

1404 return False 

1405 if issue_link not in map_issue_link_to_issues: 

1406 print("WARNING not really expected : issue link " + str(issue_link) + " not in map_issue_link_to_issues in highest function") 

1407 return True 

1408 if "links" not in map_issue_link_to_issues[issue_link]: 

1409 return True 

1410 for link in map_issue_link_to_issues[issue_link]["links"]: 

1411 if link["label"].lower() == "hierarchical" and link["target"] in map_issue_link_to_issues: 

1412 return False 

1413 return True 

1414 

1415def make_gag(map_issue_link_to_issues, 

1416 list_distrib_nb_day_activ, 

1417 out_path='doctest-output/Fotonower_Issues', 

1418 with_title_in_node=True, 

1419 with_linked_issue=False, 

1420 format_gag = {"map_label_to_format_for_edges" : 

1421 {"MOYEN" : {"shape" : "oval", "fillcolor" : "peachpuff"}, 

1422 "OBJECTIF" : {"shape" : "hexagon", "fillcolor" : "yellow"}, 

1423 "TACHE" : {"shape" : "rectangle", "fillcolor" : "lightblue"}} 

1424 ,"format": "svg", "layout": "fdp", "overlap": "prism", 

1425 "filter_links" : {"required_labels": [], 

1426 "excluded_labels": [], 

1427 "list_issues": []}, 

1428 "criteria_size": "nb_days_activity"}, 

1429 parsed_data_used_for_gag = None, 

1430 title_di_graph = "Fotonower Issues Graph", 

1431 link_title = "http://atmo.fotonower.com:8091", 

1432 generate_dynamic_css = False, 

1433 dynamic_css_config = None): 

1434 format_gag_mise_en_forme_text_tooltip = format_gag["format_text_tooltip"] if "format_text_tooltip" in format_gag else "default" 

1435 import os 

1436 out_folder = os.path.dirname(out_path) 

1437 out_file = os.path.basename(out_path) 

1438 

1439 print("Now build the dot graph and call dot") 

1440 import graphviz 

1441 

1442 import os 

1443 os.system("pwd") 

1444 

1445 # , "URL": link_title} => disturb a lot the rendering and action in the interface 

1446 

1447 map_label_to_format_for_edges = format_gag["map_label_to_format_for_edges"] if "map_label_to_format_for_edges" in format_gag else {} 

1448 edge_format = format_gag["edge_format"] if "edge_format" in format_gag else {} 

1449 

1450 map_cluster_to_title = {} 

1451 

1452 list_clusters = [] 

1453 list_title_to_dels = [] 

1454 for issue in map_issue_link_to_issues: 

1455 if "labels" in map_issue_link_to_issues[issue] and len(map_issue_link_to_issues[issue]["labels"]) > 0: 

1456 for label in map_issue_link_to_issues[issue]["labels"]: 

1457 if "cluster" == label["name"].lower() and issue not in list_clusters: 

1458 list_clusters.append(issue) 

1459 break 

1460 # VR TODO peut-etre rajouter une option dans config gag pour activer ou non cette fonctionnalité 

1461 if "use_title_node" in format_gag and format_gag["use_title_node"]: 

1462 if "title" == label["name"].lower(): 

1463 found_cluster = False 

1464 if "links" in map_issue_link_to_issues[issue]: 

1465 for link in map_issue_link_to_issues[issue]["links"]: 

1466 if link["label"].lower() == "cluster": 

1467 found_cluster = True 

1468 map_cluster_to_title[link["target"]] = map_issue_link_to_issues[issue]["title"].strip().strip("TITLE").strip() 

1469 list_title_to_dels.append(issue) 

1470 break 

1471 if not found_cluster: 

1472 # VR TODO il aurait fallu chercher uniquement le cluster hierarchical, par ailleurs ils sont un peu doublonner par le fait qu'il y a un lien aussi, ca se mord 

1473 #if highest(link["target"], map_issue_link_to_issues): 

1474 title_di_graph = map_issue_link_to_issues[issue]["title"].strip().strip("TITLE").strip() 

1475 list_title_to_dels.append(issue) 

1476 #else: 

1477 # print("WARNING : title label found without cluster link for issue " + str(issue)) 

1478# map_cluster_to_title[issue] = map_issue_link_to_issues[issue]["title"] 

1479 

1480 if "delete_title_node" in format_gag and format_gag["delete_title_node"]: 

1481 for t in list_title_to_dels: 

1482 if t in map_issue_link_to_issues: 

1483 del map_issue_link_to_issues[t] 

1484 

1485 # VR TODO DOC indicate that it works only for one layer clsuter 

1486 map_issue_to_cluster = {} 

1487 for node in list_clusters: 

1488 del map_issue_link_to_issues[node] 

1489 for issue in map_issue_link_to_issues: 

1490 for link in map_issue_link_to_issues[issue]["links"]: 

1491 if link["target"] in list_clusters: 

1492 map_issue_to_cluster[issue] = link["target"] 

1493 

1494 map_cluster_to_subgraph_object = {} 

1495 

1496 label_color_cluster = "peachpuff" 

1497 if "cluster" in map_label_to_format_for_edges : 

1498 label_color_cluster = map_label_to_format_for_edges["cluster"]["fillcolor"] if "fillcolor" in \ 

1499 map_label_to_format_for_edges["cluster"] else label_color_cluster 

1500 

1501 dot = graphviz.Digraph(out_path, 

1502 graph_attr = {"label": title_di_graph, 

1503 "fontsize": "100px", 

1504 "labelloc": "t", 

1505 "fontname": "helvetica", 

1506 "style": "bold"}, 

1507 comment='Some Data on ' + os.path.basename(out_path)) #, title="Some Data on " + os.path.basename(out_path)) 

1508 

1509 # dot.subgraph(name='child', node_attr={'shape': 'box'}) 

1510 for cluster in list_clusters: 

1511 label_cluster_display = cluster 

1512 if "cluster" in label_cluster_display: 

1513 label_cluster_display = label_cluster_display.split("cluster")[-1].strip("_") 

1514 title_cluster = cluster 

1515 graph_attr={"fillcolor": label_color_cluster, "style":"filled,rounded,dotted", 

1516 "label" : label_cluster_display, "fontsize":"39px"} 

1517 

1518 if cluster in map_cluster_to_title: 

1519 title_cluster = map_cluster_to_title[cluster] 

1520 graph_attr["label"] = title_cluster 

1521 map_cluster_to_subgraph_object[cluster] = graphviz.Digraph("cluster_" + title_cluster, 

1522 comment=title_cluster, 

1523 # title=cluster, 

1524 graph_attr=graph_attr) 

1525# dot.subgraph(map_cluster_to_subgraph_object[cluster]) 

1526# map_cluster_to_subgraph_object[cluster] = dot.subgraph(name=cluster) 

1527 # dot.subgraph(name=cluster, comment=cluster, _attributes={"class": "cluster"}) 

1528# map_issue_link_to_issues 

1529 

1530 map_node_gaglink = {} 

1531 

1532 for i in map_issue_link_to_issues: 

1533 pair_repo_number = i.split("/") 

1534 issue = map_issue_link_to_issues[i] 

1535 repo = pair_repo_number[0] 

1536 nb = pair_repo_number[1] if len(pair_repo_number) > 1 else "-1" 

1537 label_color = hex_to_rgb(issue["labels"][0]["color"]) if len(issue["labels"]) > 0 else "blue" 

1538 

1539 # new_node.attr(fillcolor="blue", style="filled", color=label_color) 

1540 # , fillcolor="blue", style="filled" OU color='blue',style='filled') 

1541 

1542 shape = 'square' if issue["assigned"] == 0 else 'oval' 

1543 image_path = None 

1544 style_node = None 

1545 fontcolor = "black" 

1546 for label in issue["labels"]: 

1547 if label["name"] in map_label_to_format_for_edges: 

1548 shape = map_label_to_format_for_edges[label["name"]]["shape"] if "shape" in map_label_to_format_for_edges[label["name"]] else shape 

1549 label_color = map_label_to_format_for_edges[label["name"]]["fillcolor"] if "fillcolor" in map_label_to_format_for_edges[label["name"]] else label_color 

1550 image_path = map_label_to_format_for_edges[label["name"]]["image"] if "image" in map_label_to_format_for_edges[label["name"]] else None 

1551 style_node = map_label_to_format_for_edges[label["name"]]["style"] if "style" in map_label_to_format_for_edges[label["name"]] else None 

1552 fontcolor = map_label_to_format_for_edges[label["name"]]["fontcolor"] if "fontcolor" in map_label_to_format_for_edges[label["name"]] else fontcolor 

1553 

1554 criteria_size = format_gag["criteria_size"] if "criteria_size" in format_gag else "nb_days_activity" 

1555 if criteria_size not in ["none", "size_total", "nb_days_activity"]: 

1556 print("Invalid criteria_size: " + criteria_size + " using nb_days_activity") 

1557 continue 

1558# list_nb_days_since_activ_for_darken.append(criteria_size) 

1559 

1560 if criteria_size == "nb_days_activity": 

1561 value_criteria_for_size = issue["nb_days_activity"] if "nb_days_activity" in issue else 30 

1562 type_scale = "decil" 

1563 elif criteria_size == "size_total": 

1564 value_criteria_for_size = issue["size_total"] 

1565 type_scale = "linear" 

1566 else: 

1567 continue 

1568 

1569 if "type_scale" in format_gag: 

1570 type_scale = format_gag["type_scale"] 

1571 

1572 fontsize = None 

1573 height = None 

1574# fontsize = convert_activ_to_size(issue[criteria_size], list_distrib_nb_day_activ) 

1575 height = convert_activ_to_size(issue[criteria_size] if criteria_size in issue else 30, 

1576 list_distrib_nb_day_activ, type_scale=type_scale) 

1577 

1578 # new_node.attr(shape='square') 

1579 # shape='oval' 

1580 

1581 # dot.node(i, i 

1582 # TODO document nice color : https://graphviz.org/doc/info/colors.html#svg 

1583 

1584 # for i in map_issue_link_to_issues: 

1585 displayed_text = i if not with_title_in_node else map_issue_link_to_issues[i]["title"] 

1586 

1587 URL = map_issue_link_to_issues[i]["link"] if "link" in map_issue_link_to_issues[i] else "https://www.github.com/fotonower/" + repo + "/issues/" + nb 

1588 for link in map_issue_link_to_issues[i]["links"]: 

1589 if link["label"].upper() == "GAGLINK": 

1590 map_node_gaglink[i] = link["target"] 

1591 if "http" in link["target"]: 

1592 URL = link["target"] 

1593 

1594 type_val = map_issue_link_to_issues[i]["type"] 

1595 labels_name = " ".join(map(lambda x: x["name"], map_issue_link_to_issues[i]["labels"])) 

1596 # print(" labels_name : " + str(labels_name)) 

1597 

1598 displayed_text = mise_en_forme_text(displayed_text, format_gag_mise_en_forme_text_tooltip) 

1599 tooltip_text = map_issue_link_to_issues[i]["title"] 

1600 if "\\" in tooltip_text: 

1601 print("REPLACE \\ by \ in tooltip_text") 

1602 tooltip_text = tooltip_text.replace("\\", "__") 

1603 if i in map_issue_to_cluster: 

1604 cluster = map_issue_to_cluster[i] 

1605 if cluster in map_cluster_to_subgraph_object: 

1606 used_cluster_or_dot = map_cluster_to_subgraph_object[cluster] 

1607 else: 

1608 print("Internal error treated as warning ") 

1609 used_cluster_or_dot = dot 

1610 

1611# Est-ce du code utile ? VR 15-8-25 

1612 # map_cluster_to_subgraph_object[cluster].node(i, displayed_text, fontsize=str(fontsize) + "pt", 

1613 # tooltip=map_issue_link_to_issues[i]["title"], 

1614 # href=URL, fillcolor=label_color, 

1615 # style="filled", shape=shape, 

1616 # _attributes={"class": type_val + " " + labels_name})) 

1617 else : 

1618 used_cluster_or_dot = dot 

1619 

1620 import json 

1621 list_args = {"tooltip":tooltip_text, 

1622 "href":URL, "fillcolor":label_color, 

1623 "style":"filled", "shape":shape, 

1624 "_attributes":{"class": type_val + " " + labels_name, 

1625 "data-json": json.dumps(map_issue_link_to_issues[i])}} 

1626 if image_path != None: 

1627 list_args["image"] = image_path 

1628# list_args["image"] = "http://localhost:4993/static/" + image_path 

1629# list_args["width"] = "20px" 

1630# list_args["height"] = "20px" 

1631# list_args["fixedsize"] = "true" 

1632 print("We should verify this, on dirait que le shape is none quand meme !") 

1633# list_args["shape"] = "none" 

1634# del list_args["shape"] 

1635 if fontcolor != None: 

1636 list_args["fontcolor"] = fontcolor 

1637 if style_node != None: 

1638 list_args["style"] = style_node 

1639 if fontsize != None: 

1640 list_args["fontsize"] = str(fontsize) + "pt" 

1641 if height != None: 

1642 list_args["height"] = str(height) + "px" 

1643 used_cluster_or_dot.node(i, displayed_text, **list_args) 

1644 # , class=type_val) # 

1645# used_cluster_or_dot.node(i, displayed_text, fontsize=str(fontsize) + "pt", tooltip=tooltip_text, 

1646# href=URL, fillcolor=label_color, 

1647# style="filled", shape=shape, _attributes={"class": type_val + " " + labels_name}) # , class=type_val) # 

1648 

1649 filter_links = format_gag["filter_links"] if "filter_links" in format_gag else {"required_labels": [], "excluded_labels": [], "list_issues": []} 

1650 for i in map_issue_link_to_issues: 

1651 for link in map_issue_link_to_issues[i]["links"]: 

1652 def filter_link(link, filter_links): 

1653 if "label" in link and link["label"].lower() in filter_links["excluded_labels"]: 

1654 return False 

1655 if len(filter_links["required_labels"]) > 0 and "label" in link and link["label"].lower() in filter_links["required_labels"]: 

1656 return True 

1657# if "target" in link and link["target"] in filter_links["list_issues"]: 

1658# return True 

1659 return True 

1660 if not filter_link(link, filter_links): 

1661 continue 

1662 if link == None: 

1663 print("Unexpected format link : " + str(link) + " ignoring this link (none) due to parsing generic btw !") 

1664 continue 

1665 if "target" in link and "label" in link: 

1666 label = link["label"] 

1667 j = link["target"] 

1668 else : 

1669 print("Unexpected format link : " + str(link) + " ignoring this link !") 

1670 continue 

1671 # dot.edges([ij]) 

1672 # if j in map_issue_link_to_issues: # Keep the open or extracted graph 

1673 if j not in map_issue_link_to_issues: 

1674 if with_linked_issue: 

1675 dot.node(j, "X:" + j, shape='circle', 

1676 fontsize="6pt") # on pourrait le rajouter dans map_issue_link_to_issues 

1677 if with_linked_issue or j in map_issue_link_to_issues: 

1678 attributes_edge_init = {"class": label} 

1679 attributes_edge = attributes_edge_init.copy() 

1680 if label in edge_format: 

1681 for attr_edge in edge_format[label]: 

1682 if type(edge_format[label][attr_edge]) == str: 

1683 attributes_edge[attr_edge] = edge_format[label][attr_edge] 

1684 else: 

1685 attributes_edge[attr_edge] = str(edge_format[label][attr_edge]) 

1686 # VR TODO : potentiellement il faudrait mieux que ce soit le js qui affiche ou non, mais cela me semble délicat ! 

1687 dot.edge(i, j, _attributes=attributes_edge) #, _attributes={"class": type_val}) # , constraint='false') 

1688 

1689 # for node in dot.nodes: 

1690 # node.attr('onmouseover', f'this.style.fill = "yellow"; this.style.stroke = "orange";') 

1691 

1692 # print(dot.source) 

1693 

1694 for cluster in list_clusters: 

1695 dot.subgraph(map_cluster_to_subgraph_object[cluster]) 

1696 

1697 # doctest_mark_exe() 

1698 

1699 # dot.attr(rankdir='LR', size='8,5') 

1700 dot.attr('node', shape='oval', fontname='Helvetica') 

1701 dot.attr('edge', fontsize='12') 

1702 dot.attr('graph', splines='true', overlap='false') 

1703 

1704 # graph [splines=true overlap=false]; 

1705 

1706 # neato, fdp (needs overlap=prism ?) , sfdp 

1707 dot.attr(layout='fdp') 

1708 dot.attr(overlap='prism') 

1709 

1710 if parsed_data_used_for_gag != None: 

1711 import json 

1712 with open(out_path + ".json", 'w') as f: 

1713 json.dump(parsed_data_used_for_gag, f) 

1714 

1715 try: 

1716 # VR TODO 31-8-25 REFACTO remove out_folder in dot.render since already in out_path, check impact on job suividev 

1717 dot.render(directory=out_folder, format='svg') # .replace('\\', '/') 

1718 # 'doctest-output/round-table.gv.pdf' 

1719 dot.render(directory=out_folder, format='pdf') 

1720 except Exception as e: 

1721 print(" Exception in dot render : " + str(e)) 

1722 

1723 print("Doc rendered ! Exiting !") 

1724 

1725 # Generate dynamic CSS if requested 

1726 if generate_dynamic_css: 

1727 svg_file_path = os.path.join(out_folder, out_file + '.svg') 

1728 if os.path.exists(svg_file_path): 

1729 if dynamic_css_config is None: 

1730 dynamic_css_config = { 

1731 'initial_delay': 1.0, 

1732 'animation_duration': 10.0, 

1733 'randomize_order': True 

1734 } 

1735 generate_dynamic_css_for_gag( 

1736 svg_file_path, 

1737 css_path=svg_file_path + '.dynamic.css', 

1738 initial_delay=dynamic_css_config.get('initial_delay', 1.0), 

1739 animation_duration=dynamic_css_config.get('animation_duration', 10.0), 

1740 randomize_order=dynamic_css_config.get('randomize_order', True) 

1741 ) 

1742 

1743 return map_node_gaglink 

1744 

1745def generate_dynamic_css_for_gag(svg_path, css_path=None, 

1746 initial_delay=1.0, 

1747 animation_duration=10.0, 

1748 randomize_order=True): 

1749 """ 

1750 Generate CSS file for progressive SVG animation and inject it into the SVG 

1751 

1752 Args: 

1753 svg_path: Path to the SVG file 

1754 css_path: Path for output CSS (default: svg_path + '.dynamic.css') 

1755 initial_delay: Delay before animation starts (seconds) 

1756 animation_duration: Total duration of animation (seconds) 

1757 randomize_order: Whether to randomize node/cluster display order 

1758 """ 

1759 import random 

1760 import os 

1761 import re 

1762 

1763 if css_path is None: 

1764 css_path = svg_path + '.dynamic.css' 

1765 

1766 # Parse SVG to extract nodes, edges, clusters 

1767 with open(svg_path, 'r', encoding='utf-8') as f: 

1768 svg_content = f.read() 

1769 

1770 # Extract all node IDs 

1771 node_pattern = r'<g id="(node\d+)" class="node' 

1772 edge_pattern = r'<g id="(edge\d+)" class="edge' 

1773 cluster_pattern = r'<g id="(clust\d+)" class="cluster' 

1774 

1775 nodes = re.findall(node_pattern, svg_content) 

1776 edges = re.findall(edge_pattern, svg_content) 

1777 clusters = re.findall(cluster_pattern, svg_content) 

1778 

1779 # Build edge dependency map (edge -> [source_node, target_node]) 

1780 edge_dependencies = {} 

1781 edge_full_pattern = r'<g id="(edge\d+)"[^>]*>[\s\S]*?<title>([^<]+)</title>' 

1782 for edge_id, edge_title in re.findall(edge_full_pattern, svg_content): 

1783 if '-->' in edge_title or '-&gt;' in edge_title: 

1784 # Parse edge title to get source and target 

1785 edge_title = edge_title.replace('-', '-').replace('&gt;', '>') 

1786 if '-->' in edge_title: 

1787 parts = edge_title.split('-->') 

1788 if len(parts) == 2: 

1789 source = parts[0].strip() 

1790 target = parts[1].strip() 

1791 

1792 # Find corresponding node IDs 

1793 source_node_pattern = r'<g id="(node\d+)"[^>]*>[\s\S]*?<title>' + re.escape(source) + r'</title>' 

1794 target_node_pattern = r'<g id="(node\d+)"[^>]*>[\s\S]*?<title>' + re.escape(target) + r'</title>' 

1795 

1796 source_matches = re.findall(source_node_pattern, svg_content) 

1797 target_matches = re.findall(target_node_pattern, svg_content) 

1798 

1799 if source_matches and target_matches: 

1800 edge_dependencies[edge_id] = { 

1801 'source': source_matches[0], 

1802 'target': target_matches[0] 

1803 } 

1804 

1805 # Create randomized order for nodes and clusters 

1806 all_elements = list(clusters) + list(nodes) 

1807 if randomize_order: 

1808 random.shuffle(all_elements) 

1809 

1810 # Calculate delays for each element 

1811 element_count = len(all_elements) 

1812 if element_count > 0: 

1813 delay_increment = animation_duration / element_count 

1814 else: 

1815 delay_increment = 0 

1816 

1817 element_delays = {} 

1818 for idx, elem_id in enumerate(all_elements): 

1819 element_delays[elem_id] = initial_delay + (idx * delay_increment) 

1820 

1821 # Generate CSS 

1822 css_lines = [] 

1823 css_lines.append("/* CSS dynamique pour animation progressive du graphique */") 

1824 css_lines.append("/* Généré automatiquement par generate_dynamic_css_for_gag */") 

1825 css_lines.append("") 

1826 css_lines.append("/* Tous les éléments commencent invisibles */") 

1827 css_lines.append("svg g.node, svg g.cluster, svg g.edge {") 

1828 css_lines.append(" opacity: 0;") 

1829 css_lines.append("}") 

1830 css_lines.append("") 

1831 

1832 # Animation for each cluster 

1833 for cluster_id in clusters: 

1834 if cluster_id in element_delays: 

1835 delay = element_delays[cluster_id] 

1836 css_lines.append(f"/* Cluster {cluster_id} */") 

1837 css_lines.append(f"svg g#{cluster_id} {{") 

1838 css_lines.append(f" animation: fadeIn 0.5s ease-in {delay:.2f}s forwards;") 

1839 css_lines.append("}") 

1840 css_lines.append("") 

1841 

1842 # Animation for each node 

1843 for node_id in nodes: 

1844 if node_id in element_delays: 

1845 delay = element_delays[node_id] 

1846 css_lines.append(f"/* Node {node_id} */") 

1847 css_lines.append(f"svg g#{node_id} {{") 

1848 css_lines.append(f" animation: fadeIn 0.5s ease-in {delay:.2f}s forwards;") 

1849 css_lines.append("}") 

1850 

1851 # Text within node appears at same time 

1852 css_lines.append(f"svg g#{node_id} text {{") 

1853 css_lines.append(f" animation: fadeIn 0.5s ease-in {delay:.2f}s forwards;") 

1854 css_lines.append("}") 

1855 css_lines.append("") 

1856 

1857 # Animation for edges (appear after both connected nodes) 

1858 for edge_id in edges: 

1859 if edge_id in edge_dependencies: 

1860 dep = edge_dependencies[edge_id] 

1861 source_delay = element_delays.get(dep['source'], 0) 

1862 target_delay = element_delays.get(dep['target'], 0) 

1863 edge_delay = max(source_delay, target_delay) + 0.5 # 0.5s after last node 

1864 else: 

1865 # If we can't determine dependencies, show at end 

1866 edge_delay = initial_delay + animation_duration 

1867 

1868 css_lines.append(f"/* Edge {edge_id} */") 

1869 css_lines.append(f"svg g#{edge_id} {{") 

1870 css_lines.append(f" animation: fadeIn 0.3s ease-in {edge_delay:.2f}s forwards;") 

1871 css_lines.append("}") 

1872 css_lines.append("") 

1873 

1874 # Add keyframe animation 

1875 css_lines.append("/* Keyframe pour l'animation de fade-in */") 

1876 css_lines.append("@keyframes fadeIn {") 

1877 css_lines.append(" from {") 

1878 css_lines.append(" opacity: 0;") 

1879 css_lines.append(" }") 

1880 css_lines.append(" to {") 

1881 css_lines.append(" opacity: 1;") 

1882 css_lines.append(" }") 

1883 css_lines.append("}") 

1884 

1885 css_content = '\n'.join(css_lines) 

1886 

1887 # Write CSS file (for reference/debugging) 

1888 with open(css_path, 'w', encoding='utf-8') as f: 

1889 f.write(css_content) 

1890 

1891 # NOUVEAU : Injecter le CSS directement dans le SVG 

1892 # Find the first <svg> tag and inject <style> after it 

1893 svg_tag_match = re.search(r'(<svg[^>]*>)', svg_content) 

1894 if svg_tag_match: 

1895 svg_tag_end = svg_tag_match.end() 

1896 style_tag = f'\n<style type="text/css">\n<![CDATA[\n{css_content}\n]]>\n</style>\n' 

1897 

1898 # Insert the style tag after the opening <svg> tag 

1899 svg_content_with_css = svg_content[:svg_tag_end] + style_tag + svg_content[svg_tag_end:] 

1900 

1901 # Write the modified SVG back to file 

1902 with open(svg_path, 'w', encoding='utf-8') as f: 

1903 f.write(svg_content_with_css) 

1904 

1905 print(f"CSS injecté dans le SVG: {svg_path}") 

1906 else: 

1907 print("ATTENTION: Impossible de trouver la balise <svg> pour injecter le CSS") 

1908 

1909 print(f"Dynamic CSS generated: {css_path}") 

1910 print(f" - {len(clusters)} clusters") 

1911 print(f" - {len(nodes)} nodes") 

1912 print(f" - {len(edges)} edges") 

1913 print(f" - Animation: {initial_delay}s delay + {animation_duration}s duration") 

1914 

1915 return css_path 

1916 

1917# f8 gag 3-8-25 graph_git_just_draw_from_data_not_load_DEPRECATED 

1918 

1919 

1920def git_issue_to_retrieval_json(list_repos, list_excluded_labels, github_token, 

1921 list_required_labels=[], 

1922 verbose=False, 

1923 with_comment=False, 

1924 load_cache=False, 

1925 with_closed_issue=False, 

1926 out_path='doctest-output/Fotonower_Issues.json', 

1927 load_all=False, 

1928 last_number_days=None, 

1929 limit=0, 

1930 check_issue_re=False, 

1931 only_recent_update=False): # pragma no cover safia git import => donc sans doute à renommer 

1932 print("TO TEST") 

1933 

1934 import os 

1935 out_folder = os.path.dirname(out_path) 

1936 out_file = os.path.basename(out_path) 

1937 

1938 recently_updated_info = "" 

1939 

1940 import json 

1941 if not load_cache: 

1942 data, repo_name_with_cond = load_github_to_json_with_type(list_repos, list_excluded_labels, github_token, 

1943 verbose, with_comment=with_comment, check_issue_re=check_issue_re, 

1944 load_all=load_all, 

1945 list_required_labels=list_required_labels, 

1946 last_number_days=last_number_days, 

1947 limit=limit) 

1948 

1949 data_for_retrieval = [] 

1950 

1951 if "recently_updated" in data: 

1952 print("Recent update : " + str(data["recently_updated"])) 

1953 

1954 aujourdhui = datetime.datetime.now() 

1955 recently_updated_info = "recently_updated_days_" + str(last_number_days) + "_before_" + str( 

1956 aujourdhui.strftime("%Y_%m_%d_%H_%M")) + "_" + "_".join(list_repos).replace("/", "_") + "_voila" 

1957 text = data["recently_updated"] 

1958 

1959 data_for_retrieval.append( 

1960 {"id": recently_updated_info, "text": text, "url": "useless"}) # , "created_at":created_at}) 

1961 

1962 if not only_recent_update: 

1963 for issue in data["issues"]: 

1964 print(".") 

1965 id = issue["repo"] + "/" + str(issue["number"]) 

1966 text = issue["title"] + issue["concat_body"] 

1967 url = issue["link"] 

1968 # created_at = issue["updated_at"] 

1969 

1970 data_for_retrieval.append({"id": id, "text": text, "url": url}) # , "created_at":created_at}) 

1971 

1972 # source = item.get("source", None) 

1973 # source_id = item.get("source_id", None) 

1974 # created_at = item.get("created_at", None) 

1975 # author = item.get("author", None) 

1976 

1977 # "title":issue.title, 

1978 # "labels":from_label_to_map(issue.labels), 

1979 # "assignees":issue.assignees, 

1980 # "count_done":count_done, 

1981 # "count_to_do":count_to_do, 

1982 # "size_body":size_body, 

1983 # "size_total":size_total, 

1984 # "updated_at":issue.updated_at, 

1985 # "link":link, 

1986 # "concat_body":concat_body}) 

1987 

1988 import ast 

1989 json_data = ast.literal_eval(json.dumps(data_for_retrieval)) 

1990 if verbose: 

1991 print(json_data) 

1992 from lib.import_util.lib_path_to_vec import manual_dump_id_path_url 

1993 if not os.path.exists(os.path.dirname(out_path)): 

1994 os.makedirs(os.path.dirname(out_path)) 

1995 manual_dump_id_path_url(out_path + ".json", json_data) 

1996 # with open(out_path + '.json', 'w') as f: 

1997 # json.dump(str(json_data), f) 

1998 

1999 # f.write(json_data) 

2000 

2001 # data = "".join(f.readlines()) 

2002 # json_data = json.load(data) 

2003 # import ast 

2004 # json_data = ast.literal_eval(data) 

2005 

2006 # Maybe to try => but makes TypeError: Object of type datetime is not JSON serializable 

2007 # f.write(json.dumps(data)) 

2008 

2009 return out_path + ".json", recently_updated_info 

2010 else: 

2011 print("Not yet managing cache for issues to gpt retrieval") 

2012 return "", "" 

2013 

2014 

2015def append_comment(github_token, verbose=False, 

2016 message_comment="Just test", 

2017 OwnRepo="fotonower/Doc", 

2018 issue_number=188): # pragma no cover because for later v1 

2019 from github import Github 

2020 

2021 g = Github(github_token) 

2022 

2023 # Then get your repository 

2024 repo = g.get_repo(OwnRepo) 

2025 

2026 # Get the issue that you want to comment on 

2027 issue = repo.get_issue(number=issue_number) 

2028 

2029 # Add a comment to the issue 

2030 return issue.create_comment(message_comment) 

2031 

2032 

2033# Should we do it by issue with context ? 

2034class GitRulesIssues(): # pragma no cover 

2035 def __init__(self, list_rules, gpt_service, do_generate_title=False, check_comment_link=True): 

2036 self.sub_rules = [] 

2037 self.list_rules = list_rules 

2038 self.methodo_rules = "contains" # contains, regexp, nlp 

2039 self.gpt_service = gpt_service 

2040 self._uplink_new_line = "[up](#up)" 

2041 self._do_generate_title = do_generate_title 

2042 self._check_comment_link = True 

2043 

2044 def enforce_comment(self, comment, title_issue, body_issue=""): 

2045 

2046 print("Need to test the list of rules !") 

2047 

2048 has_title, has_uplink = self._has_title_and_uplink(comment) 

2049 

2050 new_body = comment.body 

2051 if not has_uplink: 

2052 print("MISSING : Missing UPLINK ") 

2053 new_body = self._uplink_new_line + "\n" + new_body 

2054 new_title = False 

2055 if not has_title: 

2056 print("MISSING : Missing TITLE ") 

2057 

2058 if not has_title and self._do_generate_title: 

2059 generated_title = self._generate_title(comment, title_issue) 

2060 new_body = self._uplink_new_line + new_body 

2061 new_body = "# " + generated_title + "\n" + new_body 

2062 new_title = True 

2063 if not has_uplink or new_title: 

2064 comment.edit(new_body) 

2065 print("Now need to insert !") 

2066 

2067 id_comment = comment.id 

2068 if self._check_comment_link: 

2069 if str(id_comment) not in body_issue: 

2070 print("MISSING : Missing link from body to comment : " + str(id_comment)) 

2071 return id_comment 

2072 

2073 return None 

2074 

2075 # - [ ] VR TODO 5-8-25 : je refais en faisant un split sur # puis sur \n => donc il faut le refacto tout cela 

2076 def _has_title_and_uplink(self, comment): 

2077 has_uplink = False 

2078 has_title = False 

2079 if self.methodo_rules == "contains": 

2080 body_parse = comment.body 

2081 # if body_parse.startswith(self._uplink_new_line): 

2082 if self._uplink_new_line + "\r\n" in body_parse or self._uplink_new_line + "\n" in body_parse: 

2083 has_uplink = True 

2084 if body_parse.startswith(self._uplink_new_line): 

2085 body_parse = body_parse.lstrip(self._uplink_new_line).lstrip("\r\n").lstrip("\n") 

2086 # body_parse[len(self._uplink_new_line):] 

2087 if body_parse.startswith("#"): 

2088 has_title = True 

2089 else: 

2090 print("Other methodo_rules not yet implemented !") 

2091 

2092 return has_title, has_uplink 

2093 

2094 def _generate_title(self, comment, title_issue): 

2095 request = "PLEASE PROPOSE A TITLE FOR THE FOLLOWING COMMENT OF THE ISSUE THAT NAMES IS " + title_issue + "\n" + comment.body 

2096 result, nb_token, model = self.gpt_service.completion(request) 

2097 

2098 print("result : " + result) 

2099 return result 

2100 

2101 

2102def git_smart_edit(list_repos, list_excluded_labels, github_token, 

2103 openai_token, 

2104 list_rules, 

2105 list_required_labels=[], 

2106 verbose=False, 

2107 with_comment=False, 

2108 load_cache=False, 

2109 with_closed_issue=False, 

2110 out_path='doctest-output/Fotonower_Issues.json', 

2111 load_all=False): # pragma no cover v1 

2112 print("TO TEST") 

2113 

2114 do_generate_title = False 

2115 

2116 print("List Rules to check or enforce : " + str(list_rules)) 

2117 

2118 from lib.lib_openai import OpenAIAPI # request_gpt 

2119 gpt_service = OpenAIAPI(openai_token) 

2120 

2121 git_rules = GitRulesIssues(list_rules, gpt_service, do_generate_title=do_generate_title) 

2122 

2123 import os 

2124 out_folder = os.path.dirname(out_path) 

2125 out_file = os.path.basename(out_path) 

2126 

2127 import json 

2128 if not load_cache: 

2129 check_issue_re = False 

2130 data, repo_name_with_cond = load_github_to_json_with_type(list_repos, list_excluded_labels, github_token, 

2131 verbose, with_comment=with_comment, check_issue_re=check_issue_re, 

2132 load_all=load_all, 

2133 list_required_labels=list_required_labels) 

2134 

2135 data_for_retrieval = [] 

2136 

2137 for issue in data["issues_data"]: 

2138 print(".") 

2139 id = issue["repo"] + "/" + str(issue["number"]) 

2140 text = issue["title"] + issue["concat_body"] 

2141 url = issue["link"] 

2142 

2143 list_missing_comment = [] 

2144 for c in issue["comments_data_or_git"]: 

2145 print("c : " + str(c)) 

2146 one_missing_comment = git_rules.enforce_comment(c, issue["title"], issue["concat_body"]) 

2147 if one_missing_comment != None: 

2148 list_missing_comment.append(one_missing_comment) 

2149 # new_body = "[up](#up)\n" + c.body 

2150 # c.edit(new_body) 

2151 

2152 # https://github.com/fotonower/projects/issues/374#issuecomment-1561219418 

2153 # 

2154 print("TO CHECK") 

2155 

2156 if len(list_missing_comment) > 0: 

2157 print(" MISSING : " + str(list_missing_comment)) 

2158 

2159 else: 

2160 print("Not yet managing cache for issues to smart_edit") 

2161 

2162 print("TO FINISH") 

2163 

2164 

2165import subprocess 

2166 

2167 

2168def subprocessCommand(command, timeout=10, verbose=False): # pragma no cover util 

2169 """ permet de faire appel a des commandes shell """ 

2170 proc = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True) 

2171 try: 

2172 outs, errs = proc.communicate(timeout=timeout) 

2173 if verbose: 

2174 print(str(command) + " Error : " + str(errs)) 

2175 print(" Output : " + str(outs)) 

2176 except subprocess.TimeoutExpired: 

2177 proc.kill() 

2178 outs, errs = proc.communicate() 

2179 if verbose: 

2180 print(str(outs) + " : " + str(errs)) 

2181 return outs 

2182 

2183 

2184def get_remote_repo_from_local_repo(timeout=40, verbose=False): # pragma no cover interface simplifé 

2185 command = "git remote -v" 

2186 # Expected 

2187 # origin git@github.com:fotonower/Safia.git(fetch) 

2188 # origin git@github.com:fotonower/Safia.git(push) 

2189 remote_repo_raw = subprocessCommand(command, timeout, verbose) 

2190 

2191 remote_repo = remote_repo_raw.decode() 

2192 remote_repo_lines = remote_repo.split("\n") 

2193 if len(remote_repo_lines) == 0: 

2194 return None 

2195 

2196 if verbose: 

2197 print(remote_repo_lines) 

2198 

2199 first_line_repo = remote_repo_lines[0] 

2200 first_line_repo = first_line_repo.replace("origin\tgit@github.com:", "") 

2201 first_line_repo = first_line_repo.replace(".git", "") 

2202 first_line_repo = first_line_repo.replace("(fetch)", "") 

2203 first_line_repo = first_line_repo.replace(" ", "") 

2204 

2205 if "/" not in first_line_repo: 

2206 print("Error !") 

2207 

2208 return first_line_repo 

2209 

2210 

2211 

2212import json 

2213from pydantic import BaseModel, ConfigDict, field_validator 

2214 

2215from typing import List 

2216 

2217 

2218class ATMODataFTN(BaseModel): 

2219 name: str 

2220 links: List = [] 

2221# links : List[dict[str, str]] = [] 

2222 content : str = "" 

2223 labels : List = [] # VR 5-8-25 : on peut mettre des labels vides 

2224# labels : List[dict[str, str]] = [] 

2225 type : str = "issue" 

2226 link : str = "" 

2227 title : str # Doit etre plus petit que 128 caractères (et c'est déjà trop) 

2228 assignee : List = [] 

2229# assignee : List[str] = [] 

2230 type : str = "" 

2231# updated_at : str = "" 

2232 # datetime) = datetime.datetime.now() 

2233# data : list[ATMODataFTN] 

2234 

2235 @field_validator("title") 

2236 @classmethod 

2237 def check_title_short(cls, title: str) -> str: 

2238 if len(title) > 96: 

2239 #raise ValueError("Employees must be at least 18 years old.") 

2240 print("WARNING title too long, we don't reduce it to avoid objection char : " + str(title)) 

2241 # title = title[:960] 

2242 if len(title) > 1048576: # VR TODO data just for test duplicate ca va pas le faire 

2243 print("WARNING title is too long 1048576 , we put a default value") 

2244 title = "We should add label OBJECTION" 

2245 # title = title[:256] 

2246 

2247 return title 

2248 

2249 

2250 

2251# parse_labels (and what about bolors ?) 

2252def parse_label_on_content_or_link(content_or_link_display, list_labels): 

2253 list_labels_found = [] 

2254 default_color = "123456" 

2255 for l in list_labels: 

2256 if l.upper() in content_or_link_display: 

2257 new_label = {"name" : l, "color" : default_color} 

2258 list_labels_found.append(new_label) 

2259 

2260 return list_labels_found 

2261 

2262 

2263 

2264# TO use or deprecate 

2265def parse_object_as_atmo_doc_func(content, 

2266 context_node_parent_name, 

2267 context_node_parent_updated_at, 

2268 regexp_search, 

2269 fields, 

2270 forced_name_object = None, 

2271 id_new_object = 0): 

2272 print(" Here copy voila du parse des chekxbo et qu'on va mettre à jour ovilà !") 

2273 return None 

2274 

2275 

2276def util_get_count_iterate(context_parent_node_and_count_subobj): 

2277 if context_parent_node_and_count_subobj == None or "count_childs" not in context_parent_node_and_count_subobj: 

2278 print("ERROR unexpected behavior, we could quit even if it is quite harsh !") 

2279 return "-1" 

2280 count = context_parent_node_and_count_subobj["count_childs"] 

2281 context_parent_node_and_count_subobj["count_childs"] += 1 

2282 return str(count) 

2283 

2284 

2285 

2286def convert_json_md_a_to_link(attrs_data, current_node_context = None, context_parent_node_and_count_subobj = {}, 

2287 list_labels_to_parse = [], 

2288 text_link = None): 

2289 import sys 

2290 sys.stdout.flush() 

2291 attrs = '' 

2292 href = '' 

2293 # vr we expect to loose the text 

2294 # text = data['text'] if 'text' in data else "" 

2295 print("Text from link lost, but need at least to parse for label on link ") 

2296 if True: 

2297 href = None 

2298 name = None 

2299 content = text_link if text_link != None else "" 

2300 

2301 if "href" in attrs_data: 

2302 href = attrs_data["href"] 

2303 complete_target = "" 

2304 if href.startswith("#"): # we consider a local link VR TODO CDC 15-8-25 : how can we decide the context of relative linking ? This depends on github issue indeed ! Do we want to have these name or our name with comment flat ? 

2305 print("STRAT NAMING TO BE CORRECLTY DEFINED ! ") 

2306 grand_parent_node_name = context_parent_node_and_count_subobj["grand_parent_node_name"] if context_parent_node_and_count_subobj != None and "grand_parent_node_name" in context_parent_node_and_count_subobj else "DEFAULT_GRAND_PARENT_NODE_NAME_BECAUSE_MISSING" 

2307 complete_target = grand_parent_node_name + "/" + href.lstrip("#") 

2308 if current_node_context != None and "name" in current_node_context and "/" in current_node_context["name"]: 

2309 complete_target = "/".join((current_node_context["name"].split("/")[:-1])) + "/" + href.lstrip("#") 

2310 if current_node_context != None and "name" in current_node_context and "#" in current_node_context["name"]: 

2311 complete_target = "#".join((current_node_context["name"].split("#")[:-1])) + "#" + href.lstrip("#") 

2312 else : 

2313 complete_target = href 

2314 type_or_label = "link" 

2315 print("TODO use text maybe 15-8-25") 

2316 list_label_found = parse_label_on_content_or_link(content, list_labels_to_parse) 

2317 content_without_label = content 

2318 for l in list_label_found: 

2319 content_without_label = content_without_label.replace(l["name"], "").strip() 

2320 if content_without_label != "": 

2321 print("OBJECTION WARNING we loose stuff ! We could use the content without label : " + str(content_without_label)) 

2322 # content = content_without_label 

2323 if current_node_context != None: 

2324 print("OBJECTION ERROR DEV WARNING WE loose data and don't report (but we could) it unexpected case we cannot add OBJECTION label ! " + str(content) + " " + str(content_without_label)) 

2325# current_node_context["labels"].append({"name": "OBJECTION", "color": "FF0000"}) 

2326 else: 

2327 print("OBJECTION ERROR WE loose data and don't report it unexpected case we cannot add OBJECTION label ! " + str(content) + " " + str(content_without_label)) 

2328 

2329 if len(list_label_found) > 0: 

2330 type_or_label = list_label_found[0]["name"] 

2331 default_link_label_hierarchical = "info" # VR TODO REFACTO DEV parse_label of link at this point ! 

2332 data_atmo = {"type": type_or_label, # useless 

2333 "label": default_link_label_hierarchical, 

2334 "target": complete_target, 

2335 "content": content} # useless as well 

2336 new_link = {"label" : type_or_label, "target" : complete_target} 

2337 if current_node_context != None: 

2338 if "links" not in current_node_context: 

2339 current_node_context["links"] = [] 

2340 current_node_context["links"].append(new_link) 

2341 return None 

2342 elif "name" in attrs_data: 

2343 name = attrs_data["name"] 

2344 type_or_label = "a_name" 

2345 content = attrs_data["text"] if "text" in attrs_data else content 

2346 data_atmo = {"type": type_or_label, # useless 

2347 "name": name, 

2348 "content": content} # useless as well 

2349 print("We will need to rename") 

2350 if current_node_context != None and "name" in current_node_context: 

2351 if "#" not in current_node_context["name"]: 

2352 print("# Not sure what to do") 

2353 if "/" not in current_node_context["name"]: 

2354 print("Not sur what to do why we are in this state") 

2355 else : # STRAT NAMING relative to comment 

2356 new_name_thanks_ref = "/".join(current_node_context["name"].split("/")[:-1]) + "/" + name 

2357 print("a_name RENAMING " + (current_node_context["name"].split("/")[-1]) + " TO " + name) 

2358 current_node_context["name"] = new_name_thanks_ref 

2359 else: # STRAT NAMING relative to issue 

2360 if current_node_context["type"] == "comment" and "issuecomment" not in current_node_context["name"]: 

2361 print("We do not rename because we are in a comment and it has been already renamed") 

2362 else: 

2363 new_name_thanks_ref = "#".join(current_node_context["name"].split("#")[:-1]) + "#" + name 

2364 print("a_name RENAMING " + (current_node_context["name"].split("#")[-1]) + " TO " + name) 

2365 print("VR TODO verify, je ne suis pas vraiment sur mais il me semble qu'il faut aussi changer le parent_node_name car les string ne sont pas des references contraitement aux dict dans python") 

2366 if "parent_node_name" in context_parent_node_and_count_subobj: 

2367 context_parent_node_and_count_subobj["parent_node_name"] = new_name_thanks_ref 

2368 if "luca_name" in context_parent_node_and_count_subobj and current_node_context["name"] == context_parent_node_and_count_subobj["luca_name"]: 

2369 context_parent_node_and_count_subobj["luca_name"] = new_name_thanks_ref 

2370 current_node_context["name"] = new_name_thanks_ref 

2371 

2372 # Test 14-8-25 

2373# grand_parent_node_name = context_parent_node_and_count_subobj["grand_parent_node_name"] if context_parent_node_and_count_subobj != None and "grand_parent_node_name" in context_parent_node_and_count_subobj else "DEFAULT_GRAND_PARENT_NODE_NAME_BECAUSE_MISSING" 

2374# current_node_context["name"] = grand_parent_node_name + "/" + name 

2375# print("- [ ] VR TODO 15-8-25 : RENAMING, we need to have convention in a lot of case, for example we need the grand-parent ! ") 

2376 

2377 # we don't return the ref_name_a if it was used 

2378 return None 

2379 else : 

2380 print(" ERROR unexpected case NOT SURE what to do ! ") 

2381 

2382 else : 

2383 print("WARNING DATA LOST unexpected a not href or name : incorrectly parsed, format link unexpected : " + str(attrs_data)) 

2384 return None 

2385 

2386 

2387 return data_atmo 

2388 

2389# VR 15-8-25 : starting this in order to get back to the correct behavior of the checkbox and remove the former code of parsing 

2390# VR 15-8-25 : maybe we could start with a refacto ! 

2391def get_todone_or_tic_thanks_re(complete_content): 

2392 type_and_label = "" 

2393 content = "" 

2394 return content, type_and_label 

2395 

2396 

2397 

2398# VR 15-8-25 : CDC how to manage color of label (same pb as cache label to list ? grrr grrr) 

2399def append_label_to_checkbox(complete_content, list_labels): 

2400 labels_found = [] 

2401 type_tic_or_todone = "" 

2402 content = "" 

2403 return content, type_tic_or_todone 

2404 

2405 

2406 

2407def convert_json_md_a_to_cb_tic(data, 

2408 config_assoc_list_to_atmo ={}, 

2409 option_flat_or_tree = "flat", 

2410 context_parent_node_and_count_subobj = {}, 

2411 list_labels_to_parse = [], 

2412 verbose=False): 

2413 

2414 list_data_atmo = [] 

2415 if type(data) == list and len(data) > 0: 

2416 # if type(data[0]) == str: 

2417 for d in data: 

2418 if type(d) == str: 

2419 if d == None: 

2420 print("warn remove one None data") 

2421 continue 

2422 labels = [] 

2423 

2424 content = d 

2425 type_and_label = "tic" 

2426 import re 

2427 try: 

2428 matches = re.match(r"(\[[x|X| ]\])?(.+)", d) 

2429 except Exception as e: 

2430 print("WRONG PARSING CONTINUE TIC OR TOE CHECKBOX " + str(e) + " " + str(d)) 

2431 type_and_label = "todone" 

2432 continue 

2433 group_match = matches.groups() 

2434 if type(group_match) == tuple and len(group_match) == 2: 

2435 if group_match[0] == None: 

2436 type_and_label = "tic" 

2437 content = d 

2438 else: 

2439 content = group_match[1] 

2440 

2441 if group_match[0].lower() == "[x]": 

2442 type_and_label = "done" 

2443 elif group_match[0].lower() == "[ ]": 

2444 type_and_label = "todo" 

2445 else : 

2446 type_and_label = "todone" 

2447 print("Warning incorrect parsing of CB : " + str(d)) 

2448 

2449 else : 

2450 type_and_label = "tic" 

2451 print("WARNING checkbox with many checkbox or WTF ??!?!") 

2452 

2453 if type_and_label == 'tic': 

2454 type_tic_or_todone = "tic" 

2455 label_todone = "tic" 

2456 color_todone = "ffffff" 

2457 elif type_and_label == "todo": 

2458 type_tic_or_todone = "checkbox" 

2459 label_todone = "todo" 

2460 color_todone = "f46aff" 

2461 elif type_and_label == "done": 

2462 type_tic_or_todone = "checkbox" 

2463 label_todone = "done" 

2464 color_todone = "20fe32" 

2465 elif type_and_label == "todone": 

2466 print("ERROR treated as warning in detecting type_and_label from todone_or_tic Not really expected") 

2467 type_tic_or_todone = "todone" 

2468 label_todone = "todone" 

2469 color_todone = "ffffff" 

2470 else : 

2471 type_tic_or_todone = "ticb" # or todone ? 

2472 color_todone = "7d5c96" 

2473 label = "undefined" 

2474 print("ERROR treated as WARNING, unexpected type_and_label : " + str(type_and_label)) 

2475 

2476 labels = [{"name" : label_todone, "color" : color_todone}] 

2477 list_labels_found = parse_label_on_content_or_link(content, list_labels_to_parse) 

2478 labels.extend(list_labels_found) 

2479 data_atmo = {"name" : "DEFAULT_NAME_WHILE_ADDING_COUNT","type" : type_tic_or_todone, "content" : content, "labels" : labels} 

2480 parent_node_name = context_parent_node_and_count_subobj["parent_node_name"] if context_parent_node_and_count_subobj != None and "parent_node_name" in context_parent_node_and_count_subobj else "DEFAULT_PARENT_NODE_NAME_MISSING" 

2481 data_atmo["name"] = parent_node_name + "/" + type_tic_or_todone.upper() + util_get_count_iterate(context_parent_node_and_count_subobj) 

2482 data_atmo["title"] = content 

2483 data_atmo["links"] = [{"label":"hierarchical", "target" : context_parent_node_and_count_subobj["luca_name"]}] 

2484 

2485 if "luca_content" in context_parent_node_and_count_subobj: 

2486 content_this_comment = context_parent_node_and_count_subobj["luca_content"] 

2487 info_edit_atmo = compute_info_edit_atmo(content_this_comment, content) 

2488 data_atmo["info_edit_atmo"] = info_edit_atmo 

2489 

2490 list_data_atmo.append(data_atmo) 

2491 elif d == None: 

2492 continue 

2493 else: 

2494 print("Iteration over the rest ") 

2495 # Despite the flat option we pass at the next level 

2496# context_parent_node_and_count_subobj["grand_parent_node_name"] = context_parent_node_and_count_subobj["parent_node_name"] 

2497# current_obj_atmo_used_for_rename = None 

2498# if len(list_data_atmo) > 0: 

2499# context_parent_node_and_count_subobj["parent_node_name"] = list_data_atmo[-1]["name"] 

2500# current_obj_atmo_used_for_rename = list_data_atmo[-1] 

2501# if len(list_data_atmo) > 1: 

2502# # print("IF we have a name inside this, this is an ERROR UNEXPECTED TREATED AS WARNING ! We have trouble here, we don't know what to do with this !") 

2503# current_obj_atmo_used_for_rename = None 

2504 collected_object_output = decrit_rec_md_bs_json_tree(d, 

2505 list_data_atmo, # current_obj_atmo_used_for_rename VR TODO REFACTO rename collected_object_output_local or at this level 

2506 current_obj_atmo=None, # J'ai bien envie de mettre le data_atmo ici, et les cas ou il y en a plusieurs ou verra, de toute facon on aurait pas du etre là car cela aurait du etre géré avant, vraiment ? 

2507 config_assoc_list_to_atmo=config_assoc_list_to_atmo, 

2508 option_flat_or_tree=option_flat_or_tree, 

2509 context_parent_node_and_count_subobj = context_parent_node_and_count_subobj, 

2510 list_labels_to_parse=list_labels_to_parse, 

2511 verbose = verbose) 

2512 list_data_atmo.extend(collected_object_output) 

2513 list_data_atmo = make_unique(list_data_atmo) 

2514# del context_parent_node_and_count_subobj["grand_parent_node_name"] 

2515 else : 

2516# print("It seems we are at the end of the for loop : ") 

2517# print("ALWAYS TRUE OR ERROR : " + str(d == data[-1])) 

2518 if (d != data[-1]): 

2519 print("ERROR exiting !") 

2520 exit(1666) 

2521 # TO remove on 18-8-25 print("VR 15-8-25 : how can a else be after a for ??? We observed two new caes, don't know if it is this end, and the first case is not yet debug anyway !") 

2522 else: 

2523 print("Unexpected type data") 

2524 print(" We observed different cases, don't know if it is the last one, and code in test on 08-25 !") 

2525 return [] 

2526 

2527 # data_atmo = {"type" : "tic", "content" : str(data)} 

2528 return list_data_atmo 

2529 

2530 

2531 

2532def convert_json_md_a_to_par(data, context_parent_node_and_count_subobj): 

2533 

2534 name = context_parent_node_and_count_subobj["parent_node_name"] + "/PAR" + str(util_get_count_iterate(context_parent_node_and_count_subobj)) 

2535 labels = [{"name":"paragraph", "color" : "ff1122"}] 

2536 data_atmo = {"type" : "paragraph", "content" : str(data), "name" : name, "title" : str(data), "labels" : labels} 

2537 

2538 luca_name = context_parent_node_and_count_subobj["luca_name"] if context_parent_node_and_count_subobj != None and "luca_name" in context_parent_node_and_count_subobj else "DEFAULT_LUCA_NAME_MISSING" 

2539 

2540 data_atmo["links"] = [{"label":"hierarchical", "target" : luca_name}] 

2541 return data_atmo 

2542 

2543def make_unique(collected_object_output): 

2544 map_collected_object_output = {d["name"] : d for d in collected_object_output} 

2545 

2546 collected_object_output = list(map_collected_object_output.values()) 

2547 return collected_object_output 

2548 

2549def decrit_rec_md_bs_json_tree(input_data, 

2550collected_object_output, 

2551 current_obj_atmo = None, 

2552 config_assoc_list_to_atmo ={}, 

2553 option_flat_or_tree = "flat", 

2554 context_parent_node_and_count_subobj = {}, # VR TODO duplicate with current_obj_atmo but containing also the count of sub ojbect of a comment (or of something els if we activate option_flat_or_tree to tree) 

2555 list_labels_to_parse = [], 

2556 verbose = False): 

2557 

2558 

2559 print_mem() 

2560 local_vars = list(locals().items()) 

2561 if verbose: 

2562 for var, obj in local_vars: 

2563 print(var, sys.getsizeof(obj)) 

2564 

2565 

2566 if len(collected_object_output) > 1000000: 

2567 print(len(collected_object_output)) 

2568 

2569 import bs4 

2570 if type(input_data) == list: 

2571 for d in input_data : 

2572 if type(d) == dict and "attrs" in d: 

2573 if len(set(d.keys()) - set(["attrs", "text"])) > 0: 

2574 print("Unexpected case we loose data") 

2575 text_link = d["text"] if "text" in d else None 

2576 attrs_link = d["attrs"] 

2577 res_link = convert_json_md_a_to_link(attrs_link, current_obj_atmo, 

2578 context_parent_node_and_count_subobj, 

2579 list_labels_to_parse=list_labels_to_parse, 

2580 text_link = text_link) 

2581 if res_link != None: 

2582 print("ERROR refacto convert_json_md_a_to_link is not complete !") 

2583 else: 

2584 collected_object_output = decrit_rec_md_bs_json_tree(d, collected_object_output, 

2585 current_obj_atmo, config_assoc_list_to_atmo, 

2586 option_flat_or_tree, 

2587 context_parent_node_and_count_subobj = context_parent_node_and_count_subobj, 

2588 list_labels_to_parse = list_labels_to_parse, 

2589 verbose = verbose) 

2590 if len(collected_object_output) > 0 : 

2591 current_obj_atmo = collected_object_output[-1] 

2592 elif type(input_data) == dict : # or type(input_data) == bs4.element.XMLAttributeDict: 

2593 for k in input_data: 

2594 if k in config_assoc_list_to_atmo: 

2595 if k == 'a' and type(input_data[k]) == dict and 'attrs' in input_data[k]: 

2596 text_link = input_data[k]["text"] if "text" in input_data[k] else None 

2597 attrs_link = input_data[k]["attrs"] 

2598 context_parent_node_and_count_subobj["current_obj_atmo"] = current_obj_atmo 

2599 res_link = convert_json_md_a_to_link(attrs_link, current_obj_atmo, context_parent_node_and_count_subobj, 

2600 list_labels_to_parse = list_labels_to_parse, 

2601 text_link = text_link) 

2602 if current_obj_atmo == None or res_link == None: 

2603 print(" Link was already treated !") 

2604 else: 

2605 print("WARNING : we shouldn't be there since link is not already treated") 

2606 if "links" not in current_obj_atmo: 

2607 current_obj_atmo["links"] = [] 

2608 if "type" in res_link and "href" == res_link["type"]: 

2609 current_obj_atmo["links"].append(res_link) 

2610 elif k == 'a' and type(input_data[k]) == dict and 'attrs' not in input_data[k]: 

2611 print("ERROR UNEXPECTED PARSING GENERIC CASE") 

2612 elif type(input_data[k]) == dict and "attrs" in input_data[k]: 

2613 print("ERROR On n paasse jamais ici, c'est un cas ou on est dans depuis une lise") 

2614# if len(set(list(input_data[k].keys())) - set(["attrs", "text"])) > 0: 

2615 if len(set(input_data[k].keys()) - set(["attrs", "text"])) > 0: 

2616 print("WARNING OBJECTION Unexpected case we loose data") 

2617 if len(collected_object_output) > 0: 

2618 if current_obj_atmo != None: 

2619 print("Maybe we should do something in this ! voila !") 

2620 collected_object_output[-1]["labels"].append({"name" : "OBJECTION", "color" : "ff0000"}) 

2621 else: 

2622 print("ERROR OBJECTION unexpected case no object to inform !") 

2623 

2624 text_link = input_data[k]["text"] if "text" in input_data[k] else None 

2625 attrs_link = input_data[k]["attrs"] 

2626 res_link = convert_json_md_a_to_link(attrs_link, current_obj_atmo, 

2627 context_parent_node_and_count_subobj, 

2628 list_labels_to_parse=list_labels_to_parse, 

2629 text_link = text_link) 

2630 elif k == 'li' and type(input_data[k]) == list and len(input_data[k]) > 0 and type(input_data[k][0]) == str: 

2631 list_data_atmo = convert_json_md_a_to_cb_tic(input_data[k], config_assoc_list_to_atmo = config_assoc_list_to_atmo, 

2632 option_flat_or_tree=option_flat_or_tree, 

2633 context_parent_node_and_count_subobj = context_parent_node_and_count_subobj, 

2634 list_labels_to_parse = list_labels_to_parse, 

2635 verbose=verbose) 

2636 collected_object_output.extend(list_data_atmo) 

2637 collected_object_output = make_unique(collected_object_output) 

2638 # current_obj_atmo = res_cb_tic 

2639 current_obj_atmo = collected_object_output[-1] 

2640# elif k == 'h1': 

2641# print("VR pour l'instant je crois qu'on ne passe pas souvent là, mais de fait on voit les paragraph détaché !") 

2642# res_par = convert_json_md_a_to_par(input_data[k], context_parent_node_and_count_subobj) 

2643# collected_object_output.append(res_par) 

2644# current_obj_atmo = res_par 

2645 elif k == 'p' and type(input_data[k]) == str: 

2646 list_data_atmo = convert_json_md_a_to_cb_tic([input_data[k]], 

2647 config_assoc_list_to_atmo = config_assoc_list_to_atmo, 

2648 option_flat_or_tree=option_flat_or_tree, 

2649 context_parent_node_and_count_subobj = context_parent_node_and_count_subobj, 

2650 list_labels_to_parse = list_labels_to_parse) 

2651 collected_object_output.extend(list_data_atmo) 

2652 collected_object_output = make_unique(collected_object_output) 

2653 # current_obj_atmo = res_cb_tic 

2654 current_obj_atmo = collected_object_output[-1] # Sans doute useless dans ce cat 

2655 context_parent_node_and_count_subobj["current_obj_atmo"] = current_obj_atmo 

2656 else: 

2657 collected_object_output = decrit_rec_md_bs_json_tree(input_data[k], collected_object_output, 

2658 current_obj_atmo, config_assoc_list_to_atmo, 

2659 option_flat_or_tree, 

2660 context_parent_node_and_count_subobj = context_parent_node_and_count_subobj, 

2661 list_labels_to_parse=list_labels_to_parse, 

2662 verbose = verbose) 

2663 

2664 else : 

2665 print(" We may loose a bit of data and should add the label OBJECTION ! :-) k " + str(k)) 

2666 

2667 if k not in ["body", "html", "ul", "text"]: 

2668 print(" WARNING unexpected key in dict that is not parsed : " + str(k) + " type : " + str(type(input_data[k]))) 

2669 if len(collected_object_output) > 0: 

2670 if current_obj_atmo != None: 

2671 print("Maybe we should do something in this ! voila !") 

2672 collected_object_output[-1]["labels"].append({"name" : "OBJECTION", "color" : "ff0000"}) 

2673 else: 

2674 print("ERROR unexpected case no object to inform !") 

2675 

2676 collected_object_output = decrit_rec_md_bs_json_tree(input_data[k], 

2677 collected_object_output, 

2678 current_obj_atmo, config_assoc_list_to_atmo, 

2679 option_flat_or_tree, 

2680 context_parent_node_and_count_subobj = context_parent_node_and_count_subobj, 

2681 list_labels_to_parse=list_labels_to_parse, 

2682 verbose = verbose) 

2683 if len(collected_object_output) > 0: 

2684 current_obj_atmo = collected_object_output[-1] 

2685 elif type(input_data) == str: 

2686 list_data_atmo = convert_json_md_a_to_cb_tic([input_data], 

2687 config_assoc_list_to_atmo=config_assoc_list_to_atmo, 

2688 option_flat_or_tree=option_flat_or_tree, 

2689 context_parent_node_and_count_subobj=context_parent_node_and_count_subobj, 

2690 list_labels_to_parse=list_labels_to_parse) 

2691 collected_object_output.extend(list_data_atmo) 

2692 collected_object_output = make_unique(collected_object_output) 

2693 # current_obj_atmo = res_cb_tic 

2694 current_obj_atmo = collected_object_output[-1] # Sans doute useless dans ce cat 

2695 context_parent_node_and_count_subobj["current_obj_atmo"] = current_obj_atmo 

2696 else : 

2697 print(" WARNING OBJECTION MISSING DATA unexpected type input_data : " + str(type(input_data))) 

2698 if len(collected_object_output) > 0: 

2699 collected_object_output[-1]["labels"].append({"name" : "OBJECTION", "color" : "ff0000"}) 

2700 else: 

2701 print("ERROR OBJECTION WARNING unexpected type input_data and no object to inform ! : " + str(type(input_data))) 

2702 print("MISSING DATA : " + str(input_data)) 

2703 

2704 return collected_object_output 

2705 

2706 

2707# This function 

2708# There is some doc in the comment in and around it 

2709# - [ ] VR TODO 15-8-25 : consolidate the doc in the expected document, currently in https://github.com/fotonower/BETATMO/issues/2#issuecomment-3137936894 

2710# - [ ] VR TODO 15-8-25 : generalize parse_checkbox_as_atmo_doc_func to parse_object_as_atmo_doc_func with different type of object that couldn't be the same by the way ! 

2711# - [ ] VR OUTPUT unparsed content 

2712# - [ ] CDC parse_object_as_atmo_doc_func and parse_checkbox_as_atmo_doc_func : should we parse line by line (this must be more efficient, argh or simpler than keeping the unparsed line, and furthermore there are some stuff like details or code_example that are multi (line) but we don't want to parse it for now) 

2713# - [ ] inheritance of labels from parents will be done later (not like in checkbox parsing) 

2714# - [ ] Veut-on différencier les internal des externals links ? 

2715# - [ ] VR TODO 15-8-25 : refacto links parsing 

2716# - [ ] doc devops pip3.10 install bs2json pip3.10 install marko pip3.10 install bs4 

2717# [ ] VR CDC faut il passer le contexte dans decrit_rec_md_bs_json_tree notament pour les liens ? 

2718def parse_comment_with_config_parse(comment, 

2719 context_node_parent = None, 

2720 config_parse = {"objects" : ["checkbox", "tic", "md_paragraph", "a_name", 

2721 "implicit_hierarchical_link"], 

2722 "options": ["link", "label_on_object", "label_on_link"]}, 

2723 list_labels_to_parse = ["DEV", "DOC", "BUG", "PLAN", "DONE"], 

2724 verbose = False) : # PLAN or CDC 

2725 

2726 import marko 

2727 res_parse_marko = marko.convert(comment) 

2728 

2729 

2730 if verbose: 

2731 print(res_parse_marko) 

2732 from bs4 import BeautifulSoup 

2733 res_parse_bs = BeautifulSoup(res_parse_marko, 'xml') 

2734 res_parse_bs = BeautifulSoup(res_parse_marko, 'lxml') 

2735 

2736 if verbose: 

2737 print(res_parse_bs) 

2738 

2739 from bs2json import BS2Json 

2740 

2741# html = '<html><head><title>Page Title</title></head><body><h1>My First Heading</h1><p>My first paragraph.</p></body></html>' 

2742 bs2json = BS2Json(res_parse_bs) 

2743 

2744 # Convert soup to JSON 

2745 try : 

2746 res_parse_bs_to_json = bs2json.convert() 

2747 except Exception as e: 

2748 print("TROP GROS SANS DOUTE") 

2749 print(str(e)) 

2750 res_parse_bs_to_json = {} 

2751 

2752 # Save JSON to file 

2753# bs2json.save() 

2754 

2755 # Print prettified output 

2756 if verbose: 

2757 bs2json.prettify() 

2758 

2759 very_chiant_two_new_cases_li = {'html': {'body': {'p': {'a': {'attrs': {'href': '#top'}, 'text': 'up'}}, 

2760 'h1': ['Amélioration du rangement', 'Rangement récurrent'], 'ul': [{'li': {'text': '[ ]', 'a': { 

2761 'attrs': { 

2762 'href': 'https://docs.google.com/spreadsheets/d/1xZ06L4OkmIVjgg-vvq-XiJSPmYOwIZNsvdb-miq3GFE/edit?gid=327753039#gid=327753039'}, 

2763 'text': 'Typologie_inventaire_matos FTN'}}}, {'li': [{'p': { 

2764 'a': {'attrs': {'href': 'https://github.com/fotonower/BETATMO/issues/5'}, 

2765 'text': 'Lié au taches domestiques'}}}, { 

2766 'p': "[ ] Rangemeent recurrent table cuisine, comment l'organiser dans rangement ou dans cuisine ??"}]}]}}} 

2767 

2768 if verbose: 

2769 print(res_parse_bs_to_json) 

2770 

2771 collected_object_output = [] 

2772 context_parent_node_and_count_subobj = {"parent_node_name" : context_node_parent["name"] if context_node_parent != None and "name" in context_node_parent else "DEFAULT_PARENT_NODE_NAME_BEECAUSE_MISSING", 

2773 "luca_name": context_node_parent[ 

2774 "name"] if context_node_parent != None and "name" in context_node_parent else "DEFAULT_ONLY_ANCESTOR_NODE_FOR_FLAT_CASE", 

2775 "count_childs" : 0} 

2776 if context_node_parent != None and "body" in context_node_parent: 

2777 context_parent_node_and_count_subobj["luca_content"] = context_node_parent["body"] 

2778 

2779 collected_object_output = decrit_rec_md_bs_json_tree(res_parse_bs_to_json, 

2780 collected_object_output, 

2781 context_node_parent, 

2782 config_assoc_list_to_atmo={"a": "link", 

2783 "li": "cb", 

2784 "h1": "paragraph", 

2785 "h2": "sparagraph", 

2786 "h3": "ssparagraph", 

2787 "h4": "sssparagraph", 

2788 "p" : "only_with_string", 

2789 "attrs" : "link"}, 

2790 option_flat_or_tree="flat", 

2791 context_parent_node_and_count_subobj = context_parent_node_and_count_subobj, 

2792 list_labels_to_parse = list_labels_to_parse, 

2793 verbose = verbose) 

2794 

2795 if verbose: 

2796 print(collected_object_output) 

2797 

2798 

2799 # - [ ] VR tODO after debug Y'a pu qu'à rajouter les labels sur les liens, les labels sur les objets 

2800 # - [ ] Et puis gérer les a name 

2801 

2802 # expected info from context node 

2803 list_keys_expected_context = ["father_node_name", "source"] 

2804 # father_node_name will be commend node name for example 

2805 # source can be git or atmo (not used on 15-8-25) 

2806 

2807 list_config_parse_option_expected = ["checkbox", "tic", "md_paragraph", "a_name", 

2808 "implicit_hierarchical_link", 

2809 "link", "label_on_object", "label_on_link", 

2810 "details", "code_quote", "source_code_reference"] # as of 15-8-25 we don't want to parse this 

2811 

2812 

2813 regexp_search_diff_tic_cb = r"\[([x| ]?)\](.+)\n" 

2814 

2815 

2816 

2817 # tic_and_cb 

2818 # "label_on_object", "label_on_link" 

2819 

2820 

2821 # "link", "label_on_object", "label_on_link" 

2822 

2823 

2824 

2825 # - [ ] 15-8-25 vr todo lister les conversions de md_bs_json vers atmo ! eh he eh ! genre 

2826 # a => link 

2827 # p => CB or 

2828 # h1.... => PAR 

2829 # confusion entre a et link par ce process 

2830 # comment fais t'on pour associer les a aux bons objets ? Et les link interne, sont ils gérés ? 

2831 # => on refais un 'parsing simple', ou est-ce qu'on prend plutot des a name sur la meme ligne ? Oh ho ! => on dirait 

2832 

2833 # optino flat et optino tree et peut-etre d'autres à venir ! 

2834 

2835 return collected_object_output 

2836 

2837map_config_bijection_root_level = { 

2838 "default" : {"issue" : "global_git", 

2839 "comment" : "local"}, 

2840 "atmo" : {"issue" : "loca_atmol", 

2841 "comment" : "local"} 

2842} 

2843 

2844map_todo_voila = { 

2845 "a": "link", 

2846 "p": "cb", 

2847 "h1": "paragraph", 

2848 "h2": "sparagraph", 

2849 "h3": "ssparagraph", 

2850 "h4": "sssparagraph" 

2851} 

2852 

2853def check_name_is_default(name, repo = "BETATMO", parent_comment = ""): 

2854 

2855 if len(name.split("/")) == 3: 

2856 return True 

2857 else: 

2858 return False 

2859 

2860 import re 

2861 f = re.match(r"{repo}\/(\s+)\#(\s*)\/{tic|cb|par}(\n+)", name.lower()) 

2862 if (f): 

2863 print("WARNING default name detected, we should rename it : " + str(name) + " parent comment : " + str(parent_comment)) 

2864 return True 

2865 

2866def get_type_from_label(labels): 

2867 map_managed_type_prefix = {"tic" : "- ", 

2868 "checkbox" : "- [ ]", 

2869 "done" : "- [x]", 

2870 "todo" : "- [ ]", 

2871 "paragraph" : "# "} 

2872 for l in labels: 

2873 if "name" in l: 

2874 label = l["name"] 

2875 if label in map_managed_type_prefix: 

2876 return map_managed_type_prefix[label] 

2877 return "" 

2878 

2879 

2880def build_comment_content_from_atmo_data(list_data_atmo_one_comment, 

2881 context_node_parent_name = None, 

2882grand_parent_node_name = None, 

2883 config_export = {"root_level" : "default", 

2884 "with_up_link" : True}): 

2885 print("VR TODO 15-8-25 : build_comment_content_from_atmo_data not yet implemented") 

2886 

2887 with_up_link = config_export["with_up_link"] if "with_up_link" in config_export else False 

2888 

2889 count_objection = 0 

2890 

2891 content_comment_mld = "" 

2892 if with_up_link: 

2893 content_comment_mld += "[up](#top)\n\n" 

2894 

2895 if context_node_parent_name != None: 

2896 # match {grand_parent_node_name}#issuecomment-commentid 

2897 if grand_parent_node_name != None: 

2898 if context_node_parent_name.startswith(grand_parent_node_name + "#issuecomment-"): 

2899 context_node_parent_name = context_node_parent_name.replace(grand_parent_node_name + "#issuecomment-", "") 

2900 if not context_node_parent_name.isdecimal(): 

2901 print("ERROR unexpected case for context_node_parent_name : " + str(context_node_parent_name)) 

2902 exit(1667) 

2903 elif context_node_parent_name.startswith(grand_parent_node_name + "#issue"): 

2904 print("On ne s'en occupe pas for now de cette verif") 

2905 elif context_node_parent_name.startswith(grand_parent_node_name + "#"): 

2906 print("We had relative naming") 

2907 content_comment_mld += "<a name=" + context_node_parent_name.replace(grand_parent_node_name + "#", "") + " ></a>\n\n" 

2908 else : 

2909 print("This is really unexpected !") 

2910 

2911 

2912 begin_char = len(content_comment_mld) 

2913 

2914 # Loop over the list of data atmo for one comment 

2915 for d in list_data_atmo_one_comment: 

2916 if issue_atmo_has_label(d, "OBJECTION"): 

2917 print("SKIP OBJECTION AND Can'T " + str(d)) 

2918 count_objection += 1 

2919# continue 

2920 type_data_prefix = get_type_from_label(d["labels"]) if "labels" in d and type(d["labels"]) == list else None 

2921 if type_data_prefix == None: 

2922 print("WARNING unhandled type in build_comment_content_from_atmo_data : " + str(d)) 

2923 continue 

2924 content_node = d["title"] if "title" in d else "" 

2925 

2926 links_str = "" 

2927 if "links" in d and type(d["links"]) == list and len(d["links"]) > 0: 

2928 print("OBJECTION TO TREAT " + str(d)) 

2929 for l in d["links"]: 

2930 if l["label"].lower() != "hierarchical": 

2931 # if links_str != "": 

2932 links_str += " " 

2933 target_href = l["target"] 

2934 if grand_parent_node_name != None and not target_href.startswith("http") and target_href.startswith(grand_parent_node_name): 

2935 target_href = target_href.replace(grand_parent_node_name, "") 

2936 links_str += "[" + l["label"].upper() + "](" + target_href + ")" 

2937 # count_objection += 1 

2938 potential_a_name = "" 

2939 if "name" in d: 

2940 if not check_name_is_default(d["name"]): 

2941 print("OBJECTION TO TREAT MAIS CA VA ETRE FACILE " + str(d)) 

2942# count_objection += 1 

2943 d_name = d["name"] 

2944 if grand_parent_node_name != None and d_name.startswith(grand_parent_node_name): 

2945 d_name = d_name.replace(grand_parent_node_name + "#", "") 

2946 potential_a_name = f" <a name={d_name}></a>" 

2947 else: 

2948 print("Verify it is default !?!") 

2949 

2950 this_content = type_data_prefix + content_node + links_str + potential_a_name + "\n" 

2951 

2952# info_edit_atmo = {"begin_char": begin_char, "end_char": end_char, "len_content": len_content} 

2953 end_char = begin_char + len(this_content) 

2954 

2955 d["info_edit_atmo"] = {"begin_char": begin_char, "end_char": end_char, "len_content": len(this_content)} 

2956 begin_char = end_char 

2957 

2958 content_comment_mld += this_content 

2959 

2960 # Les passages à la ligne, c'est pas top ! 

2961 

2962 # PB : longueur, certains carachètre spécial (durant le premier test =>) 

2963 # IDée : rajouter un label : NOBIJ ou OBJECTION => ca c'est bien ! 

2964 # Il y a une priorité de labels 

2965 # - [ ] Gestion des name => comment 

2966 # - [v] Gestion des links 

2967 # - [ ] Rajout des labels sur les liens 

2968 # - [ ] Gestion des (sub) paragraphes => en objection 

2969 # - [ ] Gestion des détails => non 

2970 # - [ ] Gestion des codes quotes => non 

2971 # - [x] Clairement le link up n'est pas géré => et si mon gars par une option config_export 

2972 

2973 print(content_comment_mld) 

2974 

2975 return content_comment_mld, count_objection 

2976 

2977 

2978 

2979def rebuild_git_data_from_atmo_data(map_data_atmo, map_hierarchy, 

2980 context_node_parent_name = None, 

2981grand_parent_node_name = None, 

2982 config_export = {"root_level" : "default", 

2983 "with_up_link" : True}): 

2984 print("VR TODO 15-8-25 : rebuild_git_data_from_atmo_data not yet implemented") 

2985 # VR TODO TACHE group by repo and issue and comment 

2986 # Loop over the list of comment 

2987 

2988 # Test on one comment 

2989 if context_node_parent_name == None: 

2990 context_node_parent_name = "BETATMO/14#issuecomment-3210372351" 

2991 context_node_parent_name = "BETATMO/14#issuecomment-3289898907" 

2992 grand_parent_node_name = "BETATMO/14" 

2993 

2994 list_data_atmo_this_comment = [] 

2995 for k in map_data_atmo: 

2996 d = map_data_atmo[k] 

2997 if "links" in d and type(d["links"]) == list: 

2998 for l in d["links"]: # en fait faut que les link hierarchy, on applique un tel filtre dans build_gag 

2999 # VR TODO 15-8-25 REFACTO on pourrait aussi faire un truc plus propre en utilisant map_hierarchy 

3000 if "target" in l and l["target"] == context_node_parent_name: 

3001 d["name"] = k 

3002 list_data_atmo_this_comment.append(d) 

3003 break 

3004 

3005 one_comment_content, count_objection = build_comment_content_from_atmo_data(list_data_atmo_this_comment, 

3006 context_node_parent_name = context_node_parent_name, 

3007 grand_parent_node_name= grand_parent_node_name, 

3008 config_export = config_export) 

3009 

3010# VR TODO do_we_miss_one_data 

3011 

3012 if count_objection > 0: 

3013 print("WARNING there were " + str(count_objection) + " OBJECTION labels") 

3014 return None, count_objection 

3015 

3016 return one_comment_content, 0 

3017 

3018# --job=github.gitactions --min_date=">2026-02-08" 

3019def github_gitactions(github_token, list_repos = [], 

3020 verbose = False, min_date = None, 

3021 conditions = {}): 

3022 print("VR TODO 15-8-25 : github_gitactions not yet implemented") 

3023 # List repos 

3024 

3025 print("min_date : " + str(min_date)) 

3026 

3027 g = Github(github_token) 

3028 

3029 g_ftn = g.get_organization("fotonower") 

3030 list_runners_self = g_ftn.get_self_hosted_runners() 

3031 

3032 print(str(list_runners_self)) 

3033 

3034 for r in list_runners_self: 

3035 print("YEN A PAS, c'était pas ca") 

3036 

3037 if list_repos == []: 

3038 list_repos_sans_doute_paginated = g.get_repos() 

3039 for r in list_repos_sans_doute_paginated: 

3040 list_repos.append(r.full_name) 

3041 

3042 for r in list_repos: 

3043 try: 

3044 repo = g.get_repo(r) 

3045 except Exception as e: 

3046 print(str(e)) 

3047 print("Repo " + str(r) + " has been deleted !") 

3048 continue 

3049 # repo.get_git_actions() 

3050 # conditions actors, event created 

3051 actor = "claude[bot]" if "actor" not in conditions else conditions["actor"] 

3052 list_wf_runs = repo.get_workflow_runs(actor = actor, created=min_date) 

3053 

3054 print(" list_wf_runs for repo : " + r + " " + str(list_wf_runs.totalCount)) 

3055 for w in list_wf_runs: 

3056 print(w.actor.login + " " + w.event + " " + str(w.created_at)) 

3057