From d7fa827000356b97ae8de24c42f82806a4c8ee39 Mon Sep 17 00:00:00 2001 From: didayolo Date: Wed, 15 Apr 2026 14:25:52 +0200 Subject: [PATCH 01/32] Clean up leaderboard ordering logic --- src/apps/api/serializers/leaderboards.py | 26 +++++++++++++++---- .../riot/competitions/detail/leaderboards.tag | 18 +++++++++++-- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/apps/api/serializers/leaderboards.py b/src/apps/api/serializers/leaderboards.py index f49e7c8df..41f61b168 100644 --- a/src/apps/api/serializers/leaderboards.py +++ b/src/apps/api/serializers/leaderboards.py @@ -1,4 +1,4 @@ -from django.db.models import Sum, Q +from django.db.models import Sum, Q, F from drf_writable_nested import WritableNestedModelSerializer from rest_framework import serializers @@ -102,7 +102,11 @@ def get_submissions(self, instance): # asc == colname primary_col = instance.columns.get(index=instance.primary_index) # Order first by primary column. Then order by other columns after for tie breakers. - ordering = [f'{"-" if primary_col.sorting == "desc" else ""}primary_col'] + ordering = [ + F('primary_col').desc(nulls_last=True) + if primary_col.sorting == 'desc' + else F('primary_col').asc(nulls_last=True) + ] submissions = ( Submission.objects.filter( leaderboard=instance, @@ -114,7 +118,11 @@ def get_submissions(self, instance): ) for column in instance.columns.exclude(id=primary_col.id).order_by('index'): col_name = f'col{column.index}' - ordering.append(f'{"-" if column.sorting == "desc" else ""}{col_name}') + ordering.append( + F(col_name).desc(nulls_last=True) + if column.sorting == 'desc' + else F(col_name).asc(nulls_last=True) + ) kwargs = { col_name: Sum('scores__score', filter=Q(scores__column__index=column.index)) } @@ -157,7 +165,11 @@ def get_submissions(self, instance): # desc == -colname # asc == colname primary_col = instance.leaderboard.columns.get(index=instance.leaderboard.primary_index) - ordering = [f'{"-" if primary_col.sorting == "desc" else ""}primary_col'] + ordering = [ + F('primary_col').desc(nulls_last=True) + if primary_col.sorting == 'desc' + else F('primary_col').asc(nulls_last=True) + ] submissions = ( Submission.objects.filter( phase=instance, @@ -177,7 +189,11 @@ def get_submissions(self, instance): .order_by('index') ): col_name = f'col{column.index}' - ordering.append(f'{"-" if column.sorting == "desc" else ""}{col_name}') + ordering.append( + F(col_name).desc(nulls_last=True) + if column.sorting == 'desc' + else F(col_name).asc(nulls_last=True) + ) kwargs = { col_name: Sum('scores__score', filter=Q(scores__column__index=column.index)) } diff --git a/src/static/riot/competitions/detail/leaderboards.tag b/src/static/riot/competitions/detail/leaderboards.tag index 80af465c4..aabc566f5 100644 --- a/src/static/riot/competitions/detail/leaderboards.tag +++ b/src/static/riot/competitions/detail/leaderboards.tag @@ -63,7 +63,9 @@ { pretty_date(submission.created_when) } {submission.id} - + @@ -227,6 +229,18 @@ return dt.isValid ? dt.toMillis() : 0 } + self.get_score_sort_value = function(column, submission) { + if (column.task_id === -1) { + let value = _.get(submission, 'fact_sheet_answers[' + column.key + ']') + return (value !== null && typeof value !== 'undefined' && value !== '') ? value : '' + } + let score = _.get(_.find(submission.scores, { + task_id: column.task_id, + column_key: column.key + }), 'score') + return (score !== null && typeof score !== 'undefined' && score !== '') ? score : '' + } + self.bold_class = function(column, submission){ return_class = '' if(column.task_id != -1){ @@ -245,7 +259,7 @@ return _.get(submission, 'fact_sheet_answers[' + column.key + ']', 'n/a') } else { let score = _.get(_.find(submission.scores, {'task_id': column.task_id, 'column_key': column.key}), 'score') - if (score) { + if (score !== null && typeof score !== 'undefined' && score !== '') { return score } } From 70c0b63060ee8df84d7eaedd8b1d4bb186fa8ff4 Mon Sep 17 00:00:00 2001 From: Obada Haddad-Soussac <11889208+ObadaS@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:41:00 +0200 Subject: [PATCH 02/32] Update version.json --- version.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.json b/version.json index c42192490..60297635a 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "tag_name": "v1.26", - "release_name": "v1.26", - "html_url": "https://github.com/codalab/codabench/releases/tag/v1.26" + "tag_name": "v1.27", + "release_name": "v1.27", + "html_url": "https://github.com/codalab/codabench/releases/tag/v1.27" } From 72e1377b150882d8db821cc0c0a9fb52fe7866a2 Mon Sep 17 00:00:00 2001 From: didayolo Date: Thu, 30 Apr 2026 12:36:54 +0200 Subject: [PATCH 03/32] Fix show counts in Django admin --- src/apps/competitions/admin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/apps/competitions/admin.py b/src/apps/competitions/admin.py index 98cb64147..dc5f489c0 100644 --- a/src/apps/competitions/admin.py +++ b/src/apps/competitions/admin.py @@ -25,6 +25,10 @@ def choices(self, changelist): ) yield all_choice + def get_facet_counts(self, pk_attname, filtered_qs): + # Text-input filters have no predefined choices to count (Django 5.0+ facets). + return {} + class SubmissionsCountFilter(InputFilter): # Human-readable title which will be displayed in the From 92bed501ccaee5132e89f5fe624e24d7ba990c42 Mon Sep 17 00:00:00 2001 From: didayolo Date: Thu, 30 Apr 2026 15:17:30 +0200 Subject: [PATCH 04/32] Fix Users show counts --- src/apps/profiles/admin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/apps/profiles/admin.py b/src/apps/profiles/admin.py index a3ad7bfe8..4a9ba7ca5 100644 --- a/src/apps/profiles/admin.py +++ b/src/apps/profiles/admin.py @@ -25,6 +25,10 @@ def choices(self, changelist): ) yield all_choice + def get_facet_counts(self, pk_attname, filtered_qs): + # Text-input filters have no predefined choices to count (Django 5.0+ facets). + return {} + class QuotaFilter(InputFilter): # Human-readable title which will be displayed in the From af914ea4630bc4481b027720bca576db499d84d8 Mon Sep 17 00:00:00 2001 From: didayolo Date: Tue, 5 May 2026 15:59:22 +0200 Subject: [PATCH 05/32] Remove wrong note --- src/static/riot/competitions/editor/_phases.tag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/riot/competitions/editor/_phases.tag b/src/static/riot/competitions/editor/_phases.tag index a9c318c28..bfe69d61f 100644 --- a/src/static/riot/competitions/editor/_phases.tag +++ b/src/static/riot/competitions/editor/_phases.tag @@ -115,7 +115,7 @@
From 886c6189df755b07e1cbe10bd03f464ac1a2d05c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Pav=C3=A3o?= Date: Tue, 12 May 2026 11:11:09 +0200 Subject: [PATCH 06/32] Improve 403 and 500 error web pages (#2349) * Improve 403 and 500 error web pages * Flake8 * Fix pages --- src/apps/pages/views.py | 6 ------ src/static/img/403.png | Bin 0 -> 3021 bytes src/static/img/500.png | Bin 0 -> 3158 bytes src/templates/403.html | 11 +++++++++++ src/templates/404.html | 2 +- src/templates/500.html | 11 +++++++++++ 6 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 src/static/img/403.png create mode 100644 src/static/img/500.png create mode 100644 src/templates/403.html create mode 100644 src/templates/500.html diff --git a/src/apps/pages/views.py b/src/apps/pages/views.py index d426d485e..ea9161426 100644 --- a/src/apps/pages/views.py +++ b/src/apps/pages/views.py @@ -6,7 +6,6 @@ from announcements.models import Announcement, NewsPost from django.conf import settings -from django.shortcuts import render from utils.data import pretty_bytes @@ -115,8 +114,3 @@ def get_context_data(self, *args, **kwargs): class MonitorQueuesView(TemplateView): template_name = 'pages/monitor_queues.html' - - -def page_not_found_view(request, exception): - print(request) - return render(request, '404.html', status=404) diff --git a/src/static/img/403.png b/src/static/img/403.png new file mode 100644 index 0000000000000000000000000000000000000000..9d420d4dfdb1ddb4d7b4ee6b732f4de3d5a3a0de GIT binary patch literal 3021 zcmds3`BxM77aam&3n2*AvLjzbib)_=Sqdn-ERm%k$%I7$0aTQo6al3XkV*j;EEG|Q zfWaojFk%r2E&*f*5g}oRiY!V56a^$@X{bGZd%mar2flOOyt(h*``%~focZBpc(^$t z<<;c@06;oB*?R&23>0IgtfV;eUf)d;KO|y39c?8BS~1^?3#p?vt~LO0=OSV?5H7B_ zQ=B|q0U&V~0MOC^KqRJUuK<9G1ArMa0GQ_kz?V_ix!+ofC(C#oI_w}eh%b#0nr1Lf zJD8>wOw**$aP%%idMB3NsYRh_1<|&XHyJGb=_m8&ys7>fpBOO~w|rK`b1?LF?2W$| z@eu7G8k*jPp?4Y<|JU}p_j98dV2eA&FYtHf-!PWm@ptL-ai7TlQUC7zzrmKx8{#l; z-Y{$4`1DccjT;utn_=FxDT%>fycjoa;!oN7@usx@PX^t-TUjhf#Cf`T0oGIfq2fX= z%83*U0NeC7ZwY75oo~gYOq{c;gUpQ7Rz;-FP+aFVu}#w1-sbSB!P!Fmg>u{%_BJp% zRY>Z|`3{l|AtT>DE0I1=)tAsXDcQ8@o?UR$Vmy{RM-5czvXj+KQ?mW%6t`!k(oFZ~ z!%CqIUzVC(xJQ=fa>{WY5AvI$xw_uGDz7{JQxp1+2Vdq}3(d)T>YOhs9A6W_vx0#d zPYbWI(g>T4mowXuj$-M-lh1EN{#2eHMMSLG|((rU!4!XtM3gkZ8y$(l@ z$)Q`goIr;;-olv{t{GUYj$tN5R<}JEzASd3h!i#NoSiRi2t~Iv&caDYCD4a_nq6!q zLDsiH7Pg0`1&(7surt4`e=Rsp$;!Sd!+))q?Gf)i6OncGb zz3%s#{)8^8W34HGl=p+JW?+qOccqmoV*S|*1@uZ2n4tm{>8PM%hbRNZ6=~?{TV_s` z+e+4xu3q)8`g=EGacurvJI%6;URLlZSXblKqWW8C) zLqVh7{=z6dC!nh5O3yhmw&L0=CpChjhe^&w)+k15*+&k8g52Z?yP9t_)IQ6Q!!Qk} zRHl16EEO@rEI%ZmPTQ)^`|$wTp&ixa?Vi3Qg~kOFr6F4_@ZEUoJ3uI(&UkgRUYm$@ zv}@QsK7U425`>dSzF-%bp5e8nFY!&;rg%m_WzqKbSf?uF){$!meH|Xdn3%C66w6nw zYhvJRjeBz$K_#_3xBl|vAgzwcB=hfsx26`44>v>)&y}805K7_^-Pb2rl4$L8A3gHO zffGu?;jD?7tJB<}XN}9o@450i5a%RF--hY6NLAT|jP!RIxu3a~G`y1g?5;%;Z%gi5AUz~{&~6LHeINU37nCa; z-|ZI&CaL8$W}x(^B>+T=9?*u(h&$pyx-X;Y6CTy1#40XklvOU`2y>eKb@#j~YD|XM6;KXHtwt zXpKR9Np(SgjZVj}E^i(g-Rjti;FU1O(7e*nB~x~Y|9PC&Hwq2BApKIubtm}mKN6+n zIVStq4rD#i`L)1A*%;C*4GzC9B`e$~i^}`)P=?LTB}O<8mKCnhE&;YZW#QKR!&Loj zvNbtUBbT9syZ1H57fM07J!Evn*5!nnK0(kE@b8niH&iM*8CSq>MtsSSR`II&W!#o@8fizPYI~LZL9fU z^vVllcY!_(azo_3o9cMUlxdHZ7O48(7@rAF6w(-MTGMf18;PbkMZvC{+MUo4tRCN6 zM06c(whfTi(GK<>fSQQ5?a`^z50m-*)cJ%+5AbGfq0+>ZPH#Z|gK90pV&Vzs(d`q8 z*RCrcBN?=jF0FhZ+V&Y)KtJK%&@WhEdFEdcRd}M9MbXu&+B*x*)hh?dHiCCNmpHOF zAzkmv#Oaf=VDm&9(S$Tk;HNN{=Vl@-J-Z@4;+=Q!xH@2}ZwiHc9P^5qP%>te-Yq>1 zCofI5NfAa`7#>Ihslb)vs2H_EZ8mcNSS>B&uN_`&IzMQ&vKp~FYU^aC4#j>s84Hb?t@Tp*k&|di zu)@fd-n3SnV7Q#NLyC<~Wdu!6y6RZD6$}F)QKnqfox;!OCAvxoYqo$&vDQ$UfMqqc zm*$;K^H%jV;&CNGqx~SvlCJ=1kNKuIg`A^NQO(L_%iiH}4B9PL9G1|twAy7RQl8?r z!q~C2#V{>=uBF19Q>MmjQkAt&Pen$NNrDVJGj)+J8$vwLVx7b!>@T{x9=%mk;9B?G z*xh#Hz^lpH&V!*59~17hdpZq9-P6R& z3X=d2`1zwSv)jLw7CN5f_R=S6^fMP((^Bnw1y<3emy-;=jR@Z-$J}p^qMAe8uO4b^ z%<`!tSxBj&zJeFi1Euv>G~8qb?Y-SX*zSfyx8HRYA`tF-%tH)%H8OSSiP;BYZ|sL$I+dZ6D&uiW~}$qxw^wb7ADmG|Es1o7Q0FAHhGc7A3DZiMJW<+IWB!jGlGA`>ucFQ=6ZrYsK&|C8S_LG$?sdD3Z zEe=O9FGt6w>KhXoWj&-|WW{-{kI%UOi;<6f+~9g9X9?ySOlM^vV(MMdjfI>9yD h@y|QSzb`9?*Cdg$M)eB}7E`<-0nQF?_8ePs${(~;(T4y4 literal 0 HcmV?d00001 diff --git a/src/static/img/500.png b/src/static/img/500.png new file mode 100644 index 0000000000000000000000000000000000000000..317a31dd94af19195a883bbcbe04a1c3039991d3 GIT binary patch literal 3158 zcmds3eK^x=AHUZ^36d0UF|I-|2O z9hr`|jP?|5=v^U}RN|DB=U$zw=UmTqo$LJn{I2_V?|Xl~ulM)&y}!Ft_PXGe6*UwA z0Ll*bwr&6{AVRu)5faXVu^Vyl2XV{|Z-W@PgH3>og$H*!?*yRs{E|68Ik?uN*tdYTQ*B=0d$iN&hrvZHC(dzXW;i9~P~o z6Ww1jP4g>4XzhZ8ee==l%5!J;Fy>K1@<1HM3Hga2{A}Pjw#*c;R|@LTr_;Vn@Io=C z2Uk-8d1GSfhr2T->n=RuQy)@Ka84-$?1)ZU2zBsk*Qo{sy$ykSAa+mS^YGyY!GQcO zF;Mf2ik#zpAQ)Uq@DuHq7GChEpJCG4b`Ju9KcG@m%GlD0s1jc2jGCb*@>!}})fHbw zJD+V4T<81D#a=fkv8twk6Kll0S$$`WG_M?OkAPqZDcriWci;Xr+x*D2-_*smsWy(ez8}B3r(A2iPKwIh3OH``{iDlQ%*9lN zrfzG4+fd798DMHm98ZrNM8`jW z#Z`Jc`yPW+#W{*in=Qv+_E5hEQ9e649oU=qR3cQ^Z&J~j@+~+OLQ{8FAD`wb#XOCv zCSh+T=Px9uto#_o*<{7(d){%+-rMSR&QL89>xu+vyU_9K5nq-yL=RUeJBIc}qrxHR zfy<&k@8q5w_4;b41gai)X*=RGNK1?)<5DF$2F1aqJNdOXwl=WQJ~(T;@Rx&y4`G6l z!nMkiBopblpE%7U$CTT)QJnMDmoN&qADhu#bF3+I30_Qm+7ivfl^#R{-wg%i@I-D% zP|hnw%3Ien#e{^C_ewnPo<5f&kmIa4T9P{Wt|X)A>BHH)H=8ZulVRo;GmR}Ix9>HD zmR}bvhfDFk%Ja#Z&y40+Z6`jRisd%FMISmI8r|)enY|9tln#y(LUD(!e?( z0p^`K^M-x@=OAu8+iKV*_)?R%mMl|pRW?1iebQae)azzBFLf(nbp#@)*5PLNq}xvG zN+0?->b4v0fjeFGarO3s3WbXu!(PiPpXK7&vieVQ=}H#)@f#Zpqu9QtS5sga90_9- zm7A22l4-m7F9%nqi#{oXDl{&ohPq*CO8fBc0-baqjYR_K9jEd99jV9hKE-=xkcEWy z-rV<=s@zdnzr;5eJwqcaUeJ7cs<{QtMwVeHSzT_cF>#AVqIYHCV~uMjxULoW=|EUT z3ZA&8+woDM6HQT(&Drs}Fa=S|O|z(Dd7KL~b(v~a$w~l=s?$=wG4;Xy#l=}X-QW-) zl{VeB7fx?>O`G!eT4HxR1g3%;RK15Jmp}0f-d zfKtz~l(bVu`7puib;F{3ETyyDl=rjGgy_lcICic_8~o(#L&WIBQaR%5$J_|qkczhA zd8kV&v(3&G>1Tx7uGuPa^WOeGRuvlt$Yy2(eCBWr<2-@e=(hIgDu+h`QL?(#(b*~k zV^7~#2QsAlHs+v(M|0wn6IlwFYX(zQ=&qJqi|Re5-X7X3`+9`DqV7CuR%Zh(#s(0R z-cg&WJ7DcYR)nonuq}plDNo|3WJ5LEL=0wxz0%zCd*g zXv-Wv=gyiqIPmfMI`;9fbTiM8 zwj0}gVPH0@4dg$+e~bT&Z@((qnn_2jwIo&=MtJj8)t|YL%KpIb>`$lRHR;607z2sl zef&>x?E3thzh9zw-2Ncv*bZ8GnbUn+OmR)Cq#L$1zVcf+o%IU0?D6e5&2X6$(OLzr z{~`VC>9{^Q!)o70gx;^O=j8RQIPD_V2}-IRP57CrX&~M-QolJ(lddXKe30oIY<0TW zbo&2xAUmm~}VV2M8J1foaPJ z&z&5F@ikp$V ztm(xO>aYYYnhoDxaNoP!+3MK5+PtiH6YeMB#(S$DFc+|P8Bj)0i*kNVJRmRC^={EP zSAK!2cg1Q8dRfxyFc45nFI8I{ujTc1vc9$P38|3_^Q+3>RjroO8tVo`E)ko)bcKs>+tOi78E~8SUk@babXD)GYi0VL+ z!;yNhzgD-jQmI`WiY@G6b+PidtL?kFyS;-~&ZsHL1ZrC90{E+^%@$1+oz>1PhIZOo z_S^YBw2$15C8uZrPc2g5if9oxgWYv(Pqv^n6D}bxx4E))UIreViOF8- zFeJYNCE8%W)VisSOys80*T`sn48* + +

You do not have permission to access this page.

+

If you believe this is an error, please fill a bug report. For more information about Codabench, check out the docs.

+ Return to Home +
+{% endblock %} diff --git a/src/templates/404.html b/src/templates/404.html index eaa3948d5..9ab55820d 100644 --- a/src/templates/404.html +++ b/src/templates/404.html @@ -6,7 +6,7 @@

Could not find the page you requested.

If you are the challenge organizer and this URL previously pointed to a valid competition and it is now missing please contact us at info@codabench.org for more information.

-

If this is an error, please file a bug report. For more information about Codabench, check out the docs.

+

If this is an error, please fill a bug report. For more information about Codabench, check out the docs.

Return to Home {% endblock %} diff --git a/src/templates/500.html b/src/templates/500.html new file mode 100644 index 000000000..39410a45e --- /dev/null +++ b/src/templates/500.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} + + +{% block content %} +
+ +

Something went wrong on our end.

+

If this is an error, please fill a bug report. For more information about Codabench, check out the docs.

+ Return to Home +
+{% endblock %} From 128acc4cb6fb2a8870cc8d26668e844c28473d85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Pav=C3=A3o?= Date: Tue, 12 May 2026 11:43:51 +0200 Subject: [PATCH 07/32] Remove score edition feature (#2342) * Remove score edition * Group the submission logs * Dynamic tabs instead of always 4 * Remove useless comments * Have all logs possibilities instead of grouping them by 2 * Fix logs for multi-task case * Put back some code that were actually working better * Use Semantic UI for inner tabs, clean code * Fix tab selection * Add permission check to datasets/download (#2348) * Add permission check to download method * Flake8 * Check for 403 error, not 404 * Fix solution download * Bump django from 5.2.12 to 5.2.13 Bumps [django](https://github.com/django/django) from 5.2.12 to 5.2.13. - [Commits](https://github.com/django/django/compare/5.2.12...5.2.13) --- updated-dependencies: - dependency-name: django dependency-version: 5.2.13 dependency-type: direct:production ... Signed-off-by: dependabot[bot] * Bump cryptography from 46.0.6 to 46.0.7 Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.6 to 46.0.7. - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/46.0.6...46.0.7) --- updated-dependencies: - dependency-name: cryptography dependency-version: 46.0.7 dependency-type: indirect ... Signed-off-by: dependabot[bot] * Bump pytest from 9.0.2 to 9.0.3 Bumps [pytest](https://github.com/pytest-dev/pytest) from 9.0.2 to 9.0.3. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/9.0.2...9.0.3) --- updated-dependencies: - dependency-name: pytest dependency-version: 9.0.3 dependency-type: direct:development ... Signed-off-by: dependabot[bot] * Bump pillow from 12.1.1 to 12.2.0 Bumps [pillow](https://github.com/python-pillow/Pillow) from 12.1.1 to 12.2.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/12.1.1...12.2.0) --- updated-dependencies: - dependency-name: pillow dependency-version: 12.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] * rebase and added more packages upgrades via uv lock --upgrade * rebase and added more packages upgrades via uv lock --upgrade v2 * Update version.json * Fix show counts in Django admin * Fix Users show counts --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Obada Haddad Co-authored-by: Obada Haddad-Soussac <11889208+ObadaS@users.noreply.github.com> --- src/static/riot/competitions/detail/_tabs.tag | 4 -- .../detail/submission_manager.tag | 7 --- .../competitions/detail/submission_modal.tag | 5 -- .../competitions/detail/submission_scores.tag | 54 ------------------- 4 files changed, 70 deletions(-) delete mode 100644 src/static/riot/competitions/detail/submission_scores.tag diff --git a/src/static/riot/competitions/detail/_tabs.tag b/src/static/riot/competitions/detail/_tabs.tag index a791d05dd..02cb0aadd 100644 --- a/src/static/riot/competitions/detail/_tabs.tag +++ b/src/static/riot/competitions/detail/_tabs.tag @@ -583,10 +583,6 @@ .phase-info margin-bottom 10px - .admin-tab - margin 0 auto - width 100% - pre background #f4f4f4 border 1px solid #ddd diff --git a/src/static/riot/competitions/detail/submission_manager.tag b/src/static/riot/competitions/detail/submission_manager.tag index 87c4c0c9f..441a6cf52 100644 --- a/src/static/riot/competitions/detail/submission_manager.tag +++ b/src/static/riot/competitions/detail/submission_manager.tag @@ -232,7 +232,6 @@ Task {i + 1} -
Admin
ERROR: Submission is a parent, but has no children. There was an error @@ -248,9 +247,6 @@
-
- -
@@ -713,9 +709,6 @@ self.update() }) } - if (opts.admin) { - submission.admin = true - } self.selected_submission = submission self.update() $(self.refs.modal) diff --git a/src/static/riot/competitions/detail/submission_modal.tag b/src/static/riot/competitions/detail/submission_modal.tag index 936ce4617..575ccb8d7 100644 --- a/src/static/riot/competitions/detail/submission_modal.tag +++ b/src/static/riot/competitions/detail/submission_modal.tag @@ -3,7 +3,6 @@
DOWNLOADS
LOGS
VISUALIZATION
-
ADMIN
FACT SHEET ANSWERS
@@ -225,10 +224,6 @@ max-height 465px overflow auto - .leaderboard-tab - height 515px - overflow auto - .modal-tab height 530px diff --git a/src/static/riot/competitions/detail/submission_scores.tag b/src/static/riot/competitions/detail/submission_scores.tag deleted file mode 100644 index 6cf69730a..000000000 --- a/src/static/riot/competitions/detail/submission_scores.tag +++ /dev/null @@ -1,54 +0,0 @@ - -
-
-

{leaderboard.title}

- - - - - - - - - - - - -
- {column.title} -
- -
-
- -
- - - - -
From c1920af87e1663f8aa2a2c79ad19b1e378a24dee Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Sat, 16 May 2026 14:44:53 +0500 Subject: [PATCH 08/32] OIDC updates from #2183 - Ensure username for OIDC accounts fit into db constraints (#2376) * Ensure username for OIDC accounts fit into db constraints The username column is constrained to 50 characters. It's possible that a nickname or email derived username could be larger than this ( was in the case of Globus auth ). Truncate username to the 50 character limit. * flake fixes * code simplified --------- Co-authored-by: Chris Harris --- src/apps/oidc_configurations/views.py | 114 +++++++++++++++++--------- 1 file changed, 74 insertions(+), 40 deletions(-) diff --git a/src/apps/oidc_configurations/views.py b/src/apps/oidc_configurations/views.py index 6b04be8ff..9e739b078 100644 --- a/src/apps/oidc_configurations/views.py +++ b/src/apps/oidc_configurations/views.py @@ -53,53 +53,61 @@ def oidc_complete(request, auth_organization_id): if error: context["error"] = error - if error_description: context["error_description"] = error_description - # Token exhange process - if authorization_code: + if error or error_description: + return render(request, 'oidc/oidc_complete.html', context) - try: - # STEP 1: Get auth organization using its id - organization = get_object_or_404(Auth_Organization, pk=auth_organization_id) + if not authorization_code: + context["error"] = "Authorization Code not provided!" + return render(request, 'oidc/oidc_complete.html', context) - if organization: + try: + # STEP 1: Get auth organization using its id + organization = get_object_or_404(Auth_Organization, pk=auth_organization_id) + + # STEP 2: Get access token + access_token, token_error = get_access_token(organization, authorization_code) + if token_error: + context["error"] = token_error + return render(request, 'oidc/oidc_complete.html', context) + else: + # STEP 3: Get user info + user_info, user_info_error = get_user_info(organization, access_token) + if user_info_error: + context["error"] = user_info_error + return render(request, 'oidc/oidc_complete.html', context) + else: + if not isinstance(user_info, dict): + context["error"] = "Invalid user info payload from OIDC provider." + return render(request, 'oidc/oidc_complete.html', context) - # STEP 2: Get access token - access_token, token_error = get_access_token(organization, authorization_code) + user_email = user_info.get("email", None) + user_nickname = user_info.get("nickname", None) + if not user_email: + context["error"] = "Unable to extract email from user info." + return render(request, 'oidc/oidc_complete.html', context) - if token_error: - context["error"] = token_error + user_email = str(user_email).strip().lower() + + if user_nickname is not None: + user_nickname = str(user_nickname).strip() or None + + # get user with this email + user = get_user_by_email(user_email) + + # STEP 4: Check if user exists and user is created using oidc and oidc orgnaization matches this one + if user: + login(request, user, backend=BACKEND) + # Redirect the user home page + return redirect('pages:home') else: - # STEP 3: Get user info - user_info, user_info_error = get_user_info(organization, access_token) - if user_info_error: - context["error"] = user_info_error - else: - - # get email and nickname (username) of the user - user_email = user_info.get("email", None) - user_nickname = user_info.get("nickname", None) - if user_email: - # get user with this email - user = get_user_by_email(user_email) - # STEP 4: Check if user exists and user is created using oidc and oidc orgnaization matches this one - if user: - login(request, user, backend=BACKEND) - # Redirect the user home page - return redirect('pages:home') - else: - return register_and_authenticate_user(request, user_email, user_nickname, organization) - - else: - context["error"] = "Unable to extract email from user info! Please contact platform" - else: - context["error"] = "Invalid Organization ID!" - except Exception as e: - context["error"] = f"{e}" + return register_and_authenticate_user(request, user_email, user_nickname, organization) - return render(request, 'oidc/oidc_complete.html', context) + except Exception as e: + context["error"] = f"{e}" + return render(request, 'oidc/oidc_complete.html', context) def get_access_token(organization, authorization_code): @@ -181,14 +189,21 @@ def register_and_authenticate_user(request, user_email, user_nickname, organizat def create_unique_username(username): + # Normalize the username to remove unsupported characters and ensure it's route-safe. + username = normalize_username(username) + + # Truncate the username to fit within the maximum length allowed by the User model. + max_username_length = User._meta.get_field("username").max_length + username = username[:max_username_length] + # Check if the username already exists if User.objects.filter(username=username).exists(): # If the username already exists, modify it to make it unique suffix = 1 - new_username = f"{username}_{suffix}" + new_username = with_suffix(username, suffix, max_username_length) while User.objects.filter(username=new_username).exists(): suffix += 1 - new_username = f"{username}_{suffix}" + new_username = with_suffix(username, suffix, max_username_length) return new_username else: # If the username doesn't exist, use it as is @@ -201,3 +216,22 @@ def get_user_by_email(email): return user except User.DoesNotExist: return None + + +def normalize_username(username): + # Keep OIDC names readable while removing unsupported chars. + # Keep in sync with profile URL regex: [-a-zA-Z0-9_]+ + cleaned = re.sub(r'[^a-zA-Z0-9_-]', '', username.strip()) + if not cleaned: + raise ValueError("OIDC username contains no valid route-safe characters") + + return cleaned + + +def with_suffix(base_username, suffix, max_username_length): + suffix = f"_{suffix}" + limit = max_username_length - len(suffix) + if limit < 1: + raise ValueError("Unable to create unique username within max length") + + return f"{base_username[:limit]}{suffix}" From d06f24e46431255ab0de7e6cc5c1516abacd4b88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Pav=C3=A3o?= Date: Sat, 16 May 2026 11:47:53 +0200 Subject: [PATCH 09/32] Add description in dropdown selector (edit phase > select tasks) (#2340) * Add description in dropdown selector * code simplified * remove comment --------- Co-authored-by: Ihsan Ullah --- .../riot/competitions/editor/_phases.tag | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/static/riot/competitions/editor/_phases.tag b/src/static/riot/competitions/editor/_phases.tag index a9c318c28..c2855bdb6 100644 --- a/src/static/riot/competitions/editor/_phases.tag +++ b/src/static/riot/competitions/editor/_phases.tag @@ -261,6 +261,24 @@ // awesome markdown editor self.simple_markdown_editor = create_easyMDE(self.refs.description) + // Custom menu template that renders description under the item name + // data-text is set to name-only so that selected labels show only the title + var dropdown_menu_template = function(response) { + var html = '' + $.each(response.values, function(index, item) { + let name = item.name || '' + let value = item.value || '' + let description = item.description + html += '
' + html += '' + name + '' + if (description) { + html += '' + } + html += '
' + }) + return html + } + // semantic multiselect $(self.refs.multiselect).dropdown({ apiSettings: { @@ -270,6 +288,7 @@ return {success: true, results: _.values(data.results)} }, }, + templates: {menu: dropdown_menu_template}, onAdd: self.task_added, onRemove: self.task_removed, }) @@ -282,10 +301,11 @@ return {success: true, results: _.values(data.results)} }, }, + templates: {menu: dropdown_menu_template}, onAdd: self.public_data_added, onRemove: self.public_data_removed, }) - + $(self.refs.starting_kit_multiselect).dropdown({ apiSettings: { url: `${URLS.API}datasets/?search={query}&type=starting_kit`, @@ -294,6 +314,7 @@ return {success: true, results: _.values(data.results)} }, }, + templates: {menu: dropdown_menu_template}, onAdd: self.starting_kit_added, onRemove: self.starting_kit_removed, }) @@ -858,5 +879,11 @@ From 70463f20210fcce20e1cf67ffe04d47c10fd2a40 Mon Sep 17 00:00:00 2001 From: Obada Haddad Date: Thu, 5 Mar 2026 15:57:18 +0100 Subject: [PATCH 10/32] switch to zensical since mkdocs 1 is unmaintained and mkdocs 2 is ... special --- documentation/.gitignore | 2 +- documentation/.python-version | 1 - .../overrides/partials/copyright.html | 4 +- documentation/pyproject.toml | 10 +- documentation/uv.lock | 129 ++++++++++++++ documentation/zensical.toml | 159 ++++++++++++++++++ 6 files changed, 294 insertions(+), 11 deletions(-) delete mode 100644 documentation/.python-version create mode 100644 documentation/uv.lock create mode 100644 documentation/zensical.toml diff --git a/documentation/.gitignore b/documentation/.gitignore index af3887b1b..70a321042 100644 --- a/documentation/.gitignore +++ b/documentation/.gitignore @@ -9,6 +9,6 @@ wheels/ # Virtual environments .venv -uv.lock site/* +.python-version diff --git a/documentation/.python-version b/documentation/.python-version deleted file mode 100644 index e4fba2183..000000000 --- a/documentation/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.12 diff --git a/documentation/overrides/partials/copyright.html b/documentation/overrides/partials/copyright.html index 5016d64ef..e5109fcb9 100644 --- a/documentation/overrides/partials/copyright.html +++ b/documentation/overrides/partials/copyright.html @@ -12,8 +12,8 @@ {% endif %} {% if not config.extra.generator == false %} Made with - - Material for MkDocs + + Zensical {% endif %} \ No newline at end of file diff --git a/documentation/pyproject.toml b/documentation/pyproject.toml index ad15b5223..89508bc21 100644 --- a/documentation/pyproject.toml +++ b/documentation/pyproject.toml @@ -1,13 +1,9 @@ [project] -name = "codabench-mkdocs" +name = "documentation" version = "0.1.0" description = "Add your description here" readme = "README.md" -requires-python = ">=3.12" +requires-python = ">=3.14" dependencies = [ - "mike>=2.1.3", - "mkdocs-glightbox>=0.4.0", - "mkdocs-material>=9.6.15", - "mkdocs-open-in-new-tab>=1.0.8", - "mkdocs-to-pdf>=0.10.1", + "zensical>=0.0.24", ] diff --git a/documentation/uv.lock b/documentation/uv.lock new file mode 100644 index 000000000..49b4ac5fe --- /dev/null +++ b/documentation/uv.lock @@ -0,0 +1,129 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "deepmerge" +version = "2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/3a/b0ba594708f1ad0bc735884b3ad854d3ca3bdc1d741e56e40bbda6263499/deepmerge-2.0.tar.gz", hash = "sha256:5c3d86081fbebd04dd5de03626a0607b809a98fb6ccba5770b62466fe940ff20", size = 19890, upload-time = "2024-08-30T05:31:50.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00", size = 13475, upload-time = "2024-08-30T05:31:48.659Z" }, +] + +[[package]] +name = "documentation" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "zensical" }, +] + +[package.metadata] +requires-dist = [{ name = "zensical", specifier = ">=0.0.24" }] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/63/06673d1eb6d8f83c0ea1f677d770e12565fb516928b4109c9e2055656a9e/pymdown_extensions-10.21.tar.gz", hash = "sha256:39f4a020f40773f6b2ff31d2cd2546c2c04d0a6498c31d9c688d2be07e1767d5", size = 853363, upload-time = "2026-02-15T20:44:06.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl", hash = "sha256:91b879f9f864d49794c2d9534372b10150e6141096c3908a455e45ca72ad9d3f", size = 268877, upload-time = "2026-02-15T20:44:05.464Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "zensical" +version = "0.0.24" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "deepmerge" }, + { name = "markdown" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/96/9c6cbdd7b351d1023cdbbcf7872d4cb118b0334cfe5821b99e0dd18e3f00/zensical-0.0.24.tar.gz", hash = "sha256:b5d99e225329bf4f98c8022bdf0a0ee9588c2fada7b4df1b7b896fcc62b37ec3", size = 3840688, upload-time = "2026-02-26T09:43:44.557Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/aa/b8201af30e376a67566f044a1c56210edac5ae923fd986a836d2cf593c9c/zensical-0.0.24-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d390c5453a5541ca35d4f9e1796df942b6612c546e3153dd928236d3b758409a", size = 12263407, upload-time = "2026-02-26T09:43:14.716Z" }, + { url = "https://files.pythonhosted.org/packages/78/8e/3d910214471ade604fd39b080db3696864acc23678b5b4b8475c7dbfd2ce/zensical-0.0.24-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:81ac072869cf4d280853765b2bfb688653da0dfb9408f3ab15aca96455ab8223", size = 12142610, upload-time = "2026-02-26T09:43:17.546Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d7/eb0983640aa0419ddf670298cfbcf8b75629b6484925429b857851e00784/zensical-0.0.24-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5eb1dfa84cae8e960bfa2c6851d2bc8e9710c4c4c683bd3aaf23185f646ae46", size = 12508380, upload-time = "2026-02-26T09:43:20.114Z" }, + { url = "https://files.pythonhosted.org/packages/a3/04/4405b9e6f937a75db19f0d875798a7eb70817d6a3bec2a2d289a2d5e8aea/zensical-0.0.24-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:57d7c9e589da99c1879a1c703e67c85eaa6be4661cdc6ce6534f7bb3575983f4", size = 12440807, upload-time = "2026-02-26T09:43:22.679Z" }, + { url = "https://files.pythonhosted.org/packages/12/dc/a7ca2a4224b3072a2c2998b6611ad7fd4f8f131ceae7aa23238d97d26e22/zensical-0.0.24-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42fcc121c3095734b078a95a0dae4d4924fb8fbf16bf730456146ad6cab48ad0", size = 12782727, upload-time = "2026-02-26T09:43:25.347Z" }, + { url = "https://files.pythonhosted.org/packages/42/37/22f1727da356ed3fcbd31f68d4a477f15c232997c87e270cfffb927459ac/zensical-0.0.24-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4a2a051b9f49561031a2986ace502326f82d9a401ddf125530d30025fdd4", size = 12547616, upload-time = "2026-02-26T09:43:28.031Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/c75ff111b8e12157901d00752beef9d691dbb5a034b6a77359972262416a/zensical-0.0.24-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e5fea3bb61238dba9f930f52669db67b0c26be98e1c8386a05eb2b1e3cb875dc", size = 12684883, upload-time = "2026-02-26T09:43:30.642Z" }, + { url = "https://files.pythonhosted.org/packages/b9/92/4f6ea066382e3d068d3cadbed99e9a71af25e46c84a403e0f747960472a2/zensical-0.0.24-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:75eef0428eec2958590633fdc82dc2a58af124879e29573aa7e153b662978073", size = 12713825, upload-time = "2026-02-26T09:43:33.273Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fb/bf735b19bce0034b1f3b8e1c50b2896ebbd0c5d92d462777e759e78bb083/zensical-0.0.24-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:3c6b39659156394ff805b4831dac108c839483d9efa4c9b901eaa913efee1ac7", size = 12854318, upload-time = "2026-02-26T09:43:35.632Z" }, + { url = "https://files.pythonhosted.org/packages/7e/28/0ddab6c1237e3625e7763ff666806f31e5760bb36d18624135a6bb6e8643/zensical-0.0.24-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9eef82865a18b3ca4c3cd13e245dff09a865d1da3c861e2fc86eaa9253a90f02", size = 12818270, upload-time = "2026-02-26T09:43:37.749Z" }, + { url = "https://files.pythonhosted.org/packages/2a/93/d2cef3705d4434896feadffb5b3e44744ef9f1204bc41202c1b84a4eeef6/zensical-0.0.24-cp310-abi3-win32.whl", hash = "sha256:f4d0ff47d505c786a26c9332317aa3e9ad58d1382f55212a10dc5bafcca97864", size = 11857695, upload-time = "2026-02-26T09:43:39.906Z" }, + { url = "https://files.pythonhosted.org/packages/f1/26/9707587c0f6044dd1e1cc5bc3b9fa5fed81ce6c7bcdb09c21a9795e802d9/zensical-0.0.24-cp310-abi3-win_amd64.whl", hash = "sha256:e00a62cf04526dbed665e989b8f448eb976247f077a76dfdd84699ace4aa3ac3", size = 12057762, upload-time = "2026-02-26T09:43:42.627Z" }, +] diff --git a/documentation/zensical.toml b/documentation/zensical.toml new file mode 100644 index 000000000..f3cfc6b66 --- /dev/null +++ b/documentation/zensical.toml @@ -0,0 +1,159 @@ +[project] +site_name = "Codabench Docs" +repo_url = "https://github.com/codalab/codabench" +copyright = "Creative Commons Attribution-ShareAlike 4.0 License" +edit_uri = "edit/develop/documentation/docs" +nav = [ + {"Home" = "index.md"}, + {"Participants" = [ + {"Participating in a Competition" = "Participants/User_Participating-in-a-Competition.md"}, + {"List of Current Benchmarks and Competitions" = "https://www.codabench.org/competitions/public/?page=1"} + ]}, + {"Organizers" = [ + {"Benchmark Creation" = [ + {"Getting Started Tutorial" = "Organizers/Benchmark_Creation/Getting-started-with-Codabench.md"}, + {"Advanced Tutorial" = "Organizers/Benchmark_Creation/Advanced-Tutorial.md"}, + {"How to Transition from Codalab to Codabench?" = "Organizers/Benchmark_Creation/How-to-transition-from-CodaLab-to-Codabench.md"}, + {"Competition Creation" = "Organizers/Benchmark_Creation/Competition-Creation.md"}, + {"Competition Creation Form" = "Organizers/Benchmark_Creation/Competition-Creation-Form.md"}, + {"Competition Creation Bundle" = "Organizers/Benchmark_Creation/Competition-Creation-Bundle.md"}, + {"Competition YAML Structure" = "Organizers/Benchmark_Creation/Competition-Bundle-Structure.md"}, + {"YAML Structure" = "Organizers/Benchmark_Creation/Yaml-Structure.md"}, + {"Competition Docker Image" = "Organizers/Benchmark_Creation/Competition-docker-image.md"}, + {"Dataset Competition Creation and participate instruction" = "Organizers/Benchmark_Creation/Dataset-competition-creation-and-participate-instruction.md"}, + {"Leaderboard Features" = "Organizers/Benchmark_Creation/Leaderboard-Functionality.md"}, + {"Competition Examples" = "https://github.com/codalab/competition-examples/tree/master/codabench"}, + {"Public Tasks and Tasks Sharing" = "Organizers/Benchmark_Creation/Public-Tasks-and-Tasks-Sharing.md"}, + {"Detailed Results and Visualization" = "Organizers/Benchmark_Creation/Detailed-Results-and-Visualizations.md"}, + ]}, + {"Running a Benchmarks" = [ + {"Benchmark Management & List Page" = "Organizers/Running_a_benchmark/Competition-Management-&-List.md"}, + {"Benchmark Detail Page"= "Organizers/Running_a_benchmark/Competition-Detail-Page.md"}, + {"Ressource Management Submissions, Datasets/Programs, Tasks and Competition Bundles" = "Organizers/Running_a_benchmark/Resource-Management.md"}, + {"Update programs or data" = "Organizers/Running_a_benchmark/Update-programs-or-data.md"}, + {"Queue Management" = "Organizers/Running_a_benchmark/Queue-Management.md"}, + {"Compute Worker Management & Setup" = "Organizers/Running_a_benchmark/Compute-Worker-Management---Setup.md"}, + {"Compute Worker Management with Podman" = "Organizers/Running_a_benchmark/Compute-worker-installation-with-Podman.md"}, + {"Server Status" = "Organizers/Running_a_benchmark/Server-status-page.md"} + ]} + ]}, + {"Developers and Administrators" = [ + {"Codabench Basic Installation Guide" = "Developers_and_Administrators/Codabench-Installation.md"}, + {"How to Deploy a Server" = "Developers_and_Administrators/How-to-deploy-Codabench-on-your-server.md"}, + {"Administrative Procedures" = "Developers_and_Administrators/Administrator-procedures.md"}, + {"Codabench Docker Architecture" = "Developers_and_Administrators/Codabench-Architecture.md"}, + {"Submission Docker Container Layout" = "Developers_and_Administrators/Submission-Docker-Container-Layout.md"}, + {"Backups - Automating Creation and Restoring" = "Developers_and_Administrators/Creating-and-Restoring-from-Backup.md"}, + {"Submission Process Overview" = "Developers_and_Administrators/Submission-Process-Overview.md"}, + {"Robot Submissions" = "Developers_and_Administrators/Robot-submissions.md"}, + {"Adding Tests" = "Developers_and_Administrators/Adding-e2e-tests.md"}, + {"Running Tests" = "Developers_and_Administrators/Running-tests.md"}, + {"Automation" = "Developers_and_Administrators/Automating-with-Selenium.md"}, + {"Manual Validation" = "Developers_and_Administrators/Manual-validation.md"}, + {"Validation and deployement of pull requests" = "Developers_and_Administrators/Validation-and-deployment-of-pull-requests.md"}, + {" Upgrading Codabench" = [ + "Developers_and_Administrators/Upgrading_Codabench/index.md", + {"Upgrade RabbitMQ (version < 1.0.0)" = "Developers_and_Administrators/Upgrading_Codabench/Upgrade-RabbitMQ.md"}, + {"Create new logos for each competitions (version < 1.4.1)" = "Developers_and_Administrators/Upgrading_Codabench/Create-new-logos-for-each-competition.md"}, + {"Worker docker image manual update (version < 1.3.1)" = "Developers_and_Administrators/Upgrading_Codabench/Worker-Docker-Image-manual-update.md"}, + {"Add line in .env file for default worker queue duration (version < 1.7.0)" = "Developers_and_Administrators/Upgrading_Codabench/Add-line-into-.env-for-default-queue-worker-duration.md"}, + {"Uncomment a line in your .env file (version < 1.8.0)" = "Developers_and_Administrators/Upgrading_Codabench/Uncomment-a-line-in-your-.env-file.md"}, + {"Rebuilding all docker images (version < 1.9.2)" = "Developers_and_Administrators/Upgrading_Codabench/Rebuilding-all-docker-images.md"}, + {"Move the latest storage_inconsistency files from the logs folder to var/logs (version < 1.12.0)" = "Developers_and_Administrators/Upgrading_Codabench/Move-the-last-storage_inconsistency-files-from--logs-folder-to--var-logs--folder.md"}, + {"Submissions and Participants count (version < 1.14.0)" = "Developers_and_Administrators/Upgrading_Codabench/Submissions-and-Participants-Counts.md"}, + {"Homepage Counters (version < 1.15.0)" = "Developers_and_Administrators/Upgrading_Codabench/Homepage-counters.md"}, + {"User Removal (version < 1.17.0)" = "Developers_and_Administrators/Upgrading_Codabench/User-removal.md"}, + {"Database size fix (version < 1.18.0)" = "Developers_and_Administrators/Upgrading_Codabench/Database-size-fixes.md"} + ]} + ]}, + {"Newsletters Archive" = [ + {"2024" = "Newsletters_Archive/CodaLab-in-2024.md"}, + {"2025" = "Newsletters_Archive/CodaLab-in-2025.md"} + ]}, + {"FAQ" = "Project_CodaBench_FAQ.md"}, + {"Contact Us" = "contact-us.md"} +] + + +[project.theme] +variant = "modern" +custom_dir = "overrides" +features = [ + "content.code.copy", + "navigation.tabs", + "navigation.tabs.sticky", + "search.share", + "navigation.footer", + "navigation.top", + "content.action.edit", + "content.action.view", + "navigation.expand", + "navigation.instant", + "navigation.instant.progress", +] + +# Palette toggle for automatic mode +[[project.theme.palette]] +media = "(prefers-color-scheme)" +toggle.icon = "lucide/sun-moon" +toggle.name = "Switch to light mode" + +# Palette toggle for light mode +[[project.theme.palette]] +media = "(prefers-color-scheme: light)" +scheme = "default" +toggle.icon = "lucide/sun" +toggle.name = "Switch to dark mode" + +# Palette toggle for dark mode +[[project.theme.palette]] +media = "(prefers-color-scheme: dark)" +scheme = "slate" +toggle.icon = "lucide/moon" +toggle.name = "Switch to system preference" + +# Mardown extensions +[project.markdown_extensions.pymdownx.highlight] +anchor_linenums = true +line_spans = "__span" +pygments_lang_class = true +[project.markdown_extensions.pymdownx.inlinehilite] +[project.markdown_extensions.pymdownx.snippets] +[project.markdown_extensions.pymdownx.superfences] +custom_fences = [ + { name = "mermaid", class = "mermaid", format = "pymdownx.superfences.fence_code_format" } +] +[project.markdown_extensions.admonition] +[project.markdown_extensions.pymdownx.details] +[project.markdown_extensions.attr_list] +[project.markdown_extensions.pymdownx.emoji] +[project.markdown_extensions.tables] +[project.markdown_extensions.pymdownx.tabbed] +alternate_style = true +[project.markdown_extensions.pymdownx.tabbed.slugify] +object = "pymdownx.slugs.slugify" +kwds = { case = "lower" } +[project.markdown_extensions.pymdownx.caret] +[project.markdown_extensions.pymdownx.keys] +[project.markdown_extensions.pymdownx.mark] +[project.markdown_extensions.pymdownx.tilde] +[project.markdown_extensions.md_in_html] +[project.markdown_extensions.pymdownx.blocks.caption] +[project.markdown_extensions.zensical.extensions.preview] +configurations = [ + { targets.include = [ + "customization.md", + "setup/extensions/*" + ]} +] + +# Extras +[project.extra] +footer_links = [ + {"text" = "Governance Document"}, + {"href" = "https://github.com/codalab/codabench/blob/master/documentation/GOVERNANCE.md"}, + {"text" = "Privacy Policy"}, + {"href" = "https://github.com/codalab/codabench/blob/master/documentation/PRIVACY.md"}, + {"text" = "About"}, + {"href" = "https://github.com/codalab/codabench/blob/master/documentation/ABOUT.md"} +] \ No newline at end of file From d16118b8fcd3ad3852992ffb6b1f096124930445 Mon Sep 17 00:00:00 2001 From: Obada Haddad Date: Thu, 5 Mar 2026 16:10:32 +0100 Subject: [PATCH 11/32] remove some mkdocs traces, update workflow to use zensical instead of mkdocs --- .github/workflows/ghPages-dev.yml | 25 --------------------- .github/workflows/ghPages-prod.yml | 36 +++++++++++++++++------------- documentation/README.md | 20 +++-------------- documentation/main.py | 6 ----- 4 files changed, 23 insertions(+), 64 deletions(-) delete mode 100644 .github/workflows/ghPages-dev.yml delete mode 100644 documentation/main.py diff --git a/.github/workflows/ghPages-dev.yml b/.github/workflows/ghPages-dev.yml deleted file mode 100644 index 9616e1fb5..000000000 --- a/.github/workflows/ghPages-dev.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: github-pages-dev -on: - push: - branches: - - develop -permissions: - contents: write -jobs: - deploy-dev: - runs-on: ubuntu-latest - defaults: - run: - working-directory: documentation/ - steps: - - uses: actions/checkout@v4 - - name: Configure Git Credentials - run: | - git config user.name github-actions[bot] - git config user.email 41898282+github-actions[bot]@users.noreply.github.com - - uses: actions/setup-python@v5 - with: - python-version: 3.x - - run: curl -LsSf https://astral.sh/uv/install.sh | sh - - run: /home/runner/.local/bin/uv sync - - run: git fetch origin gh-pages --depth=1 && PDF=1 /home/runner/.local/bin/uv run mike deploy -u dev --push \ No newline at end of file diff --git a/.github/workflows/ghPages-prod.yml b/.github/workflows/ghPages-prod.yml index b12be4cd6..d4e64e59b 100644 --- a/.github/workflows/ghPages-prod.yml +++ b/.github/workflows/ghPages-prod.yml @@ -1,25 +1,29 @@ -name: github-pages-prod +name: Documentation on: push: - tags: - - '*' + branches: + - master + - main permissions: - contents: write + contents: read + pages: write + id-token: write jobs: - deploy-prod: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest - defaults: - run: - working-directory: documentation/ steps: - - uses: actions/checkout@v4 - - name: Configure Git Credentials - run: | - git config user.name github-actions[bot] - git config user.email 41898282+github-actions[bot]@users.noreply.github.com + - uses: actions/configure-pages@v5 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: 3.x - - run: curl -LsSf https://astral.sh/uv/install.sh | sh - - run: /home/runner/.local/bin/uv sync - - run: git fetch origin gh-pages --depth=1 && PDF=1 /home/runner/.local/bin/uv run mike deploy -u ${{ github.ref_name }} latest --push \ No newline at end of file + - run: pip install zensical + - run: zensical build --clean + - uses: actions/upload-pages-artifact@v4 + with: + path: site + - uses: actions/deploy-pages@v4 + id: deployment \ No newline at end of file diff --git a/documentation/README.md b/documentation/README.md index 8fc6cd6bd..20e8325a8 100644 --- a/documentation/README.md +++ b/documentation/README.md @@ -14,26 +14,12 @@ To build the docs locally, you will have to first install [uv](https://github.co Once that is done, you can run the following commands (while inside this folder): ```bash -uv sync # You only need to run this once, it will download all the necessary python packages -PDF=1 uv run mkdocs serve -a localhost:8888 # This will build the site and serve it on localhost:8888 +uv sync --frozen # You only need to run this once, it will download all the necessary python packages +uv run zensical serve -a localhost:8888 # This will build the site and serve it on localhost:8888 ``` Open [localhost:8888](http://localhost:8888/) in your browser and you will see the docs. Every changes you make will rebuild the documentation. You can remove the `PDF=1` environement variable if you want to speed up the build process, but you will not generate the related PDF. - -### Versioning -We use the [mike](https://github.com/jimporter/mike) plugin to preserve multiple version of the docs. - -To use it, you can run the following command: -```bash -PDF=1 uv run mike deploy -u dev # This will create the site and push the changes in the gh-pages branch -uv run mike serve -a localhost:8888 # Serve the site on localhost:8888 -``` -Check the official Github page of the plugin for more information on how it works. - ## Images and Assets -Images and assets are saved in the `_attachments` folder closest to the documentation file that calls for the image. If an image is used in multiple different places, then it should be put in `_attachements` folder in the `docs/` root directory. - -## Github workflow -We have Github workflows set up to automatically rebuild the docs when the `develop` branch receives changes, and when a new tag is created for the `master` branch. \ No newline at end of file +Images and assets are saved in the `_attachments` folder closest to the documentation file that calls for the image. If an image is used in multiple different places, then it should be put in `_attachements` folder in the `docs/` root directory. \ No newline at end of file diff --git a/documentation/main.py b/documentation/main.py deleted file mode 100644 index 04325bc6b..000000000 --- a/documentation/main.py +++ /dev/null @@ -1,6 +0,0 @@ -def main(): - print("Hello from mkdocs!") - - -if __name__ == "__main__": - main() From eb6fdacc40ab6800022f8946e1c5b35cae41a311 Mon Sep 17 00:00:00 2001 From: Obada Haddad Date: Fri, 6 Mar 2026 09:07:35 +0100 Subject: [PATCH 12/32] update the documentation to use system font instead of google fonts --- documentation/zensical.toml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/documentation/zensical.toml b/documentation/zensical.toml index f3cfc6b66..1626492a7 100644 --- a/documentation/zensical.toml +++ b/documentation/zensical.toml @@ -91,7 +91,7 @@ features = [ "navigation.instant", "navigation.instant.progress", ] - +font = false # Palette toggle for automatic mode [[project.theme.palette]] media = "(prefers-color-scheme)" @@ -117,11 +117,12 @@ toggle.name = "Switch to system preference" anchor_linenums = true line_spans = "__span" pygments_lang_class = true +auto_title = true [project.markdown_extensions.pymdownx.inlinehilite] [project.markdown_extensions.pymdownx.snippets] [project.markdown_extensions.pymdownx.superfences] custom_fences = [ - { name = "mermaid", class = "mermaid", format = "pymdownx.superfences.fence_code_format" } + { name = "mermaid", class = "mermaid", format = "pymdownx.superfences.fence_code_format" } ] [project.markdown_extensions.admonition] [project.markdown_extensions.pymdownx.details] @@ -146,14 +147,14 @@ configurations = [ "setup/extensions/*" ]} ] - +[project.markdown_extensions.pymdownx.smartsymbols] # Extras [project.extra] footer_links = [ - {"text" = "Governance Document"}, - {"href" = "https://github.com/codalab/codabench/blob/master/documentation/GOVERNANCE.md"}, - {"text" = "Privacy Policy"}, - {"href" = "https://github.com/codalab/codabench/blob/master/documentation/PRIVACY.md"}, - {"text" = "About"}, - {"href" = "https://github.com/codalab/codabench/blob/master/documentation/ABOUT.md"} + {"text" = "Governance Document"}, + {"href" = "https://github.com/codalab/codabench/blob/master/documentation/GOVERNANCE.md"}, + {"text" = "Privacy Policy"}, + {"href" = "https://github.com/codalab/codabench/blob/master/documentation/PRIVACY.md"}, + {"text" = "About"}, + {"href" = "https://github.com/codalab/codabench/blob/master/documentation/ABOUT.md"} ] \ No newline at end of file From 274a4546f24ef711f983bf1cb82aa559980ca5e2 Mon Sep 17 00:00:00 2001 From: Obada Haddad Date: Mon, 9 Mar 2026 14:53:31 +0100 Subject: [PATCH 13/32] rebase and update branch with new documentation --- documentation/zensical.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/documentation/zensical.toml b/documentation/zensical.toml index 1626492a7..ccd47bfd2 100644 --- a/documentation/zensical.toml +++ b/documentation/zensical.toml @@ -63,7 +63,12 @@ nav = [ {"Submissions and Participants count (version < 1.14.0)" = "Developers_and_Administrators/Upgrading_Codabench/Submissions-and-Participants-Counts.md"}, {"Homepage Counters (version < 1.15.0)" = "Developers_and_Administrators/Upgrading_Codabench/Homepage-counters.md"}, {"User Removal (version < 1.17.0)" = "Developers_and_Administrators/Upgrading_Codabench/User-removal.md"}, - {"Database size fix (version < 1.18.0)" = "Developers_and_Administrators/Upgrading_Codabench/Database-size-fixes.md"} + {"Database size fix (version < 1.18.0)" = "Developers_and_Administrators/Upgrading_Codabench/Database-size-fixes.md"}, + {"Hide Output update (version < 1.19.0)" = "Developers_and_Administrators/Upgrading_Codabench/Hide-output.md"}, + {"Django 3 Upgrades (version < 1.20.0)" = "Developers_and_Administrators/Upgrading_Codabench/Django-3.md"}, + {"Minio Image Upgrade (version < 1.21.0)" = "Developers_and_Administrators/Upgrading_Codabench/Minio-image.md"}, + {"Docker-Py (version < 1.22.0)" = "Developers_and_Administrators/Upgrading_Codabench/Docker-py.md"}, + {"Django 4 Upgrades (version < 1.23.0)" = "Developers_and_Administrators/Upgrading_Codabench/Django-4.md"} ]} ]}, {"Newsletters Archive" = [ From ecc6591046a820042b7483b434159b80e8a3ddca Mon Sep 17 00:00:00 2001 From: Obada Haddad Date: Mon, 9 Mar 2026 14:54:16 +0100 Subject: [PATCH 14/32] remove mkdocs.yml; update zensical.toml --- documentation/mkdocs.yml | 177 ------------------------------------ documentation/zensical.toml | 5 +- 2 files changed, 3 insertions(+), 179 deletions(-) delete mode 100644 documentation/mkdocs.yml diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml deleted file mode 100644 index 341169c8f..000000000 --- a/documentation/mkdocs.yml +++ /dev/null @@ -1,177 +0,0 @@ -site_name: Codabench Docs -repo_url: https://github.com/codalab/codabench -copyright: Apache-2.0 -edit_uri: edit/develop/documentation/docs - -theme: - name: material - #logo: assets/favicon.ico - #favicon: assets/favicon.ico - custom_dir: overrides - features: - - content.code.copy - - navigation.tabs - - navigation.tabs.sticky - - navigation.indexes - - search.share - - navigation.footer - - navigation.top - - content.action.edit - - content.action.view - - navigation.expand - palette: - # Palette toggle for light mode - - scheme: slate - toggle: - icon: material/brightness-7 - name: Switch to dark mode - primary: indigo - accent: orange - - # Palette toggle for dark mode - - scheme: default - toggle: - icon: material/brightness-4 - name: Switch to light mode - - icon: - repo: fontawesome/brands/github - -markdown_extensions: - - pymdownx.highlight: - anchor_linenums: true - line_spans: __span - pygments_lang_class: true - - pymdownx.inlinehilite - - pymdownx.snippets - - pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format - - tables - - attr_list - - abbr - - toc: - permalink: true - - admonition - - pymdownx.details - - pymdownx.tabbed: - alternate_style: true - - pymdownx.emoji: - emoji_index: !!python/name:material.extensions.emoji.twemoji - emoji_generator: !!python/name:material.extensions.emoji.to_svg - -plugins: - - search - - to-pdf: - enabled_if_env: PDF - download_link: header - author: Codabench Team - copyright: Apache-2.0 - cover_subtitle: Codabench Docs PDF - output_path: codabench-docs.pdf - toc_level: 3 - - # Version docs (with git) - - mike: - # These fields are all optional; the defaults are as below... - alias_type: symlink - redirect_template: null - deploy_prefix: "" - canonical_version: null - version_selector: true - css_dir: css - javascript_dir: js - - - open-in-new-tab: - add_icon: true - - # Image zoom - - glightbox - - - privacy - -extra: - version: - provider: mike - alias: true - footer_links: - - text: "Governance Document" - href: "https://github.com/codalab/codabench/blob/master/documentation/GOVERNANCE.md" - - text: "Privacy Policy" - href: "https://github.com/codalab/codabench/blob/master/documentation/PRIVACY.md" - - text: "About" - href: "https://github.com/codalab/codabench/blob/master/documentation/ABOUT.md" - -nav: - - Home: index.md - - Participants: - - Participating in a Competition: Participants/User_Participating-in-a-Competition.md - - List of Current Benchmarks and Competitions: https://www.codabench.org/competitions/public/?page=1 - - Organizers: - - Benchmark Creation: - - Getting Started Tutorial: Organizers/Benchmark_Creation/Getting-started-with-Codabench.md - - Advanced Tutorial: Organizers/Benchmark_Creation/Advanced-Tutorial.md - - How to Transition from Codalab to Codabench?: Organizers/Benchmark_Creation/How-to-transition-from-CodaLab-to-Codabench.md - - Competition Creation: Organizers/Benchmark_Creation/Competition-Creation.md - - Competition Creation Form: Organizers/Benchmark_Creation/Competition-Creation-Form.md - - Competition Creation Bundle: Organizers/Benchmark_Creation/Competition-Creation-Bundle.md - - Competition Bundle Structure: Organizers/Benchmark_Creation/Competition-Bundle-Structure.md - - YAML Structure: Organizers/Benchmark_Creation/Yaml-Structure.md - - Competition Docker Image: Organizers/Benchmark_Creation/Competition-docker-image.md - - Dataset Competition Creation and participate instruction: Organizers/Benchmark_Creation/Dataset-competition-creation-and-participate-instruction.md - - Leaderboard Features: Organizers/Benchmark_Creation/Leaderboard-Functionality.md - - Competition Examples: Organizers/Benchmark_Creation/Benchmark-Examples.md - - Public Tasks and Tasks Sharing: Organizers/Benchmark_Creation/Public-Tasks-and-Tasks-Sharing.md - - Detailed Results and Visualization: Organizers/Benchmark_Creation/Detailed-Results-and-Visualizations.md - - Running a Benchmarks: - - Benchmark Management & List Page: Organizers/Running_a_benchmark/Competition-Management-&-List.md - - Benchmark Detail Page: Organizers/Running_a_benchmark/Competition-Detail-Page.md - - Ressource Management Submissions, Datasets/Programs, Tasks and Competition Bundles: Organizers/Running_a_benchmark/Resource-Management.md - - Update programs or data: Organizers/Running_a_benchmark/Update-programs-or-data.md - - Queue Management: Organizers/Running_a_benchmark/Queue-Management.md - - Compute Worker Management & Setup: Organizers/Running_a_benchmark/Compute-Worker-Management---Setup.md - - Compute Worker Management with Podman: Organizers/Running_a_benchmark/Compute-worker-installation-with-Podman.md - - Server Status: Organizers/Running_a_benchmark/Server-status-page.md - - Developers and Administrators: - - Codabench Basic Installation Guide: Developers_and_Administrators/Codabench-Installation.md - - How to Deploy a Server: Developers_and_Administrators/How-to-deploy-Codabench-on-your-server.md - - Administrative Procedures: Developers_and_Administrators/Administrator-procedures.md - - Codabench Docker Architecture: Developers_and_Administrators/Codabench-Architecture.md - - Submission Docker Container Layout: Developers_and_Administrators/Submission-Docker-Container-Layout.md - - Backups - Automating Creation and Restoring: Developers_and_Administrators/Creating-and-Restoring-from-Backup.md - - Submission Process Overview: Developers_and_Administrators/Submission-Process-Overview.md - - Robot Submissions: Developers_and_Administrators/Robot-submissions.md - - Adding Tests: Developers_and_Administrators/Adding-e2e-tests.md - - Running Tests: Developers_and_Administrators/Running-tests.md - - Automation: Developers_and_Administrators/Automating-with-Selenium.md - - Manual Validation: Developers_and_Administrators/Manual-validation.md - - Validation and deplyement of pull requests: Developers_and_Administrators/Validation-and-deployment-of-pull-requests.md - - Upgrading Codabench: - - Developers_and_Administrators/Upgrading_Codabench/index.md - - Upgrade RabbitMQ (version < 1.0.0): Developers_and_Administrators/Upgrading_Codabench/Upgrade-RabbitMQ.md - - Create new logos for each competitions (version < 1.4.1): Developers_and_Administrators/Upgrading_Codabench/Create-new-logos-for-each-competition.md - - Worker docker image manual update (version < 1.3.1): Developers_and_Administrators/Upgrading_Codabench/Worker-Docker-Image-manual-update.md - - Add line in .env file for default worker queue duration (version < 1.7.0): Developers_and_Administrators/Upgrading_Codabench/Add-line-into-.env-for-default-queue-worker-duration.md - - Uncomment a line in your .env file (version < 1.8.0): Developers_and_Administrators/Upgrading_Codabench/Uncomment-a-line-in-your-.env-file.md - - Rebuilding all docker images (version < 1.9.2): Developers_and_Administrators/Upgrading_Codabench/Rebuilding-all-docker-images.md - - Move the latest storage_inconsistency files from the logs folder to var/logs (version < 1.12.0): Developers_and_Administrators/Upgrading_Codabench/Move-the-last-storage_inconsistency-files-from--logs-folder-to--var-logs--folder.md - - Submissions and Participants count (version < 1.14.0): Developers_and_Administrators/Upgrading_Codabench/Submissions-and-Participants-Counts.md - - Homepage Counters (version < 1.15.0): Developers_and_Administrators/Upgrading_Codabench/Homepage-counters.md - - User Removal (version < 1.17.0): Developers_and_Administrators/Upgrading_Codabench/User-removal.md - - Database size fix (version < 1.18.0): Developers_and_Administrators/Upgrading_Codabench/Database-size-fixes.md - - Hide Output update (version < 1.19.0): Developers_and_Administrators/Upgrading_Codabench/Hide-output.md - - Django 3 Upgrades (version < 1.20.0): Developers_and_Administrators/Upgrading_Codabench/Django-3.md - - Minio Image Upgrade (version < 1.21.0): Developers_and_Administrators/Upgrading_Codabench/Minio-image.md - - Docker-Py (version < 1.22.0): Developers_and_Administrators/Upgrading_Codabench/Docker-py.md - - Django 4 Upgrades (version < 1.23.0): Developers_and_Administrators/Upgrading_Codabench/Django-4.md - - Postgres 18 upgrade (version < 1.24.0): Developers_and_Administrators/Upgrading_Codabench/Postgres-18.md - - Newsletters Archive: - - 2024: Newsletters_Archive/CodaLab-in-2024.md - - 2025: Newsletters_Archive/CodaLab-in-2025.md - - How you can contribue: - - Contribute/index.md - - Contribute/contributing.md - - FAQ: Project_CodaBench_FAQ.md - - Contact Us: contact-us.md diff --git a/documentation/zensical.toml b/documentation/zensical.toml index ccd47bfd2..2ce33bfc9 100644 --- a/documentation/zensical.toml +++ b/documentation/zensical.toml @@ -22,7 +22,7 @@ nav = [ {"Competition Docker Image" = "Organizers/Benchmark_Creation/Competition-docker-image.md"}, {"Dataset Competition Creation and participate instruction" = "Organizers/Benchmark_Creation/Dataset-competition-creation-and-participate-instruction.md"}, {"Leaderboard Features" = "Organizers/Benchmark_Creation/Leaderboard-Functionality.md"}, - {"Competition Examples" = "https://github.com/codalab/competition-examples/tree/master/codabench"}, + {"Competition Examples" = "Organizers/Benchmark_Creation/Benchmark-Examples.md"}, {"Public Tasks and Tasks Sharing" = "Organizers/Benchmark_Creation/Public-Tasks-and-Tasks-Sharing.md"}, {"Detailed Results and Visualization" = "Organizers/Benchmark_Creation/Detailed-Results-and-Visualizations.md"}, ]}, @@ -68,7 +68,8 @@ nav = [ {"Django 3 Upgrades (version < 1.20.0)" = "Developers_and_Administrators/Upgrading_Codabench/Django-3.md"}, {"Minio Image Upgrade (version < 1.21.0)" = "Developers_and_Administrators/Upgrading_Codabench/Minio-image.md"}, {"Docker-Py (version < 1.22.0)" = "Developers_and_Administrators/Upgrading_Codabench/Docker-py.md"}, - {"Django 4 Upgrades (version < 1.23.0)" = "Developers_and_Administrators/Upgrading_Codabench/Django-4.md"} + {"Django 4 Upgrades (version < 1.23.0)" = "Developers_and_Administrators/Upgrading_Codabench/Django-4.md"}, + {"Postgres 18 upgrade (version < 1.24.0)" = "Developers_and_Administrators/Upgrading_Codabench/Postgres-18.md"} ]} ]}, {"Newsletters Archive" = [ From 5f9727b5e0e02880a4f9df4714cd01745d81cb82 Mon Sep 17 00:00:00 2001 From: Obada Haddad Date: Tue, 5 May 2026 15:18:12 +0200 Subject: [PATCH 15/32] rebase; add mike for versioning; change workflows to include mike --- .github/workflows/ghPages-dev.yml | 25 +++++ .github/workflows/ghPages-prod.yml | 36 +++---- documentation/pyproject.toml | 4 + documentation/uv.lock | 152 ++++++++++++++++++++++++----- documentation/zensical.toml | 4 +- 5 files changed, 176 insertions(+), 45 deletions(-) create mode 100644 .github/workflows/ghPages-dev.yml diff --git a/.github/workflows/ghPages-dev.yml b/.github/workflows/ghPages-dev.yml new file mode 100644 index 000000000..9616e1fb5 --- /dev/null +++ b/.github/workflows/ghPages-dev.yml @@ -0,0 +1,25 @@ +name: github-pages-dev +on: + push: + branches: + - develop +permissions: + contents: write +jobs: + deploy-dev: + runs-on: ubuntu-latest + defaults: + run: + working-directory: documentation/ + steps: + - uses: actions/checkout@v4 + - name: Configure Git Credentials + run: | + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - run: curl -LsSf https://astral.sh/uv/install.sh | sh + - run: /home/runner/.local/bin/uv sync + - run: git fetch origin gh-pages --depth=1 && PDF=1 /home/runner/.local/bin/uv run mike deploy -u dev --push \ No newline at end of file diff --git a/.github/workflows/ghPages-prod.yml b/.github/workflows/ghPages-prod.yml index d4e64e59b..b12be4cd6 100644 --- a/.github/workflows/ghPages-prod.yml +++ b/.github/workflows/ghPages-prod.yml @@ -1,29 +1,25 @@ -name: Documentation +name: github-pages-prod on: push: - branches: - - master - - main + tags: + - '*' permissions: - contents: read - pages: write - id-token: write + contents: write jobs: - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} + deploy-prod: runs-on: ubuntu-latest + defaults: + run: + working-directory: documentation/ steps: - - uses: actions/configure-pages@v5 - - uses: actions/checkout@v5 + - uses: actions/checkout@v4 + - name: Configure Git Credentials + run: | + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com - uses: actions/setup-python@v5 with: python-version: 3.x - - run: pip install zensical - - run: zensical build --clean - - uses: actions/upload-pages-artifact@v4 - with: - path: site - - uses: actions/deploy-pages@v4 - id: deployment \ No newline at end of file + - run: curl -LsSf https://astral.sh/uv/install.sh | sh + - run: /home/runner/.local/bin/uv sync + - run: git fetch origin gh-pages --depth=1 && PDF=1 /home/runner/.local/bin/uv run mike deploy -u ${{ github.ref_name }} latest --push \ No newline at end of file diff --git a/documentation/pyproject.toml b/documentation/pyproject.toml index 89508bc21..ae91862df 100644 --- a/documentation/pyproject.toml +++ b/documentation/pyproject.toml @@ -5,5 +5,9 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.14" dependencies = [ + "mike", "zensical>=0.0.24", ] + +[tool.uv.sources] +mike = { git = "https://github.com/squidfunk/mike.git" } diff --git a/documentation/uv.lock b/documentation/uv.lock index 49b4ac5fe..aec2a9fde 100644 --- a/documentation/uv.lock +++ b/documentation/uv.lock @@ -4,14 +4,14 @@ requires-python = ">=3.14" [[package]] name = "click" -version = "8.3.1" +version = "8.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, ] [[package]] @@ -37,11 +37,27 @@ name = "documentation" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "mike" }, { name = "zensical" }, ] [package.metadata] -requires-dist = [{ name = "zensical", specifier = ">=0.0.24" }] +requires-dist = [ + { name = "mike", git = "https://github.com/squidfunk/mike.git" }, + { name = "zensical", specifier = ">=0.0.24" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] [[package]] name = "markdown" @@ -52,26 +68,76 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, ] +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mike" +version = "2.2.0+zensical.0.1.0" +source = { git = "https://github.com/squidfunk/mike.git#2d4ad799442f4592db8ad53b179bfb33db8c69ac" } +dependencies = [ + { name = "jinja2" }, + { name = "pyparsing" }, + { name = "verspec" }, + { name = "zensical" }, +] + [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] name = "pymdown-extensions" -version = "10.21" +version = "10.21.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/63/06673d1eb6d8f83c0ea1f677d770e12565fb516928b4109c9e2055656a9e/pymdown_extensions-10.21.tar.gz", hash = "sha256:39f4a020f40773f6b2ff31d2cd2546c2c04d0a6498c31d9c688d2be07e1767d5", size = 853363, upload-time = "2026-02-15T20:44:06.748Z" } +sdist = { url = "https://files.pythonhosted.org/packages/df/08/f1c908c581fd11913da4711ea7ba32c0eee40b0190000996bb863b0c9349/pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc", size = 853922, upload-time = "2026-03-29T15:01:55.233Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl", hash = "sha256:91b879f9f864d49794c2d9534372b10150e6141096c3908a455e45ca72ad9d3f", size = 268877, upload-time = "2026-02-15T20:44:05.464Z" }, + { url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] [[package]] @@ -100,30 +166,68 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "verspec" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/44/8126f9f0c44319b2efc65feaad589cadef4d77ece200ae3c9133d58464d0/verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e", size = 27123, upload-time = "2020-11-30T02:24:09.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31", size = 19640, upload-time = "2020-11-30T02:24:08.387Z" }, +] + [[package]] name = "zensical" -version = "0.0.24" +version = "0.0.40" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "deepmerge" }, + { name = "jinja2" }, { name = "markdown" }, { name = "pygments" }, { name = "pymdown-extensions" }, { name = "pyyaml" }, + { name = "tomli" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3b/96/9c6cbdd7b351d1023cdbbcf7872d4cb118b0334cfe5821b99e0dd18e3f00/zensical-0.0.24.tar.gz", hash = "sha256:b5d99e225329bf4f98c8022bdf0a0ee9588c2fada7b4df1b7b896fcc62b37ec3", size = 3840688, upload-time = "2026-02-26T09:43:44.557Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/a6/88062f7e235f58a5f05d82005fc35d9dbaed27c024fe9ffae5bce7f33661/zensical-0.0.40.tar.gz", hash = "sha256:5c294751977a664614cb84e987186ad8e282af77ce0d0d800fe48ee57791279d", size = 3920555, upload-time = "2026-05-04T16:19:07.962Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/aa/b8201af30e376a67566f044a1c56210edac5ae923fd986a836d2cf593c9c/zensical-0.0.24-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d390c5453a5541ca35d4f9e1796df942b6612c546e3153dd928236d3b758409a", size = 12263407, upload-time = "2026-02-26T09:43:14.716Z" }, - { url = "https://files.pythonhosted.org/packages/78/8e/3d910214471ade604fd39b080db3696864acc23678b5b4b8475c7dbfd2ce/zensical-0.0.24-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:81ac072869cf4d280853765b2bfb688653da0dfb9408f3ab15aca96455ab8223", size = 12142610, upload-time = "2026-02-26T09:43:17.546Z" }, - { url = "https://files.pythonhosted.org/packages/cf/d7/eb0983640aa0419ddf670298cfbcf8b75629b6484925429b857851e00784/zensical-0.0.24-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5eb1dfa84cae8e960bfa2c6851d2bc8e9710c4c4c683bd3aaf23185f646ae46", size = 12508380, upload-time = "2026-02-26T09:43:20.114Z" }, - { url = "https://files.pythonhosted.org/packages/a3/04/4405b9e6f937a75db19f0d875798a7eb70817d6a3bec2a2d289a2d5e8aea/zensical-0.0.24-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:57d7c9e589da99c1879a1c703e67c85eaa6be4661cdc6ce6534f7bb3575983f4", size = 12440807, upload-time = "2026-02-26T09:43:22.679Z" }, - { url = "https://files.pythonhosted.org/packages/12/dc/a7ca2a4224b3072a2c2998b6611ad7fd4f8f131ceae7aa23238d97d26e22/zensical-0.0.24-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42fcc121c3095734b078a95a0dae4d4924fb8fbf16bf730456146ad6cab48ad0", size = 12782727, upload-time = "2026-02-26T09:43:25.347Z" }, - { url = "https://files.pythonhosted.org/packages/42/37/22f1727da356ed3fcbd31f68d4a477f15c232997c87e270cfffb927459ac/zensical-0.0.24-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4a2a051b9f49561031a2986ace502326f82d9a401ddf125530d30025fdd4", size = 12547616, upload-time = "2026-02-26T09:43:28.031Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ff/c75ff111b8e12157901d00752beef9d691dbb5a034b6a77359972262416a/zensical-0.0.24-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e5fea3bb61238dba9f930f52669db67b0c26be98e1c8386a05eb2b1e3cb875dc", size = 12684883, upload-time = "2026-02-26T09:43:30.642Z" }, - { url = "https://files.pythonhosted.org/packages/b9/92/4f6ea066382e3d068d3cadbed99e9a71af25e46c84a403e0f747960472a2/zensical-0.0.24-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:75eef0428eec2958590633fdc82dc2a58af124879e29573aa7e153b662978073", size = 12713825, upload-time = "2026-02-26T09:43:33.273Z" }, - { url = "https://files.pythonhosted.org/packages/bc/fb/bf735b19bce0034b1f3b8e1c50b2896ebbd0c5d92d462777e759e78bb083/zensical-0.0.24-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:3c6b39659156394ff805b4831dac108c839483d9efa4c9b901eaa913efee1ac7", size = 12854318, upload-time = "2026-02-26T09:43:35.632Z" }, - { url = "https://files.pythonhosted.org/packages/7e/28/0ddab6c1237e3625e7763ff666806f31e5760bb36d18624135a6bb6e8643/zensical-0.0.24-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9eef82865a18b3ca4c3cd13e245dff09a865d1da3c861e2fc86eaa9253a90f02", size = 12818270, upload-time = "2026-02-26T09:43:37.749Z" }, - { url = "https://files.pythonhosted.org/packages/2a/93/d2cef3705d4434896feadffb5b3e44744ef9f1204bc41202c1b84a4eeef6/zensical-0.0.24-cp310-abi3-win32.whl", hash = "sha256:f4d0ff47d505c786a26c9332317aa3e9ad58d1382f55212a10dc5bafcca97864", size = 11857695, upload-time = "2026-02-26T09:43:39.906Z" }, - { url = "https://files.pythonhosted.org/packages/f1/26/9707587c0f6044dd1e1cc5bc3b9fa5fed81ce6c7bcdb09c21a9795e802d9/zensical-0.0.24-cp310-abi3-win_amd64.whl", hash = "sha256:e00a62cf04526dbed665e989b8f448eb976247f077a76dfdd84699ace4aa3ac3", size = 12057762, upload-time = "2026-02-26T09:43:42.627Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c4/3066f4442923ca1e49269147b70ca7c84467524e8f5228724693b9ac85c2/zensical-0.0.40-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b65a7143c9c6a460880bf3e65b777952bd2dcede9dd17a6c6bac9b4a0686ad9b", size = 12691533, upload-time = "2026-05-04T16:18:31.72Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cb/03e961cbd01620ea91aeb835b0b4e8848c7bcdf5a799a620fb3e57bfc277/zensical-0.0.40-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:045bdcb6d00a11ddcab7d379d0d986cdf78dba8e9287d8e628ef11958241507d", size = 12556486, upload-time = "2026-05-04T16:18:35.278Z" }, + { url = "https://files.pythonhosted.org/packages/60/76/7dde50220808bdc5f5e63b97866a684418410b3cae9d00cdae1d449bcc20/zensical-0.0.40-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d48ec476c2e8ce3f8585a1278083aabc35ec80361f2c4fc4a53b9a525778f7fc", size = 12935602, upload-time = "2026-05-04T16:18:38.308Z" }, + { url = "https://files.pythonhosted.org/packages/51/55/6c8ef951c390b42249738f4338498e7a1fd64ff09e44d7cc19f5c948c45b/zensical-0.0.40-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:48c38e0ae314c25f2e5e64210bbad9be6e970f2d40fe9da106586ad90ce5e85e", size = 12904314, upload-time = "2026-05-04T16:18:41.007Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ae/95008f5dc2ee441efcdc2fab36ff29ce24d7477e53390fc340c8add39342/zensical-0.0.40-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f25f62dcd61f6306cab890dfa34c81d2709f5db290b4c3f2675343771db28c90", size = 13269946, upload-time = "2026-05-04T16:18:44.387Z" }, + { url = "https://files.pythonhosted.org/packages/b9/96/cdbb2bf04255ccaaa07861bdda1ee8dd1630d2233fc2f09636abbd5e084c/zensical-0.0.40-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:168fe3489dd93ae92978b4db11d9300c63e10d382b81634232c2872ce9e746c2", size = 12974962, upload-time = "2026-05-04T16:18:47.462Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ce/66e86f89fc15bbe667794ba67d7efc8fa72fe7a1be19e1efb4246ff55442/zensical-0.0.40-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8652ba203bd588ebf2d66bda4457a4a7d8e193c886960859c75081c0e3b946de", size = 13111599, upload-time = "2026-05-04T16:18:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/87/76/3d71ebdabb02d79a5c523b5e646141c362c9559947078c8d56a9f3bd7a30/zensical-0.0.40-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:9ffa6cf208b7ab6b771703be827d4d8c7f07f173abeffb35a8015a0b832b2a40", size = 13175406, upload-time = "2026-05-04T16:18:53.209Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6a/2bb5f730786d590f02cb0fef796c148d5ac0d5c1556f2d78c987ad4e1346/zensical-0.0.40-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:7101ba0c739c78bc3a57d22130b59b9e6fdf96c21c8a6b4244070de6b34527d4", size = 13324783, upload-time = "2026-05-04T16:18:56.41Z" }, + { url = "https://files.pythonhosted.org/packages/2f/8c/1d2ba1454360ee948dd0f0807b048c076d9578d0d9ebba2a438ecfa9f82f/zensical-0.0.40-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:39bf728a68a5418feeda8f3385cd1063fdb8d896a6812c3dede4267b2868df12", size = 13260045, upload-time = "2026-05-04T16:18:59.244Z" }, + { url = "https://files.pythonhosted.org/packages/6c/61/efd51c5c5e15cfd5498d59df250f60294cc44d36d8ce4dc2a76fa3669c2f/zensical-0.0.40-cp310-abi3-win32.whl", hash = "sha256:bc750c3ba8d11833d9b9ac8fc14adc3435225b6d17314a21a91eb60209511ca5", size = 12244913, upload-time = "2026-05-04T16:19:02.219Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9e/f3f2118fbcfd1c2dc705491c8864c596b1a748b67ffe2a024e512b9201ab/zensical-0.0.40-cp310-abi3-win_amd64.whl", hash = "sha256:c5c86ac468df2dfe515ff54ffa97725c38226f1e5c970059b7e88078abab89ab", size = 12475762, upload-time = "2026-05-04T16:19:05.025Z" }, ] diff --git a/documentation/zensical.toml b/documentation/zensical.toml index 2ce33bfc9..9382bb149 100644 --- a/documentation/zensical.toml +++ b/documentation/zensical.toml @@ -163,4 +163,6 @@ footer_links = [ {"href" = "https://github.com/codalab/codabench/blob/master/documentation/PRIVACY.md"}, {"text" = "About"}, {"href" = "https://github.com/codalab/codabench/blob/master/documentation/ABOUT.md"} -] \ No newline at end of file +] +[project.extra.version] +provider = "mike" From ea61786c7c7d67421cd75a49e82f3c5214449933 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Fri, 27 Mar 2026 01:53:34 +0500 Subject: [PATCH 16/32] reorganzing and cleaning of compute worker rebasing --- compute_worker/compute_worker.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/compute_worker/compute_worker.py b/compute_worker/compute_worker.py index 147ed2d2f..c82fea64e 100644 --- a/compute_worker/compute_worker.py +++ b/compute_worker/compute_worker.py @@ -114,6 +114,7 @@ def to_bool(val): WORKER_BUNDLE_URL_REWRITE = get("WORKER_BUNDLE_URL_REWRITE", "").strip() + # ----------------------------------------------- # Program Kind # ----------------------------------------------- @@ -264,7 +265,6 @@ def setup_celery_logging(**kwargs): ), ] - # ----------------------------------------------- # Exceptions # ----------------------------------------------- @@ -291,7 +291,9 @@ def rewrite_bundle_url_if_needed(url): Example: http://localhost:9000|http://minio:9000 """ + rule = Settings.WORKER_BUNDLE_URL_REWRITE + if not rule or "|" not in rule: return url src, dst = rule.split("|", 1) @@ -1229,6 +1231,8 @@ def prepare(self): # Only during prediction step do we want to announce "preparing" self._update_status(SubmissionStatus.PREPARING, extra_information=f"ingestion_hostname-{hostname}") + + # Setup cache and prune if it's out of control self._prep_cache_dir() @@ -1301,25 +1305,24 @@ def start(self): "error_message": error_message, "is_scoring": self.is_scoring, } + # Cleanup containers containers_to_kill = [ self.ingestion_container_name, self.program_container_name ] - logger.debug( - "Trying to kill and remove container " + str(containers_to_kill) - ) + logger.debug("Trying to kill and remove container " + str(containers_to_kill)) + for container in containers_to_kill: try: client.remove_container(str(container), force=True) except docker.errors.APIError as e: logger.error(e) except Exception as e: - logger.error( - f"There was a problem killing {containers_to_kill}: {e}" - ) + logger.error(f"There was a problem killing {containers_to_kill}: {e}") if Settings.LOG_LEVEL == Settings.LOG_LEVEL_DEBUG: logger.exception(e) + # Send data to be written to ingestion/scoring std_err self._update_submission(execution_time_limit_exceeded_data) # Send error through web socket to the frontend From 9e7e65e4b881dc53c9ce414be2a6eef92aa8c0c5 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Fri, 27 Mar 2026 02:02:02 +0500 Subject: [PATCH 17/32] minor updates rebasing --- compute_worker/compute_worker.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/compute_worker/compute_worker.py b/compute_worker/compute_worker.py index c82fea64e..f70cba522 100644 --- a/compute_worker/compute_worker.py +++ b/compute_worker/compute_worker.py @@ -114,7 +114,6 @@ def to_bool(val): WORKER_BUNDLE_URL_REWRITE = get("WORKER_BUNDLE_URL_REWRITE", "").strip() - # ----------------------------------------------- # Program Kind # ----------------------------------------------- @@ -293,7 +292,6 @@ def rewrite_bundle_url_if_needed(url): """ rule = Settings.WORKER_BUNDLE_URL_REWRITE - if not rule or "|" not in rule: return url src, dst = rule.split("|", 1) From 5b95ab444a990be582d81f2f3da20d6a2fef988a Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Tue, 31 Mar 2026 21:01:27 +0500 Subject: [PATCH 18/32] updates to compute worker to avoid duplicating submission during ingestion and making submission available during scoring. Also copying submission files to ingestion predictions i.e. /app/input/res to make sure already existing competitions do not break rebased --- compute_worker/compute_worker.py | 448 +++++++++++++++++-------------- src/apps/competitions/tasks.py | 9 +- 2 files changed, 246 insertions(+), 211 deletions(-) diff --git a/compute_worker/compute_worker.py b/compute_worker/compute_worker.py index f70cba522..d918fcd48 100644 --- a/compute_worker/compute_worker.py +++ b/compute_worker/compute_worker.py @@ -117,11 +117,9 @@ def to_bool(val): # ----------------------------------------------- # Program Kind # ----------------------------------------------- -# NOTE: This is not used, to be used in next PR class ProgramKind: INGESTION_PROGRAM = "ingestion_program" SCORING_PROGRAM = "scoring_program" - SUBMISSION = "submission" # ----------------------------------------------- @@ -355,7 +353,7 @@ def replace_legacy_metadata_command( command, kind, is_scoring, ingestion_only_during_scoring=False ): vars_to_replace = [ - ("$input", "/app/input_data" if kind == "ingestion" else "/app/input"), + ("$input", "/app/input_data" if kind == ProgramKind.INGESTION_PROGRAM else "/app/input"), ("$output", "/app/output"), ( "$program", @@ -476,24 +474,24 @@ def __init__(self, run_args): self.stdout, self.stderr, self.ingestion_stdout, self.ingestion_stderr = ( self._get_stdout_stderr_file_names(run_args) ) - self.ingestion_container_name = f"ingestion_{self.run_related_name}" - self.program_container_name = f"scoring_{self.run_related_name}" - self.program_data = run_args.get("program_data") - self.ingestion_program_data = run_args.get("ingestion_program") + # Setting up container names for ingestion, scoring and submission + self.ingestion_program_container_name = f"ingestion_{self.run_related_name}" + self.scoring_program_container_name = f"scoring_{self.run_related_name}" + + # Setting up ingestion, scoring and submission data + self.ingestion_program_data = run_args.get("ingestion_program_data") + self.scoring_program_data = run_args.get("scoring_program_data") + self.submission_data = run_args.get("submission_data") + self.input_data = run_args.get("input_data") self.reference_data = run_args.get("reference_data") - self.ingestion_only_during_scoring = run_args.get( - "ingestion_only_during_scoring" - ) + self.ingestion_only_during_scoring = run_args.get("ingestion_only_during_scoring") self.detailed_results_url = run_args.get("detailed_results_url") - # During prediction program will be the submission program, during scoring it will be the - # scoring program - self.program_exit_code = None self.ingestion_program_exit_code = None - - self.program_elapsed_time = None - self.ingestion_elapsed_time = None + self.ingestion_program_elapsed_time = None + self.scoring_program_exit_code = None + self.scoring_program_elapsed_time = None # Socket connection to stream output of submission submission_api_url_parsed = urlparse(self.submissions_api_url) @@ -522,7 +520,8 @@ async def watch_detailed_results(self): start = time.time() expiration_seconds = 60 - while self.watch and self.completed_program_counter < 2: + expected_completed_program_counters = 1 + int(bool(self.ingestion_only_during_scoring)) + while self.watch and self.completed_program_counter < expected_completed_program_counters: if file_path: new_time = os.path.getmtime(file_path) if new_time != last_modified_time: @@ -774,6 +773,98 @@ def _get_bundle(self, url, destination, cache=True): # Return the zip file path for other uses, e.g. for creating a MD5 hash to identify it return bundle_file + def _create_container( + self, + container_name: str, + command: str, + volumes_host: list, + volumes_config: dict + ): + """ + Helper to create and configure a container for ingestion, scoring, or submission. + Returns the container object. + """ + + logger.info( + "Creating Container with: " + f"Container Name: {container_name} \n" + f"Command: {command} \n" + "Volumes config:" + ) + pprint(volumes_config) + + cap_drop_list = [ + "AUDIT_WRITE", + "CHOWN", + "DAC_OVERRIDE", + "FOWNER", + "FSETID", + "KILL", + "MKNOD", + "NET_BIND_SERVICE", + "NET_RAW", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_CHROOT", + ] + + # Configure whether or not we use the GPU. Also setting auto_remove to False because + if Settings.CONTAINER_ENGINE_EXECUTABLE == Settings.DOCKER: + security_options = ["no-new-privileges"] + else: + security_options = ["label=disable"] + + # Setting the device ID like this allows users to specify which gpu to use in the .env file, with all being the default if no value is given + device_id = [Settings.GPU_DEVICE] + if Settings.USE_GPU: + logger.info("Running the container with GPU capabilities") + host_config = client.create_host_config( + auto_remove=False, + cap_drop=cap_drop_list, + binds=volumes_config, + userns_mode="host", + security_opt=security_options, + device_requests=[ + { + "Driver": "cdi", + "DeviceIDs": device_id, + }, + ], + ) + else: + host_config = client.create_host_config( + auto_remove=False, + cap_drop=cap_drop_list, + binds=volumes_config, + userns_mode="host", + security_opt=security_options, + ) + + # Creating container + # COMPETITION_CONTAINER_NETWORK_DISABLED: Disable or not the competition container access to Internet (False by default) + # HTTP and HTTPS proxy for the competition container if needed + container = client.create_container( + self.container_image, + name=container_name, + host_config=host_config, + detach=False, + volumes=volumes_host, + command=command, + working_dir="/app/program", + environment=[ + "PYTHONUNBUFFERED=1", + "http_proxy=" + Settings.COMPETITION_CONTAINER_HTTP_PROXY, + "https_proxy=" + Settings.COMPETITION_CONTAINER_HTTPS_PROXY, + ], + network_disabled=Settings.COMPETITION_CONTAINER_NETWORK_DISABLED, + ) + + logger.debug("Created container: " + str(container)) + + return container + async def _run_container_engine_cmd(self, container, kind): """This runs a command and asynchronously writes the data to both a storage file and a socket @@ -902,13 +993,13 @@ async def _run_container_engine_cmd(self, container, kind): "data": logs_Unified[0], "stream": logs_Unified[0], "continue": True, - "location": self.stdout if kind == "program" else self.ingestion_stdout, + "location": self.stdout if kind == ProgramKind.SCORING_PROGRAM else self.ingestion_stdout, }, "stderr": { "data": logs_Unified[1], "stream": logs_Unified[1], "continue": True, - "location": self.stderr if kind == "program" else self.ingestion_stderr, + "location": self.stderr if kind == ProgramKind.SCORING_PROGRAM else self.ingestion_stderr, }, } @@ -937,129 +1028,85 @@ def _get_host_path(self, *paths): return path - async def _run_program_directory(self, program_dir, kind): + async def _run_program_directory(self, kind, program_dir): """ - Function responsible for running program directory + Function responsible for running + - ingestion program + - scoring program Args: - - program_dir : can be either ingestion program or program/submission - - kind : either `program` or `ingestion` + kind: `ingestion_program` or `scoring_program` + program_dir: path to the program to run """ - # If the directory doesn't even exist, move on + # Return if directory does not exist if not os.path.exists(program_dir): - logger.warning(f"{program_dir} not found, no program to execute") + logger.warning(f"{program_dir} for {kind} not found, no program to execute") # Communicate that the program is closing self.completed_program_counter += 1 return + # Find metadata file. + # Raise error if metadata is not found if os.path.exists(os.path.join(program_dir, "metadata.yaml")): metadata_path = "metadata.yaml" elif os.path.exists(os.path.join(program_dir, "metadata")): metadata_path = "metadata" else: - # Display a warning in logs when there is no metadata file in submission/program dir - if kind == "program": - logger.warning( - "Program directory missing metadata, assuming it's going to be handled by ingestion" - ) - # Copy submission files into prediction output - # This is useful for results submissions but wrongly uses storage - shutil.copytree(program_dir, self.output_dir) - return - else: - raise SubmissionException( - "Program directory missing 'metadata.yaml/metadata'" - ) + error_message = f"{program_dir} for {kind} missing 'metadata.yaml/metadata' file." + logger.error(error_message) + raise SubmissionException(error_message) + # Metadata file is found logger.info(f"Metadata path is {os.path.join(program_dir, metadata_path)}") + + # Reading metadata file to find command. + # Raise error if command is not found for ingestion or scoring with open(os.path.join(program_dir, metadata_path), "r") as metadata_file: - try: # try to find a command in the metadata, in other cases set metadata to None + command = None + try: metadata = yaml.safe_load(metadata_file.read()) logger.info(f"Metadata contains:\n {metadata}") - if isinstance(metadata, dict): # command found + if isinstance(metadata, dict): command = metadata.get("command") - else: - command = None + except yaml.YAMLError as e: + logger.error(f"Error parsing YAML file: {e}") - print(f"Error parsing YAML file: {e}") - command = None - if not command and kind == "ingestion": - raise SubmissionException( - "Program directory missing 'command' in metadata" - ) - elif not command: - logger.warning( - f"Warning: {program_dir} has no command in metadata, continuing anyway " - f"(may be meant to be consumed by an ingestion program)" - ) - return + + if not command: + raise SubmissionException(f"Missing 'command' for {kind} in metadata or metadata format is not correct!") + + # Prepare volumes_host and volumes_config volumes_host = [ self._get_host_path(program_dir), self._get_host_path(self.output_dir), self.data_dir, + self._get_host_path(self.root_dir, "submission") ] volumes_config = { - volumes_host[0]: { - "bind": "/app/program", - "mode": "z", - }, - volumes_host[1]: { - "bind": "/app/output", - "mode": "z", - }, - volumes_host[2]: { - "bind": "/app/data", - "mode": "ro", - }, + volumes_host[0]: {"bind": "/app/program", "mode": "z"}, + volumes_host[1]: {"bind": "/app/output", "mode": "z"}, + volumes_host[2]: {"bind": "/app/data", "mode": "ro"}, + volumes_host[3]: {"bind": "/app/ingested_program", "mode": "ro"}, } - if kind == "ingestion": - # program here is either scoring program or submission, depends on if this ran during Prediction or Scoring - if self.ingestion_only_during_scoring and self.is_scoring: - # submission program moved to 'input/res' with shutil.move() above - ingested_program_location = "input/res" - else: - ingested_program_location = "program" - volumes_host.extend( - [self._get_host_path(self.root_dir, ingested_program_location)] - ) - tempvolumeConfig = { - volumes_host[-1]: { - "bind": "/app/ingested_program", - } - } - volumes_config.update(tempvolumeConfig) - - if self.is_scoring: - # For scoring programs, we want to have a shared directory just in case we have an ingestion program. - # This will add the share dir regardless of ingestion or scoring, as long as we're `is_scoring` + # During Scoring: Add `shared` and `/app/input` to volumes_host and update volumes_config + if kind == ProgramKind.SCORING_PROGRAM: + # For scoring program, we want to have a shared directory just in case we have an ingestion program. volumes_host.extend([self._get_host_path(self.root_dir, "shared")]) - tempvolumeConfig = { - volumes_host[-1]: { - "bind": "/app/shared", - } - } - volumes_config.update(tempvolumeConfig) + volumes_config.update({volumes_host[-1]: {"bind": "/app/shared"}}) - # Input from submission (or submission + ingestion combo) + # Input dir for scoring program volumes_host.extend([self._get_host_path(self.input_dir)]) - tempvolumeConfig = { - volumes_host[-1]: { - "bind": "/app/input", - } - } - volumes_config.update(tempvolumeConfig) - - if self.input_data: - volumes_host.extend([self._get_host_path(self.root_dir, "input_data")]) - tempvolumeConfig = { - volumes_host[-1]: { - "bind": "/app/input_data", - } - } - volumes_config.update(tempvolumeConfig) + volumes_config.update({volumes_host[-1]: {"bind": "/app/input"}}) + + # During Ingestion: Add `/app/input_data` to volumes_host and update volumes_config + if kind == ProgramKind.INGESTION_PROGRAM: + # NOTE: self.input_data is valid when running an ingestion program and competition task has input data + if self.input_data: + volumes_host.extend([self._get_host_path(self.root_dir, "input_data")]) + volumes_config.update({volumes_host[-1]: {"bind": "/app/input_data"}}) # Handle Legacy competitions by replacing anything in the run command command = replace_legacy_metadata_command( @@ -1069,84 +1116,15 @@ async def _run_program_directory(self, program_dir, kind): ingestion_only_during_scoring=self.ingestion_only_during_scoring, ) - cap_drop_list = [ - "AUDIT_WRITE", - "CHOWN", - "DAC_OVERRIDE", - "FOWNER", - "FSETID", - "KILL", - "MKNOD", - "NET_BIND_SERVICE", - "NET_RAW", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_CHROOT", - ] - - # Configure whether or not we use the GPU. Also setting auto_remove to False because - if Settings.CONTAINER_ENGINE_EXECUTABLE == Settings.DOCKER: - security_options = ["no-new-privileges"] - else: - security_options = ["label=disable"] - - # Setting the device ID like this allows users to specify which gpu to use in the .env file, with all being the default if no value is given - device_id = [Settings.GPU_DEVICE] - if Settings.USE_GPU: - logger.info("Running the container with GPU capabilities") - host_config = client.create_host_config( - auto_remove=False, - cap_drop=cap_drop_list, - binds=volumes_config, - userns_mode="host", - security_opt=security_options, - device_requests=[ - { - "Driver": "cdi", - "DeviceIDs": device_id, - }, - ], - ) - else: - host_config = client.create_host_config( - auto_remove=False, - cap_drop=cap_drop_list, - binds=volumes_config, - userns_mode="host", - security_opt=security_options, - ) - - logger.info("Running container with command " + command) - container_name = ( - self.ingestion_container_name - if kind == "ingestion" - else self.program_container_name - ) - - # Creating container - # COMPETITION_CONTAINER_NETWORK_DISABLED: Disable or not the competition container access to Internet (False by default) - # HTTP and HTTPS proxy for the competition container if needed - container = client.create_container( - self.container_image, - name=container_name, - host_config=host_config, - detach=False, - volumes=volumes_host, + # Create container + container_name = self.ingestion_program_container_name if kind == ProgramKind.INGESTION_PROGRAM else self.scoring_program_container_name + container = self._create_container( + container_name=container_name, command=command, - working_dir="/app/program", - environment=[ - "PYTHONUNBUFFERED=1", - "http_proxy=" + Settings.COMPETITION_CONTAINER_HTTP_PROXY, - "https_proxy=" + Settings.COMPETITION_CONTAINER_HTTPS_PROXY, - ], - network_disabled=Settings.COMPETITION_CONTAINER_NETWORK_DISABLED, + volumes_host=volumes_host, + volumes_config=volumes_config ) - logger.debug("Created container: " + str(container)) - logger.info("Volume configuration of the container: ") - pprint(volumes_config) # This runs the container engine command and asynchronously passes data back via websocket try: return await self._run_container_engine_cmd(container, kind=kind) @@ -1221,6 +1199,30 @@ def _prep_cache_dir(self, max_size=Settings.MAX_CACHE_DIR_SIZE_GB): else: logger.info("Cache directory does not need to be pruned!") + def _copy_submission_to_input_res(self): + """ + Temporary backward-compatibility function. + + Earlier, scoring programs expected submission files in ingestion output: + /app/input/res/ + + Newer changes expose submission under: + /app/ingested_program/ + + To avoid breaking older scoring programs, we copy the submission + directory into input/res + """ + + submission_directory = os.path.join(self.root_dir, "submission") + ingestion_res_directory = os.path.join(self.root_dir, "input/res") + + # copy from submission_directory ingestion_res_directory + try: + shutil.copytree(submission_directory, ingestion_res_directory, dirs_exist_ok=True) + logger.info("Copied submission files to input/res successfully") + except Exception as e: + logger.error(f"Failed to copy submission to input/res: {e}") + def prepare(self): hostname = utils.nodenames.gethostname() if self.is_scoring: @@ -1238,8 +1240,9 @@ def prepare(self): # sub folder. bundles = [ # (url to file, relative folder destination) - (self.program_data, "program"), (self.ingestion_program_data, "ingestion_program"), + (self.scoring_program_data, "scoring_program"), + (self.submission_data, "submission"), (self.input_data, "input_data"), (self.reference_data, "input/ref"), ] @@ -1253,8 +1256,8 @@ def prepare(self): cache_this_bundle = path in ("input_data", "input/ref") zip_file = self._get_bundle(url, path, cache=cache_this_bundle) - # TODO: When we have `is_scoring_only` this needs to change... - if url == self.program_data and not self.is_scoring: + # Computing checksum of the submission file during ingestion run + if url == self.submission_data and not self.is_scoring: # We want to get a checksum of submissions so we can check if they are # a solution, or maybe match them against other submissions later logger.info(f"Beginning MD5 checksum of submission: {zip_file}") @@ -1262,6 +1265,11 @@ def prepare(self): logger.info(f"Checksum result: {checksum}") self._update_submission({"md5": checksum}) + # During scoring: copy submission files into "input/res" + if self.is_scoring: + # NOTE: Temporary compatibility hook (To be removed in the future) + self._copy_submission_to_input_res() + # For logging purposes let's dump file names for filename in glob.iglob(self.root_dir + "**/*.*", recursive=True): logger.info(filename) @@ -1272,19 +1280,42 @@ def prepare(self): self._update_status(SubmissionStatus.RUNNING) def start(self): - program_dir = os.path.join(self.root_dir, "program") + + logger.info(f"Preparing to run: {ProgramKind.SCORING_PROGRAM if self.is_scoring else ProgramKind.INGESTION_PROGRAM}") + + # Define directories for ingestion, scoring and submission ingestion_program_dir = os.path.join(self.root_dir, "ingestion_program") + scoring_program_dir = os.path.join(self.root_dir, "scoring_program") - logger.info("Running scoring program, and then ingestion program") loop = asyncio.new_event_loop() # Set the event loop for the gather asyncio.set_event_loop(loop) - gathered_tasks = asyncio.gather( - self._run_program_directory(program_dir, kind="program"), - self._run_program_directory(ingestion_program_dir, kind="ingestion"), - self.watch_detailed_results(), - return_exceptions=True, - ) + + tasks = [] + if self.is_scoring: + # During scoring, run scoring program directory + tasks.append( + self._run_program_directory(kind=ProgramKind.SCORING_PROGRAM, program_dir=scoring_program_dir) + ) + + # If ingestion_only_during_scoring is true, we also run ingestion program directory in parallel to scoring program + if self.ingestion_only_during_scoring: + tasks.append( + self._run_program_directory(kind=ProgramKind.INGESTION_PROGRAM, program_dir=ingestion_program_dir) + ) + + # During scoring we watch for detailed results + tasks.append( + self.watch_detailed_results() + ) + else: + # During ingestion we run ingestion program directory and submission directory + tasks.extend([ + self._run_program_directory(kind=ProgramKind.INGESTION_PROGRAM, program_dir=ingestion_program_dir), + ]) + + gathered_tasks = asyncio.gather(*tasks, return_exceptions=True) + task_results = [] # will store results/exceptions from gather signal.signal(signal.SIGALRM, alarm_handler) signal.alarm(self.execution_time_limit) @@ -1358,10 +1389,10 @@ def start(self): ) if return_code is None: logger.warning("No return code from Process. Killing it") - if kind == "ingestion": - containers_to_kill = self.ingestion_container_name + if kind == ProgramKind.INGESTION_PROGRAM: + containers_to_kill = self.ingestion_program_container_name else: - containers_to_kill = self.program_container_name + containers_to_kill = self.scoring_program_container_name try: client.kill(containers_to_kill) client.remove_container(containers_to_kill, force=True) @@ -1373,12 +1404,12 @@ def start(self): ) if Settings.LOG_LEVEL == Settings.LOG_LEVEL_DEBUG: logger.exception(e) - if kind == "program": - self.program_exit_code = return_code - self.program_elapsed_time = elapsed_time - elif kind == "ingestion": + if kind == ProgramKind.SCORING_PROGRAM: + self.scoring_program_exit_code = return_code + self.scoring_program_elapsed_time = elapsed_time + elif kind == ProgramKind.INGESTION_PROGRAM: self.ingestion_program_exit_code = return_code - self.ingestion_elapsed_time = elapsed_time + self.ingestion_program_elapsed_time = elapsed_time logger.info(f"[exited with {logs['returncode']}]") for key, value in logs.items(): if key not in ["stdout", "stderr"]: @@ -1393,12 +1424,15 @@ def start(self): if self.is_scoring: # Check if scoring program failed - program_results, _, _ = task_results + try: + program_results, _, _ = task_results + except Exception: + program_results, _ = task_results # Gather returns either normal values or exception instances when return_exceptions=True had_async_exc = isinstance( program_results, BaseException ) and not isinstance(program_results, asyncio.CancelledError) - program_rc = getattr(self, "program_exit_code", None) + program_rc = getattr(self, "scoring_program_exit_code", None) failed_rc = (program_rc is None) or (program_rc != 0) if had_async_exc or failed_rc: self._update_status( @@ -1453,11 +1487,11 @@ def push_output(self): """Output is pushed at the end of both prediction and scoring steps.""" # V1.5 compatibility, write program statuses to metadata file prog_status = { - "exitCode": self.program_exit_code, + "exitCode": self.scoring_program_exit_code, # for v1.5 compat, send `ingestion_elapsed_time` if no `program_elapsed_time` - "elapsedTime": self.program_elapsed_time or self.ingestion_elapsed_time, + "elapsedTime": self.scoring_program_elapsed_time or self.ingestion_program_elapsed_time, "ingestionExitCode": self.ingestion_program_exit_code, - "ingestionElapsedTime": self.ingestion_elapsed_time, + "ingestionElapsedTime": self.ingestion_program_elapsed_time, } logger.info(f"Metadata output: {prog_status}") diff --git a/src/apps/competitions/tasks.py b/src/apps/competitions/tasks.py index d72e9191a..ee35eeeb9 100644 --- a/src/apps/competitions/tasks.py +++ b/src/apps/competitions/tasks.py @@ -165,7 +165,7 @@ def _send_to_compute_worker(submission, is_scoring): if task.ingestion_program: if (task.ingestion_only_during_scoring and is_scoring) or (not task.ingestion_only_during_scoring and not is_scoring): - run_args['ingestion_program'] = make_url_sassy(task.ingestion_program.data_file.name) + run_args['ingestion_program_data'] = make_url_sassy(task.ingestion_program.data_file.name) if task.input_data and (not is_scoring or task.ingestion_only_during_scoring): run_args['input_data'] = make_url_sassy(task.input_data.data_file.name) @@ -175,9 +175,10 @@ def _send_to_compute_worker(submission, is_scoring): run_args['ingestion_only_during_scoring'] = task.ingestion_only_during_scoring - run_args['program_data'] = make_url_sassy( - path=submission.data.data_file.name if not is_scoring else task.scoring_program.data_file.name - ) + if is_scoring: + run_args['scoring_program_data'] = make_url_sassy(path=task.scoring_program.data_file.name) + + run_args['submission_data'] = make_url_sassy(path=submission.data.data_file.name) if not is_scoring: detail_names = SubmissionDetails.DETAILED_OUTPUT_NAMES_PREDICTION From d9ae6e3549573b4d5161ca721c0367d412b6f8c5 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Wed, 1 Apr 2026 14:15:43 +0500 Subject: [PATCH 19/32] comments added for clarity --- compute_worker/compute_worker.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compute_worker/compute_worker.py b/compute_worker/compute_worker.py index d918fcd48..d5f58283a 100644 --- a/compute_worker/compute_worker.py +++ b/compute_worker/compute_worker.py @@ -520,6 +520,8 @@ async def watch_detailed_results(self): start = time.time() expiration_seconds = 60 + # When running scoring program, we have at least one program to run i.e. scoring_program + # Sometimes when ingestion_only_during_scoring is True, we have two programs to run expected_completed_program_counters = 1 + int(bool(self.ingestion_only_during_scoring)) while self.watch and self.completed_program_counter < expected_completed_program_counters: if file_path: From 479a787718847c52d5c5d7a7d3e2456cc28bff29 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Wed, 1 Apr 2026 21:37:15 +0500 Subject: [PATCH 20/32] spacing fixed --- compute_worker/compute_worker.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/compute_worker/compute_worker.py b/compute_worker/compute_worker.py index d5f58283a..2091d2829 100644 --- a/compute_worker/compute_worker.py +++ b/compute_worker/compute_worker.py @@ -262,6 +262,7 @@ def setup_celery_logging(**kwargs): ), ] + # ----------------------------------------------- # Exceptions # ----------------------------------------------- @@ -1233,8 +1234,6 @@ def prepare(self): # Only during prediction step do we want to announce "preparing" self._update_status(SubmissionStatus.PREPARING, extra_information=f"ingestion_hostname-{hostname}") - - # Setup cache and prune if it's out of control self._prep_cache_dir() From 9bbc49c602b623e9e59db06da1605dd8d85ce031 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Wed, 1 Apr 2026 21:41:56 +0500 Subject: [PATCH 21/32] comments added for clarity, empty line removed --- compute_worker/compute_worker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compute_worker/compute_worker.py b/compute_worker/compute_worker.py index 2091d2829..28a0f4fc2 100644 --- a/compute_worker/compute_worker.py +++ b/compute_worker/compute_worker.py @@ -289,7 +289,6 @@ def rewrite_bundle_url_if_needed(url): Example: http://localhost:9000|http://minio:9000 """ - rule = Settings.WORKER_BUNDLE_URL_REWRITE if not rule or "|" not in rule: return url @@ -1425,6 +1424,8 @@ def start(self): if self.is_scoring: # Check if scoring program failed + # We have try except here because when running scoring program we can have 2 or 3 gathered tasks + # 3 gathered tasks in case when `ingestion_only_during_scoring` is True try: program_results, _, _ = task_results except Exception: From 5b81089f6cfef6d34d83c90d822dcb986e854e77 Mon Sep 17 00:00:00 2001 From: didayolo Date: Fri, 17 Apr 2026 14:02:17 +0200 Subject: [PATCH 22/32] Correct error message --- compute_worker/compute_worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compute_worker/compute_worker.py b/compute_worker/compute_worker.py index 28a0f4fc2..429b9d45a 100644 --- a/compute_worker/compute_worker.py +++ b/compute_worker/compute_worker.py @@ -1510,7 +1510,7 @@ def push_output(self): f.write(yaml.dump(prog_status, default_flow_style=False)) except Exception as e: logger.error(e) - raise SubmissionException("Metadata file not found") + raise SubmissionException("Failed to write metadata file.") if not self.is_scoring: self._put_dir(self.prediction_result, self.output_dir) From f4b6cd7919da44b3398b7e62a0e4afbc8c666311 Mon Sep 17 00:00:00 2001 From: didayolo Date: Fri, 17 Apr 2026 15:32:27 +0200 Subject: [PATCH 23/32] Update prepare --- compute_worker/compute_worker.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/compute_worker/compute_worker.py b/compute_worker/compute_worker.py index 429b9d45a..3e7a2f92b 100644 --- a/compute_worker/compute_worker.py +++ b/compute_worker/compute_worker.py @@ -1233,6 +1233,9 @@ def prepare(self): # Only during prediction step do we want to announce "preparing" self._update_status(SubmissionStatus.PREPARING, extra_information=f"ingestion_hostname-{hostname}") + # Ensure output_dir exists on the host + self._get_host_path(self.output_dir) + # Setup cache and prune if it's out of control self._prep_cache_dir() From 0aefea1ef3a1016ca7c9fdc84322931e0a5be559 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Sat, 2 May 2026 22:27:49 +0500 Subject: [PATCH 24/32] create output dir if does not exist and some minor fixes to container_variable names --- compute_worker/compute_worker.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/compute_worker/compute_worker.py b/compute_worker/compute_worker.py index 3e7a2f92b..8129256f3 100644 --- a/compute_worker/compute_worker.py +++ b/compute_worker/compute_worker.py @@ -1340,8 +1340,8 @@ def start(self): # Cleanup containers containers_to_kill = [ - self.ingestion_container_name, - self.program_container_name + self.ingestion_program_container_name, + self.scoring_program_container_name ] logger.debug("Trying to kill and remove container " + str(containers_to_kill)) @@ -1501,6 +1501,8 @@ def push_output(self): logger.info(f"Metadata output: {prog_status}") + # Create output_dir if does not exist + os.makedirs(self.output_dir, exist_ok=True) metadata_path = os.path.join(self.output_dir, "metadata") if os.path.exists(metadata_path): From b7ec7d13e3bc36039ea3e8250f72551762dc042c Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Tue, 19 May 2026 17:41:42 +0500 Subject: [PATCH 25/32] try/except converted to if else for unpacking task_results --- compute_worker/compute_worker.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/compute_worker/compute_worker.py b/compute_worker/compute_worker.py index 8129256f3..c02abcb0f 100644 --- a/compute_worker/compute_worker.py +++ b/compute_worker/compute_worker.py @@ -1427,11 +1427,10 @@ def start(self): if self.is_scoring: # Check if scoring program failed - # We have try except here because when running scoring program we can have 2 or 3 gathered tasks - # 3 gathered tasks in case when `ingestion_only_during_scoring` is True - try: + # We have can have 2 or 3 gathered tasks: 3 gathered tasks in case when `ingestion_only_during_scoring` is True, 2 otherwise + if self.ingestion_only_during_scoring: program_results, _, _ = task_results - except Exception: + else: program_results, _ = task_results # Gather returns either normal values or exception instances when return_exceptions=True had_async_exc = isinstance( From 752809955d87b7598a05d1b10005509311452e11 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 20:50:41 +0000 Subject: [PATCH 26/32] Bump pymdown-extensions from 10.21.2 to 10.21.3 in /documentation Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 10.21.2 to 10.21.3. - [Release notes](https://github.com/facelessuser/pymdown-extensions/releases) - [Commits](https://github.com/facelessuser/pymdown-extensions/compare/10.21.2...10.21.3) --- updated-dependencies: - dependency-name: pymdown-extensions dependency-version: 10.21.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- documentation/uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/documentation/uv.lock b/documentation/uv.lock index aec2a9fde..5b7d224d3 100644 --- a/documentation/uv.lock +++ b/documentation/uv.lock @@ -120,15 +120,15 @@ wheels = [ [[package]] name = "pymdown-extensions" -version = "10.21.2" +version = "10.21.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/08/f1c908c581fd11913da4711ea7ba32c0eee40b0190000996bb863b0c9349/pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc", size = 853922, upload-time = "2026-03-29T15:01:55.233Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/26/d1015444da4d952a1ca487a236b522eb979766f0295a0bd0c5fc089989a9/pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354", size = 854140, upload-time = "2026-05-13T12:57:32.267Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" }, + { url = "https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6", size = 269002, upload-time = "2026-05-13T12:57:30.296Z" }, ] [[package]] From d01488a9d5d115dc302e972aa77ee3179138114a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 21:06:12 +0000 Subject: [PATCH 27/32] Bump idna from 3.13 to 3.15 Bumps [idna](https://github.com/kjd/idna) from 3.13 to 3.15. - [Release notes](https://github.com/kjd/idna/releases) - [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md) - [Commits](https://github.com/kjd/idna/compare/v3.13...v3.15) --- updated-dependencies: - dependency-name: idna dependency-version: '3.15' dependency-type: indirect ... Signed-off-by: dependabot[bot] --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 41ca175d6..e27c5616b 100644 --- a/uv.lock +++ b/uv.lock @@ -981,11 +981,11 @@ wheels = [ [[package]] name = "idna" -version = "3.13" +version = "3.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, ] [[package]] From f431f06bcdbfaf04784192130d5b6d717f991acb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 18:50:24 +0000 Subject: [PATCH 28/32] Bump urllib3 from 2.6.3 to 2.7.0 Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.6.3 to 2.7.0. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/2.6.3...2.7.0) --- updated-dependencies: - dependency-name: urllib3 dependency-version: 2.7.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8d308a843..cd63d838c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ dependencies = [ "pillow==12.2.0", "celery==5.6.2", "gunicorn==23.0", - "urllib3==2.6.3", + "urllib3==2.7.0", "uvicorn==0.38", "pyyaml==6.0.3", "watchdog==6.0.0", diff --git a/uv.lock b/uv.lock index 41ca175d6..c14e0ecd7 100644 --- a/uv.lock +++ b/uv.lock @@ -489,7 +489,7 @@ requires-dist = [ { name = "social-auth-core", specifier = "==4.8.5" }, { name = "twisted", specifier = "==25.5.0" }, { name = "tzdata", specifier = ">=2025.3" }, - { name = "urllib3", specifier = "==2.6.3" }, + { name = "urllib3", specifier = "==2.7.0" }, { name = "uvicorn", specifier = "==0.38" }, { name = "watchdog", specifier = "==6.0.0" }, { name = "websockets", specifier = "==16.0.0" }, @@ -1841,11 +1841,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] From 4a0b43f8f00521e2e4504a71fe82fef2c8b9f27c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 18:18:28 +0000 Subject: [PATCH 29/32] Bump urllib3 from 2.6.3 to 2.7.0 in /compute_worker Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.6.3 to 2.7.0. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/2.6.3...2.7.0) --- updated-dependencies: - dependency-name: urllib3 dependency-version: 2.7.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- compute_worker/uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compute_worker/uv.lock b/compute_worker/uv.lock index 9f2498d73..c48968b27 100644 --- a/compute_worker/uv.lock +++ b/compute_worker/uv.lock @@ -397,11 +397,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] From 0bdd8718f6094cd275c03d1625a0838fc2fe09e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 23:15:11 +0000 Subject: [PATCH 30/32] Bump django from 5.2.13 to 5.2.14 Bumps [django](https://github.com/django/django) from 5.2.13 to 5.2.14. - [Commits](https://github.com/django/django/compare/5.2.13...5.2.14) --- updated-dependencies: - dependency-name: django dependency-version: 5.2.14 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cd63d838c..698462a4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", ] dependencies = [ - "django==5.2.13", + "django==5.2.14", "django-oauth-toolkit==1.6.3", "social-auth-core==4.8.5", "social-auth-app-django==5.6.0", diff --git a/uv.lock b/uv.lock index c14e0ecd7..32e0e0159 100644 --- a/uv.lock +++ b/uv.lock @@ -450,7 +450,7 @@ requires-dist = [ { name = "channels-redis", specifier = "==4.0.0" }, { name = "configobj", specifier = "==5.0.9" }, { name = "dj-database-url", specifier = "==0.4.2" }, - { name = "django", specifier = "==5.2.13" }, + { name = "django", specifier = "==5.2.14" }, { name = "django-ajax-selects", specifier = "==3.0.3" }, { name = "django-cors-headers", specifier = "==4.9.0" }, { name = "django-enforce-host", specifier = "==1.1.0" }, @@ -635,16 +635,16 @@ wheels = [ [[package]] name = "django" -version = "5.2.13" +version = "5.2.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, { name = "sqlparse" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/c5/c69e338eb2959f641045802e5ea87ca4bf5ac90c5fd08953ca10742fad51/django-5.2.13.tar.gz", hash = "sha256:a31589db5188d074c63f0945c3888fad104627dfcc236fb2b97f71f89da33bc4", size = 10890368, upload-time = "2026-04-07T14:02:15.072Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/95/95f7faa0950867afaa0bef2460c6263afd6a2c78cc9434046ed28160b015/django-5.2.14.tar.gz", hash = "sha256:58a63ba841662e5c686b57ba1fec52ddd68c0b93bd96ac3029d55728f00bf8a2", size = 10895118, upload-time = "2026-05-05T13:57:31.104Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/b1/51ab36b2eefcf8cdb9338c7188668a157e29e30306bfc98a379704c9e10d/django-5.2.13-py3-none-any.whl", hash = "sha256:5788fce61da23788a8ce6f02583765ab060d396720924789f97fa42119d37f7a", size = 8310982, upload-time = "2026-04-07T14:02:08.883Z" }, + { url = "https://files.pythonhosted.org/packages/14/44/f172870cf87aa25afef48fb72adba89ee8b77fcab6f3b23d240b923f1528/django-5.2.14-py3-none-any.whl", hash = "sha256:6f712143bd3064310d1f50fac859c3e9a274bdcfc9595339853be7779297fc76", size = 8311320, upload-time = "2026-05-05T13:57:25.795Z" }, ] [[package]] From dd91cd845b38ed2a392175b3e126797d740698c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 21:19:10 +0000 Subject: [PATCH 31/32] Bump idna from 3.13 to 3.15 in /compute_worker Bumps [idna](https://github.com/kjd/idna) from 3.13 to 3.15. - [Release notes](https://github.com/kjd/idna/releases) - [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md) - [Commits](https://github.com/kjd/idna/compare/v3.13...v3.15) --- updated-dependencies: - dependency-name: idna dependency-version: '3.15' dependency-type: indirect ... Signed-off-by: dependabot[bot] --- compute_worker/uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compute_worker/uv.lock b/compute_worker/uv.lock index c48968b27..6715c4671 100644 --- a/compute_worker/uv.lock +++ b/compute_worker/uv.lock @@ -202,11 +202,11 @@ wheels = [ [[package]] name = "idna" -version = "3.13" +version = "3.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, ] [[package]] From d4f7d17954c6ed1f16f6b0860a052be5003d47bb Mon Sep 17 00:00:00 2001 From: Idir Chikhoune Date: Tue, 26 May 2026 11:48:13 +0200 Subject: [PATCH 32/32] Compute worker monitoring for organizers and collaborators. (#2316) * feature ok / needs to be tested * feature deactivated by default + hiden behind a button by default + visibile only by admin or comp organizer or collaborators * worker monitoring button behind user menu * files blacked for fixing the formatting issues * fixing synthax and format * fixing synthax and format * fixing synthax and format * feature in progress * new file for monitoring implentation imported into frontend * test monitoring private queues * compute worker monitoring on private queues (amazing stuff) * test number 234 * test number 238 * test number 245 * test number 246 * test number 249 * test number 435 * add d'un fichier manquant pour test * model queue = a problem * clean feature/ needs to be tested * clean feature code and imports * feature production ready * fix comment // feature production ready * debug site worker * add collapse button on default worker panel * feature production ready --- pyproject.toml | 2 + src/apps/competitions/tasks.py | 118 +++- src/celery_config.py | 18 +- src/routing.py | 3 + src/settings/base.py | 4 + .../riot/competitions/detail/_header.tag | 7 + .../riot/competitions/detail/detail.tag | 2 +- .../detail/worker-monitor-toggle.tag | 650 ++++++++++++++++++ src/templates/competitions/detail.html | 8 +- src/urls.py | 1 - src/utils/consumers.py | 142 ++++ src/utils/worker_utils.py | 28 + uv.lock | 79 +++ 13 files changed, 1051 insertions(+), 11 deletions(-) create mode 100644 src/static/riot/competitions/detail/worker-monitor-toggle.tag create mode 100644 src/utils/consumers.py create mode 100644 src/utils/worker_utils.py diff --git a/pyproject.toml b/pyproject.toml index 698462a4e..8ae6e2086 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,8 @@ dependencies = [ "django-cors-headers==4.9.0", "nh3==0.3.3", "configobj==5.0.9", + "black>=26.3.1", + "redis-cli>=1.0.1", ] [tool.uv] diff --git a/src/apps/competitions/tasks.py b/src/apps/competitions/tasks.py index ee35eeeb9..9a51e6332 100644 --- a/src/apps/competitions/tasks.py +++ b/src/apps/competitions/tasks.py @@ -1,4 +1,5 @@ import asyncio +import json import os import re import traceback @@ -8,10 +9,13 @@ from io import BytesIO from tempfile import TemporaryDirectory, NamedTemporaryFile +import urllib + import oyaml as yaml import requests from celery._state import app_or_default from django.conf import settings +from django_redis import get_redis_connection from django.core.exceptions import ObjectDoesNotExist from django.core.files.base import ContentFile from django.db.models import Subquery, OuterRef, Count, Case, When, Value, F @@ -20,9 +24,10 @@ from django.utils.timezone import now from rest_framework.exceptions import ValidationError -from celery_config import app +from celery_config import app, app_for_vhost from competitions.models import Submission, CompetitionCreationTaskStatus, SubmissionDetails, Competition, \ CompetitionDump, Phase +from queues.models import Queue from competitions.unpackers.utils import CompetitionUnpackingException from competitions.unpackers.v1 import V15Unpacker from competitions.unpackers.v2 import V2Unpacker @@ -31,8 +36,12 @@ from datasets.models import Data from utils.data import make_url_sassy from utils.email import codalab_send_markdown_email +from channels.layers import get_channel_layer +from asgiref.sync import async_to_sync import logging + +from utils.worker_utils import WORKER_HEARTBEAT_TTL, WORKERS_REGISTRY_KEY, extract_queue_names, is_compute_worker, known_compute_queue_names logger = logging.getLogger(__name__) COMPETITION_FIELDS = [ @@ -791,3 +800,110 @@ def submission_status_cleanup(): sub.parent.cancel(status=Submission.FAILED) else: sub.cancel(status=Submission.FAILED) + + +# ------------------------------------------------- +def _broadcast_worker_state(payload): + channel_layer = get_channel_layer() + if not channel_layer: + return + + async_to_sync(channel_layer.group_send)( + "compute_workers", + { + "type": "worker.health", + "worker": payload, + }, + ) + + +@app.task(queue="site-worker", soft_time_limit=120) +def refresh_compute_worker_health(): + celery_app = app + r = get_redis_connection("default") + known_queue_names = known_compute_queue_names() + broker_sources = [] + broker_sources.append(("default", celery_app.conf.broker_url, celery_app)) + + private_queues = ( + Queue.objects.filter(competitions__isnull=False) + .exclude(name__isnull=True) + .exclude(name="") + .distinct() + ) + for queue in private_queues: + if not queue.broker_url: + continue + parsed = urllib.parse.urlparse(queue.broker_url) + vhost = parsed.path + broker_url = urllib.parse.urljoin(celery_app.conf.broker_url, vhost) + broker_sources.append((queue.name, broker_url, app_for_vhost(vhost))) + + inspected_brokers = set() + for source_name, broker_url, broker_app in broker_sources: + if broker_url in inspected_brokers: + continue + inspected_brokers.add(broker_url) + + try: + # timeout=5 : 4 appels × 5s × N brokers + inspector = broker_app.control.inspect(timeout=5) + if inspector is None: + logger.warning( + "Celery inspect returned None for broker=%s", source_name + ) + continue + stats = inspector.stats() or {} + active = inspector.active() or {} + reserved = inspector.reserved() or {} + active_queues = inspector.active_queues() or {} + except Exception: + logger.exception( + "Unable to inspect Celery workers for broker %s", source_name + ) + continue + + for worker_name in stats.keys(): + queues = active_queues.get(worker_name, []) or [] + queue_names = extract_queue_names(queues) + if not is_compute_worker(worker_name, queue_names, known_queue_names): + continue + + running_jobs = len(active.get(worker_name, [])) + len( + reserved.get(worker_name, []) + ) + status = "busy" if running_jobs > 0 else "available" + payload = { + "hostname": worker_name, + "status": status, + "running_jobs": running_jobs, + "timestamp": now().timestamp(), + "queue_source": source_name, + "queue_names": sorted(queue_names), + } + heartbeat_key = f"worker:{source_name}:{worker_name}:heartbeat" + r.set(heartbeat_key, json.dumps(payload), ex=WORKER_HEARTBEAT_TTL) + r.hset( + WORKERS_REGISTRY_KEY, + f"{source_name}:{worker_name}", + json.dumps( + { + "hostname": worker_name, + "status": status, + "running_jobs": running_jobs, + "last_seen": payload["timestamp"], + "queue_source": source_name, + "queue_names": sorted(queue_names), + } + ), + ) + _broadcast_worker_state(payload) + # Logs about CW health HERE + # logger.info( + # "[WORKER-HEALTH] source=%s worker=%s status=%s jobs=%d queues=%s", + # source_name, + # worker_name, + # status, + # running_jobs, + # sorted(queue_names), + # ) diff --git a/src/celery_config.py b/src/celery_config.py index 760614783..e8ba5961e 100644 --- a/src/celery_config.py +++ b/src/celery_config.py @@ -1,18 +1,24 @@ +import copy +import urllib.parse + from celery import Celery -from kombu import Queue, Exchange from django.conf import settings -import urllib.parse -import copy +from kombu import Exchange, Queue app = Celery() from django.conf import settings # noqa -app.config_from_object('django.conf:settings', namespace='CELERY') +app.config_from_object("django.conf:settings", namespace="CELERY") app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) app.conf.task_queues = [ # Mostly defining queue here so we can set x-max-priority - Queue('compute-worker', Exchange('compute-worker'), routing_key='compute-worker', queue_arguments={'x-max-priority': 10}), + Queue( + "compute-worker", + Exchange("compute-worker"), + routing_key="compute-worker", + queue_arguments={"x-max-priority": 10}, + ), ] _vhost_apps = {} @@ -32,7 +38,7 @@ def app_for_vhost(vhost): # Copy the settings so we can modify the broker url to include the vhost django_settings = copy.copy(settings) django_settings.CELERY_BROKER_URL = broker_url - vhost_app.config_from_object(django_settings, namespace='CELERY') + vhost_app.config_from_object(django_settings, namespace="CELERY") vhost_app.conf.task_queues = app.conf.task_queues _vhost_apps[vhost] = vhost_app return _vhost_apps[vhost] diff --git a/src/routing.py b/src/routing.py index 2ef280e73..adf3f44cc 100644 --- a/src/routing.py +++ b/src/routing.py @@ -1,7 +1,10 @@ from django.urls import re_path from apps.competitions.consumers import SubmissionIOConsumer, SubmissionOutputConsumer +from utils.consumers import ComputeWorkersConsumer + websocket_urlpatterns = [ re_path(r'submission_input/(?P\d+)/(?P\d+)/(?P[^/]+)/$', SubmissionIOConsumer.as_asgi()), re_path(r'submission_output/$', SubmissionOutputConsumer.as_asgi()), + re_path(r"ws/workers/$", ComputeWorkersConsumer.as_asgi()), ] diff --git a/src/settings/base.py b/src/settings/base.py index d2698bb2a..47c76ad6b 100644 --- a/src/settings/base.py +++ b/src/settings/base.py @@ -276,6 +276,10 @@ 'task': 'profiles.tasks.clean_non_activated_users', 'schedule': timedelta(days=1), # Run every 24 hours }, + "refresh_compute_worker_health": { + "task": "competitions.tasks.refresh_compute_worker_health", + "schedule": 60, + }, } CELERY_TIMEZONE = 'UTC' CELERY_WORKER_PREFETCH_MULTIPLIER = 1 diff --git a/src/static/riot/competitions/detail/_header.tag b/src/static/riot/competitions/detail/_header.tag index f8b020377..b8234bb1d 100644 --- a/src/static/riot/competitions/detail/_header.tag +++ b/src/static/riot/competitions/detail/_header.tag @@ -35,6 +35,13 @@ + + + +
diff --git a/src/static/riot/competitions/detail/detail.tag b/src/static/riot/competitions/detail/detail.tag index f971675b3..7bed78b29 100644 --- a/src/static/riot/competitions/detail/detail.tag +++ b/src/static/riot/competitions/detail/detail.tag @@ -645,4 +645,4 @@ } } - + \ No newline at end of file diff --git a/src/static/riot/competitions/detail/worker-monitor-toggle.tag b/src/static/riot/competitions/detail/worker-monitor-toggle.tag new file mode 100644 index 000000000..d31b64e2e --- /dev/null +++ b/src/static/riot/competitions/detail/worker-monitor-toggle.tag @@ -0,0 +1,650 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/templates/competitions/detail.html b/src/templates/competitions/detail.html index 43a2228db..8e011f45f 100644 --- a/src/templates/competitions/detail.html +++ b/src/templates/competitions/detail.html @@ -1,7 +1,11 @@ {% extends "base.html" %} -{% block title %}{{ competition.title }} - Codabench{% endblock %} +{% block title %}{{ competition.title }} – Codabench{% endblock %} {% block content %} - + + {% endblock %} \ No newline at end of file diff --git a/src/urls.py b/src/urls.py index 2634d19d6..88013d5a7 100644 --- a/src/urls.py +++ b/src/urls.py @@ -33,7 +33,6 @@ ] - if settings.DEBUG: # Static files for local dev, so we don't have to collectstatic and such urlpatterns += staticfiles_urlpatterns() diff --git a/src/utils/consumers.py b/src/utils/consumers.py new file mode 100644 index 000000000..6d076bac9 --- /dev/null +++ b/src/utils/consumers.py @@ -0,0 +1,142 @@ +import asyncio +import json +import logging +import time + +from competitions.models import Competition + +from asgiref.sync import sync_to_async +from channels.generic.websocket import AsyncJsonWebsocketConsumer +from django_redis import get_redis_connection + +from utils.worker_utils import WORKER_HEARTBEAT_TTL, WORKERS_REGISTRY_KEY + +logger = logging.getLogger(__name__) + +r = get_redis_connection("default") + + +def _load_snapshot(competition_queue_name=None): + """ + Charge les workers depuis Redis. + - workers par défaut : toujours inclus (queue_source == 'default') + - workers privés : inclus uniquement si leur queue_source correspond + à la queue de la compétition courante + """ + raw = r.hgetall(WORKERS_REGISTRY_KEY) + workers = [] + private_workers = [] + now = time.time() + + for _, value in raw.items(): + try: + worker = json.loads(value) + except Exception: + continue + + if now - worker.get("last_seen", 0) > WORKER_HEARTBEAT_TTL: + continue + + if worker.get("queue_source") == "default": + workers.append(worker) + else: + # Worker privé : n'afficher que si la queue correspond à la compétition + if competition_queue_name and worker.get("queue_source") == competition_queue_name: + private_workers.append(worker) + + workers.sort(key=lambda x: x.get("hostname", "")) + private_workers.sort(key=lambda x: (x.get("queue_source", ""), x.get("hostname", ""))) + return workers, private_workers + + +def _get_competition_queue_name(competition_id): + """Retourne le nom de la queue de la compétition, ou None.""" + if not competition_id: + return None + try: + competition = Competition.objects.select_related("queue").get(pk=competition_id) + if competition.queue and competition.queue.name: + return competition.queue.name + except Exception: + logger.warning("Competition %s not found or has no queue", competition_id) + return None + + +class ComputeWorkersConsumer(AsyncJsonWebsocketConsumer): + + async def connect(self): + user = self.scope.get("user") + if user is None or user.is_anonymous: + await self.close() + return + await self.accept() + await self.channel_layer.group_add("compute_workers", self.channel_name) + self._competition_queue_name = None + self._running = True + self._subscribed = asyncio.Event() + self._task = asyncio.create_task(self._push_workers_loop()) + + async def disconnect(self, close_code): + self._running = False + await self.channel_layer.group_discard("compute_workers", self.channel_name) + task = getattr(self, "_task", None) + if task: + task.cancel() + try: + await task + except (asyncio.CancelledError, RuntimeError): + pass + + async def receive_json(self, content): + logger.debug("WebSocket received: %s", content) + if content.get("type") == "subscribe": + competition_id = content.get("competition_id") + self._competition_queue_name = await sync_to_async(_get_competition_queue_name)( + competition_id) + self._subscribed.set() + + async def _push_workers_loop(self): + try: + try: + await asyncio.wait_for(self._subscribed.wait(), timeout=5.0) + except asyncio.TimeoutError: + logger.warning("WebSocket subscribe timeout, proceeding without competition filter") + + while self._running: + workers, private_workers = await sync_to_async(_load_snapshot)( + self._competition_queue_name + ) + if not self._running: + break + try: + await self.send_json({ + "type": "workers.snapshot", + "workers": workers, + "private_workers": private_workers, + }) + except RuntimeError: + break + await asyncio.sleep(3) + except asyncio.CancelledError: + pass + + async def worker_health(self, event): + worker = event["worker"] + is_default = worker.get("queue_source") == "default" + is_mine = ( + self._competition_queue_name is not None + and worker.get("queue_source") == self._competition_queue_name + ) + if not is_default and not is_mine: + return + try: + workers, private_workers = await sync_to_async(_load_snapshot)( + self._competition_queue_name + ) + await self.send_json({ + "type": "workers.snapshot", + "workers": workers, + "private_workers": private_workers, + }) + except RuntimeError: + pass diff --git a/src/utils/worker_utils.py b/src/utils/worker_utils.py new file mode 100644 index 000000000..f1b9a474b --- /dev/null +++ b/src/utils/worker_utils.py @@ -0,0 +1,28 @@ +from queues.models import Queue + +WORKERS_REGISTRY_KEY = "workers:registry" +WORKER_HEARTBEAT_TTL = 180 + + +def extract_queue_names(active_queues): + names = set() + for q in active_queues or []: + if isinstance(q, dict) and q.get("name"): + names.add(q["name"]) + return names + + +def known_compute_queue_names(): + return set( + Queue.objects.exclude(name__isnull=True) + .exclude(name="") + .values_list("name", flat=True) + ) + + +def is_compute_worker(worker_name, queue_names, known_queue_names): + return ( + bool(queue_names & known_queue_names) + or "compute-worker" in queue_names + or worker_name.startswith("compute-worker") + ) diff --git a/uv.lock b/uv.lock index 21bb58ba5..486257380 100644 --- a/uv.lock +++ b/uv.lock @@ -138,6 +138,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/87/8bab77b323f16d67be364031220069f79159117dd5e43eeb4be2fef1ac9b/billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", size = 87070, upload-time = "2025-11-30T13:28:47.016Z" }, ] +[[package]] +name = "black" +version = "26.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, + { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, + { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, +] + [[package]] name = "blessed" version = "1.38.0" @@ -371,6 +393,7 @@ dependencies = [ { name = "argh" }, { name = "azure-storage-blob" }, { name = "azure-storage-common" }, + { name = "black" }, { name = "blessings" }, { name = "boto3" }, { name = "botocore" }, @@ -412,6 +435,7 @@ dependencies = [ { name = "python-dateutil" }, { name = "pytz" }, { name = "pyyaml" }, + { name = "redis-cli" }, { name = "requests" }, { name = "s3transfer" }, { name = "setuptools" }, @@ -441,6 +465,7 @@ requires-dist = [ { name = "argh", specifier = "==0.31.3" }, { name = "azure-storage-blob", specifier = ">=12,<13" }, { name = "azure-storage-common", specifier = "==2.1.0" }, + { name = "black", specifier = ">=26.3.1" }, { name = "blessings", specifier = "==1.7" }, { name = "boto3", specifier = "==1.42.50" }, { name = "botocore", specifier = "==1.42.50" }, @@ -482,6 +507,7 @@ requires-dist = [ { name = "python-dateutil", specifier = "==2.9.0" }, { name = "pytz", specifier = ">=2025.2" }, { name = "pyyaml", specifier = "==6.0.3" }, + { name = "redis-cli", specifier = ">=1.0.1" }, { name = "requests", specifier = "==2.33.1" }, { name = "s3transfer", specifier = "==0.16.0" }, { name = "setuptools", specifier = "==82.0.0" }, @@ -1272,6 +1298,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" }, ] +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + [[package]] name = "nh3" version = "0.3.3" @@ -1335,6 +1370,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, ] +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + [[package]] name = "pexpect" version = "4.9.0" @@ -1380,6 +1424,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, ] +[[package]] +name = "platformdirs" +version = "4.9.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -1552,6 +1605,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/a5/c6ba13860bdf5525f1ab01e01cc667578d6f1efc8a1dba355700fb04c29b/python3_openid-3.2.0-py3-none-any.whl", hash = "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b", size = 133681, upload-time = "2020-06-29T12:15:47.502Z" }, ] +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + [[package]] name = "pytz" version = "2026.1.post1" @@ -1597,6 +1664,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/74/3a/95deec7db1eb53979973ebd156f3369a72732208d1391cd2e5d127062a32/redis-7.4.0-py3-none-any.whl", hash = "sha256:a9c74a5c893a5ef8455a5adb793a31bb70feb821c86eccb62eebef5a19c429ec", size = 409772, upload-time = "2026-03-24T09:14:35.968Z" }, ] +[[package]] +name = "redis-cli" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "redis" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/7a/464077bce2a14cd8399776ae0c694e7c240b58ffe4b231413013f2fb48fe/redis-cli-1.0.1.tar.gz", hash = "sha256:6be46d8e6ae638fec27cb425d499b7f25b1592402c74d5d17f5b85b5e7f988a0", size = 5958, upload-time = "2023-10-02T05:58:36.407Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/29/33cbdafefd22613cc003f837b6f0295462b2e91696180f06c5210f576a1b/redis_cli-1.0.1-py3-none-any.whl", hash = "sha256:e9f6af4a8b7591d8bfd1e23be89bdd9666ce824df881748fb02b88dd30575dc8", size = 5851, upload-time = "2023-10-02T05:58:32.924Z" }, +] + [[package]] name = "referencing" version = "0.37.0"