mirror of
https://github.com/harivansh-afk/cp.nvim.git
synced 2026-04-15 14:03:49 +00:00
Compare commits
746 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff5ba39a59 | ||
|
|
760e7d7731 | ||
|
|
49e4233b3f | ||
|
|
622620f6d0 | ||
|
|
976838d981 | ||
|
|
06f72bbe2b | ||
|
|
6045042dfb | ||
|
|
c192afc5d7 | ||
|
|
b6f3398bbc | ||
|
|
e02a29bd40 | ||
|
|
0f9715298e | ||
|
|
2148d9bd07 | ||
|
|
1162e7046b | ||
|
|
b36ffba63a | ||
|
|
04d0c124cf | ||
|
|
da433068ef | ||
|
|
51504b0121 | ||
|
|
49df7e015d | ||
|
|
029ea125b9 | ||
|
|
43193c3762 | ||
|
|
de2bc07532 | ||
|
|
041e09ac04 | ||
|
|
d23b4e59d1 | ||
|
|
19e71ac7fa | ||
|
|
a54a06f939 | ||
|
|
b2c7f16890 | ||
|
|
276241447c | ||
|
|
dc635d5167 | ||
|
|
81ddd1ea87 | ||
|
|
7444a99b22 | ||
|
|
ec487aa489 | ||
|
|
c4af9bf604 | ||
|
|
a4437bc1c6 | ||
|
|
1a7e9517ba | ||
|
|
11b8365aac | ||
|
|
585ebf0daf | ||
|
|
08fb654d23 | ||
|
|
01efc7c344 | ||
|
|
f9f993db0c | ||
|
|
f184a7874a | ||
|
|
89e3c0e21d | ||
|
|
a9ce31a291 | ||
|
|
c8f735617a | ||
|
|
a14f543371 | ||
|
|
56ec178cdd | ||
|
|
5cd6f75419 | ||
|
|
99d907aa7a | ||
|
|
c06d819597 | ||
|
|
682b267019 | ||
|
|
8a2871ec1b | ||
|
|
de1295d361 | ||
|
|
32f449850b | ||
|
|
6966e8e101 | ||
|
|
a5e094d44a | ||
|
|
5de6fb2fee | ||
|
|
bd25f1db0b | ||
|
|
9daa4e4ec4 | ||
|
|
0b5c0f0c40 | ||
|
|
af559b0fa3 | ||
|
|
d496509fce | ||
|
|
383b327442 | ||
|
|
3f677137de | ||
|
|
0a1cea9b43 | ||
|
|
6ba51a92c2 | ||
|
|
86f2e41983 | ||
|
|
d89a40b21f | ||
|
|
3348ac3e51 | ||
|
|
ee38da5074 | ||
|
|
9af359eb01 | ||
|
|
0b21d02f24 | ||
|
|
282d701327 | ||
|
|
dcadf7447d | ||
|
|
89c1a3c683 | ||
|
|
83514c453e | ||
|
|
d5c6783124 | ||
|
|
5293515aca | ||
|
|
7dafb7ea43 | ||
|
|
0f82ae4fdb | ||
|
|
873ddee0d4 | ||
|
|
fb7888b83c | ||
|
|
ae7b571b68 | ||
|
|
4c5c44742e | ||
|
|
d4c5f08b5f | ||
|
|
0f513370ac | ||
|
|
8969dbccf8 | ||
|
|
ba26cee7f9 | ||
|
|
b88e2ce746 | ||
|
|
c8c0da6d61 | ||
|
|
d40d80c541 | ||
|
|
4369fe8b0c | ||
|
|
363a1e88e9 | ||
|
|
702cce959d | ||
|
|
ebeed1887d | ||
|
|
48bafffcde | ||
|
|
b85113b805 | ||
|
|
fa45d912b8 | ||
|
|
d613d3d24a | ||
|
|
445059a498 | ||
|
|
e0596aefff | ||
|
|
3a0c0de599 | ||
|
|
10b3dcd846 | ||
|
|
edb341ae51 | ||
|
|
dfd8275421 | ||
|
|
680a22f303 | ||
|
|
eb3f93587f | ||
|
|
9926965677 | ||
|
|
c7f573a93b | ||
|
|
ac51b2c799 | ||
|
|
ecd76795ce | ||
|
|
3c3e6172fc | ||
|
|
f805251762 | ||
|
|
6647e4120e | ||
|
|
06f8627331 | ||
|
|
5b43b64401 | ||
|
|
99109f5e91 | ||
|
|
944d37dc75 | ||
|
|
f91fbb2ca0 | ||
|
|
bbe04589b8 | ||
|
|
6aca33e371 | ||
|
|
675917796d | ||
|
|
e12b39bda1 | ||
|
|
c9769e04b8 | ||
|
|
864e6ceeae | ||
|
|
9cc2b52111 | ||
|
|
dcf8150cb2 | ||
|
|
71863fde7f | ||
|
|
5bcee87892 | ||
|
|
00987bb0ff | ||
|
|
d121784de5 | ||
|
|
07e4372a4a | ||
|
|
0e778a128e | ||
|
|
d0f1dbf132 | ||
|
|
5995ded7d5 | ||
|
|
e7ba6b4bb4 | ||
|
|
7d8d00c5ad | ||
|
|
13d931ed19 | ||
|
|
96c01bf796 | ||
|
|
127de3d6a5 | ||
|
|
6a1534124d | ||
|
|
8237dc4c16 | ||
|
|
cea90dbda5 | ||
|
|
1b0d5e4d77 | ||
|
|
bd557ab069 | ||
|
|
e1c8c4beaf | ||
|
|
71efb24cda | ||
|
|
aab211902e | ||
|
|
6477fdc20c | ||
|
|
9238118fbe | ||
|
|
6a61780928 | ||
|
|
fef73887e4 | ||
|
|
3654748632 | ||
|
|
73c91e2b28 | ||
|
|
91f85d066d | ||
|
|
71a6aac826 | ||
|
|
7bfa839c84 | ||
|
|
6a2f58430d | ||
|
|
161c4cc113 | ||
|
|
9b1f97dfec | ||
|
|
701d70a7ae | ||
|
|
8fd4ce9651 | ||
|
|
e89c2e1cf5 | ||
|
|
f78e43bdd4 | ||
|
|
2ab03e624c | ||
|
|
fa3de99222 | ||
|
|
4fe623c806 | ||
|
|
8ba2a598fe | ||
|
|
2fda5a74ca | ||
|
|
401494aab0 | ||
|
|
9b90e3a452 | ||
|
|
5de81e55a9 | ||
|
|
8345d147cf | ||
|
|
1d89fa0bdd | ||
|
|
b978c3ed13 | ||
|
|
4eb9c9a21f | ||
|
|
b736fd0131 | ||
|
|
181fff42de | ||
|
|
3fdb74a3d8 | ||
|
|
a45657c583 | ||
|
|
11b6056d8c | ||
|
|
de45fd3393 | ||
|
|
8ffa3cb0d2 | ||
|
|
a842886933 | ||
|
|
4b1b75fd6e | ||
|
|
c857b66998 | ||
|
|
3daf582b7a | ||
|
|
b3168ff3f0 | ||
|
|
6ff0320531 | ||
|
|
9bf3438466 | ||
|
|
0790fa7d6f | ||
|
|
3822348642 | ||
|
|
0418ef4613 | ||
|
|
9d848eba22 | ||
|
|
f52244c534 | ||
|
|
c48cf384a4 | ||
|
|
48d4c6f113 | ||
|
|
bd30fb626c | ||
|
|
36e75ad71b | ||
|
|
ce12ab0e1a | ||
|
|
f715075dbe | ||
|
|
249e84eb5a | ||
|
|
f9a1f79aef | ||
|
|
6f9452c7e1 | ||
|
|
7e2e712b56 | ||
|
|
64b8b03cca | ||
|
|
82021e3d97 | ||
|
|
9ffc285e16 | ||
|
|
6a6cf2c594 | ||
|
|
743c29e634 | ||
|
|
038fcd36f8 | ||
|
|
92ffa41ed0 | ||
|
|
1becd25cc0 | ||
|
|
b735af6a25 | ||
|
|
b77c81d63e | ||
|
|
a9ac06de83 | ||
|
|
ce975d0f1e | ||
|
|
57b4a3ff15 | ||
|
|
00fe1abcf1 | ||
|
|
d4b88be44b | ||
|
|
f45926c094 | ||
|
|
60e5aabd99 | ||
|
|
99544905df | ||
|
|
59f5066327 | ||
|
|
dc6f2fd5b6 | ||
|
|
c312ccbb4d | ||
|
|
9ac2d148d2 | ||
|
|
13933fc7fd | ||
|
|
114187164e | ||
|
|
c9d7d51732 | ||
|
|
92b6ce31f9 | ||
|
|
ad17855532 | ||
|
|
52a4286b70 | ||
|
|
347be72774 | ||
|
|
f0edb103ce | ||
|
|
018d801121 | ||
|
|
c29ec1c6b0 | ||
|
|
352f98f26f | ||
|
|
0c87ce2ac4 | ||
|
|
7f9f60af5b | ||
|
|
c9f6ef8278 | ||
|
|
14b8bded1d | ||
|
|
0a9e83d8f9 | ||
|
|
32a46b4e98 | ||
|
|
600a578a17 | ||
|
|
c0e175d84b | ||
|
|
2bc56195fd | ||
|
|
75f9f5ca4f | ||
|
|
e36a40a9ac | ||
|
|
6ae9488761 | ||
|
|
fa26344cd0 | ||
|
|
617c1741cc | ||
|
|
c9ebdcdda5 | ||
|
|
b30c036478 | ||
|
|
4fac6c8019 | ||
|
|
c143600c5b | ||
|
|
2426e1cbd4 | ||
|
|
c509102b37 | ||
|
|
a7eb731730 | ||
|
|
d4df57bd05 | ||
|
|
78c4cc779e | ||
|
|
2b8bd503ad | ||
|
|
a154d85a0d | ||
|
|
735bb04c95 | ||
|
|
6d4299ec68 | ||
|
|
5d479b26ce | ||
|
|
a0b5264761 | ||
|
|
9134a0742b | ||
|
|
edbf118c25 | ||
|
|
41a8d1a75b | ||
|
|
f00691ae40 | ||
|
|
cedcd82367 | ||
|
|
fd550bc654 | ||
|
|
25fde26943 | ||
|
|
ee88450b3b | ||
|
|
1945999099 | ||
|
|
5e9c00014d | ||
|
|
44bfc7317d | ||
|
|
91864b2992 | ||
|
|
fa2660455a | ||
|
|
f5a72a3a8f | ||
|
|
b68ecbbe96 | ||
|
|
45d21be879 | ||
|
|
794426402a | ||
|
|
d2bde9bad8 | ||
|
|
a76d228e3f | ||
|
|
2c0e808c8c | ||
|
|
c627a40c0a | ||
|
|
aae98a5796 | ||
|
|
0a320945a0 | ||
|
|
ef8ee26edf | ||
|
|
17b5e0a52b | ||
|
|
3fbbfa9423 | ||
|
|
91c37e35e5 | ||
|
|
2ac0a4996d | ||
|
|
018f61af92 | ||
|
|
b9a2c7a4ff | ||
|
|
18dbcd43d2 | ||
|
|
a725925434 | ||
|
|
8f466f135a | ||
|
|
bb0ee24476 | ||
|
|
88315ed6e6 | ||
|
|
f929c8e826 | ||
|
|
179b333505 | ||
|
|
b8c79401da | ||
|
|
f48acb4672 | ||
|
|
33cc2ca36b | ||
|
|
4498c4a7fa | ||
|
|
34ef7bafd6 | ||
|
|
1520939d4b | ||
|
|
fea9835436 | ||
|
|
7be37ad96e | ||
|
|
5991670ef2 | ||
|
|
d35ed0450b | ||
|
|
c9ba8281b0 | ||
|
|
312a74d785 | ||
|
|
3b2752685b | ||
|
|
b338ee6ca1 | ||
|
|
82444412aa | ||
|
|
6e01714fe1 | ||
|
|
91dbc4560c | ||
|
|
778ce7b8e2 | ||
|
|
357d1601b4 | ||
|
|
bfcf2242ee | ||
|
|
d480975652 | ||
|
|
cddd61f061 | ||
|
|
69ffc2d9dd | ||
|
|
0061161a90 | ||
|
|
5fdb522095 | ||
|
|
d9537e72ba | ||
|
|
1a4573a4e4 | ||
|
|
3c0f8d7deb | ||
|
|
de6969e982 | ||
|
|
4f10377255 | ||
|
|
db98153b11 | ||
|
|
1974addbd2 | ||
|
|
057b0890c2 | ||
|
|
35ccd6e217 | ||
|
|
2809689494 | ||
|
|
27c265141e | ||
|
|
00a1d57005 | ||
|
|
57be0c0044 | ||
|
|
91e6fbe455 | ||
|
|
6b8a1e2087 | ||
|
|
7eb314b02c | ||
|
|
e6c09a4897 | ||
|
|
a925686a17 | ||
|
|
62af1965f8 | ||
|
|
b406c0ce4e | ||
|
|
1b0b5e5039 | ||
|
|
91ce43e529 | ||
|
|
67c23c4d69 | ||
|
|
c1b15c2991 | ||
|
|
2bdb06ddef | ||
|
|
551da072e1 | ||
|
|
b52b679d39 | ||
|
|
5c04fd9270 | ||
|
|
79339ff945 | ||
|
|
e14bc99964 | ||
|
|
fe3a472428 | ||
|
|
7332d7e871 | ||
|
|
6cd3f9179f | ||
|
|
64d4d59d06 | ||
|
|
3427bf9bbb | ||
|
|
aa1dd43e70 | ||
|
|
7761c7c759 | ||
|
|
fe90c0b95d | ||
|
|
02fe97956f | ||
|
|
ea098e6c9c | ||
|
|
f94ae157c7 | ||
|
|
a54e6398cf | ||
|
|
46cd509747 | ||
|
|
b5b2c770fc | ||
|
|
99d6569809 | ||
|
|
9704b11e7c | ||
|
|
7a6690f360 | ||
|
|
ec9bc8cb64 | ||
|
|
5588eae526 | ||
|
|
a7cd41712d | ||
|
|
e3309e8f3c | ||
|
|
49ba922ff7 | ||
|
|
5d7719ec4a | ||
|
|
abe078b73d | ||
|
|
22d0f72878 | ||
|
|
dc4326524c | ||
|
|
02019dbdef | ||
|
|
df7896709f | ||
|
|
b7114042d7 | ||
|
|
2d5ff2bd93 | ||
|
|
f65f9baa73 | ||
|
|
42d2ae4aaa | ||
|
|
9d30e214e0 | ||
|
|
ae2f8b94cf | ||
|
|
e5aca06955 | ||
|
|
f0fbb15765 | ||
|
|
b41ed5be13 | ||
|
|
83645b48be | ||
|
|
bf191d7f67 | ||
|
|
0c4d09a0a9 | ||
|
|
433a468ee6 | ||
|
|
316b6628db | ||
|
|
2e478f2742 | ||
|
|
7efd6404b6 | ||
|
|
543a2a7c06 | ||
|
|
6b4dd32683 | ||
|
|
bcb555ec7e | ||
|
|
7711788d3d | ||
|
|
52c50cde79 | ||
|
|
092b4de05f | ||
|
|
383c59a2ea | ||
|
|
a48f4d049b | ||
|
|
170021af8e | ||
|
|
71b827fe95 | ||
|
|
bcbcc4365f | ||
|
|
81206aa050 | ||
|
|
7c337d6b33 | ||
|
|
a24ac2314c | ||
|
|
b70f38626e | ||
|
|
d862df9104 | ||
|
|
177c172205 | ||
|
|
646b0047dc | ||
|
|
62c4d1e89e | ||
|
|
975e829f78 | ||
|
|
9e84d57b8a | ||
|
|
cbd5569f95 | ||
|
|
a0171ee81e | ||
|
|
0e4c46c31a | ||
|
|
4429b5fe67 | ||
|
|
699207e713 | ||
|
|
7ac91a3c4d | ||
|
|
540364926d | ||
|
|
2d3432335c | ||
|
|
ca652c04ff | ||
|
|
a2b3de51d7 | ||
|
|
30c1c0f2cf | ||
|
|
a08ad8e2ee | ||
|
|
75994c07a5 | ||
|
|
1769ea079a | ||
|
|
4b9d63e4b8 | ||
|
|
2707df28ce | ||
|
|
79e1f1096b | ||
|
|
f3666a30be | ||
|
|
1f517309f2 | ||
|
|
8df8c16a72 | ||
|
|
62eab3df2d | ||
|
|
8a9bc7434f | ||
|
|
f9cf5b1614 | ||
|
|
545793df39 | ||
|
|
e171017ab0 | ||
|
|
5dd4d9109a | ||
|
|
5f555a0285 | ||
|
|
de14552a3e | ||
|
|
4b70a21210 | ||
|
|
a84b1697bf | ||
|
|
76cb1e456e | ||
|
|
7ad64677a5 | ||
|
|
1f384b0ba0 | ||
|
|
a32fd396d3 | ||
|
|
5707a28d58 | ||
|
|
53562eb6a8 | ||
|
|
0a8dc50c76 | ||
|
|
89440e5d14 | ||
|
|
358b22077f | ||
|
|
3b768cc6c4 | ||
|
|
db391da52c | ||
|
|
eb3f7762de | ||
|
|
87f9439607 | ||
|
|
101062cb48 | ||
|
|
80c7697340 | ||
|
|
23310eed53 | ||
|
|
847f04d1e8 | ||
|
|
1b5e713945 | ||
|
|
36806d6f5a | ||
|
|
3bf94cf979 | ||
|
|
9b443459e2 | ||
|
|
138f5bb2a2 | ||
|
|
7ec59109c3 | ||
|
|
ebf4856a3e | ||
|
|
a2a3c8f365 | ||
|
|
9c2be9c6b0 | ||
|
|
5a6902633f | ||
|
|
b1ba0007e0 | ||
|
|
b7ef866a14 | ||
|
|
a69d9f3756 | ||
|
|
ba81df2266 | ||
|
|
510393a788 | ||
|
|
beda8a3a03 | ||
|
|
d7f5112841 | ||
|
|
464ce8906c | ||
|
|
7352189339 | ||
|
|
039fad1614 | ||
|
|
5015a8636a | ||
|
|
f810958fdb | ||
|
|
73ee75642c | ||
|
|
355cb5df82 | ||
|
|
0851339e63 | ||
|
|
ff20efca71 | ||
|
|
7d51fc2931 | ||
|
|
dc2b96a3c0 | ||
|
|
f6b82b85f6 | ||
|
|
6fb27cf394 | ||
|
|
37ad916802 | ||
|
|
0e3ec89f17 | ||
|
|
34b252f892 | ||
|
|
c88d6a4a5b | ||
|
|
18a747dd8a | ||
|
|
05968657f5 | ||
|
|
cb4d39b4a7 | ||
|
|
0a39a2e6a2 | ||
|
|
16ddbb5b4e | ||
|
|
afb15150af | ||
|
|
d851dda461 | ||
|
|
8defe763ad | ||
|
|
78fb4f8f4b | ||
|
|
a40a53fafa | ||
|
|
e1b91ffffe | ||
|
|
102b69d4d7 | ||
|
|
965e47a1df | ||
|
|
9761cded88 | ||
|
|
e48e70a5f9 | ||
|
|
d4f1678b03 | ||
|
|
fe158aa65f | ||
|
|
45d439a7b2 | ||
|
|
d96d810328 | ||
|
|
36ef39479f | ||
|
|
3f713131eb | ||
|
|
07756d5da8 | ||
|
|
fdc1441fa3 | ||
|
|
1fd7fa2a81 | ||
|
|
4f31678a29 | ||
|
|
373e7f6e76 | ||
|
|
1822714a0c | ||
|
|
a827d4f67c | ||
|
|
0938b9bbd6 | ||
|
|
0dd145b71e | ||
|
|
3edc3db8aa | ||
|
|
9d92021fcf | ||
|
|
be143d408b | ||
|
|
c1529c5d91 | ||
|
|
2c994a8bdc | ||
|
|
1b8365265d | ||
|
|
c68e6fbc19 | ||
|
|
46c615416f | ||
|
|
58f9be5f9a | ||
|
|
a33e66680b | ||
|
|
ea9883895f | ||
|
|
d26fd29c52 | ||
|
|
3988d6febc | ||
|
|
c9ed129bd5 | ||
|
|
f758d54363 | ||
|
|
56c7cf00a5 | ||
|
|
1f38dba57f | ||
|
|
df1b4c2009 | ||
|
|
d827b6dd0b | ||
|
|
03bb0bda33 | ||
|
|
98aa3edd41 | ||
|
|
18939a9d5f | ||
|
|
3821174c6e | ||
|
|
7a027c7379 | ||
|
|
9deedec15a | ||
|
|
a8984d013a | ||
|
|
5bf9ae731f | ||
|
|
7b8aae7921 | ||
|
|
847307bd1f | ||
|
|
e6c54e01fd | ||
|
|
67fad79fb6 | ||
|
|
b3ccce1ee7 | ||
|
|
0c9ae37d74 | ||
|
|
f86eeb7876 | ||
|
|
ac21638550 | ||
|
|
26807d42ba | ||
|
|
27a44697ce | ||
|
|
3a66930732 | ||
|
|
d1994d07a3 | ||
|
|
d4adc9316e | ||
|
|
f3321f269d | ||
|
|
f60f6dd5bb | ||
|
|
3a65e5745e | ||
|
|
bd81743274 | ||
|
|
803c2dc76e | ||
|
|
b992e1e635 | ||
|
|
315e5a790c | ||
|
|
35545a1ad2 | ||
|
|
8e13b8c61d | ||
|
|
07be94d7aa | ||
|
|
8df38d0ca8 | ||
|
|
f487b5d006 | ||
|
|
069df71871 | ||
|
|
b2083bf649 | ||
|
|
5309cd0596 | ||
|
|
cae0ea1914 | ||
|
|
1d95192b7a | ||
|
|
eada64de41 | ||
|
|
56c31b22b9 | ||
|
|
b507dad4a7 | ||
|
|
f493b44ca3 | ||
|
|
0b35ff8f8e | ||
|
|
8a66b92684 | ||
|
|
9bfd495ef0 | ||
|
|
8e0b2bdb6c | ||
|
|
8db8c1bd9f | ||
|
|
f8de0207ee | ||
|
|
1093ff26f6 | ||
|
|
e780b8ad4e | ||
|
|
ffb5b2b209 | ||
|
|
2b081640df | ||
|
|
21b7765105 | ||
|
|
4e880a2d84 | ||
|
|
94e020b535 | ||
|
|
2846cf83f0 | ||
|
|
57160f4d50 | ||
|
|
26ed0e6d52 | ||
|
|
a8f16fb4f9 | ||
|
|
97873ffd37 | ||
|
|
cdcf11767e | ||
|
|
a00799abf4 | ||
|
|
a1aa4ccbf9 | ||
|
|
77aa5dd4c4 | ||
|
|
13005a3caa | ||
|
|
8e7f273f40 | ||
|
|
e5dcab36c3 | ||
|
|
3e2cff09e5 | ||
|
|
7f8e84437f | ||
|
|
de232ed96c | ||
|
|
ad3cd32bac | ||
|
|
b34ace85a5 | ||
|
|
9ea6f878de | ||
|
|
a3dd6f4e1e | ||
|
|
8cf32d5877 | ||
|
|
69fc2ecdbb | ||
|
|
db85bacd4c | ||
|
|
93be3b0dc9 | ||
|
|
99c7844aa8 | ||
|
|
9d98c3ed54 | ||
|
|
36adafd5bd | ||
|
|
ff9a3d1abb | ||
|
|
a7cd58ad90 | ||
|
|
ddff996ee2 | ||
|
|
f148f77ec6 | ||
|
|
01adf0a381 | ||
|
|
0a2abc5dcd | ||
|
|
161a84ff12 | ||
|
|
514761733b | ||
|
|
b12844c3a0 | ||
|
|
c540ba3050 | ||
|
|
b219633fc1 | ||
|
|
793063a68e | ||
|
|
fe25b00537 | ||
|
|
1b77763648 | ||
|
|
aedbccffb4 | ||
|
|
ad4d040431 | ||
|
|
e8157a5491 | ||
|
|
2613399d01 | ||
|
|
f22eccfa89 | ||
|
|
653a139395 | ||
|
|
c7338d01d8 | ||
|
|
b5b37074fb | ||
|
|
97c7161c2e | ||
|
|
5f1e6dff9c | ||
|
|
0b14c2bb87 | ||
|
|
99340e551b | ||
|
|
5e412e341a | ||
|
|
dd6bf47684 | ||
|
|
ef3d39c7f4 | ||
|
|
8bd570b89e | ||
|
|
2b9e55f077 | ||
|
|
7a850ab228 | ||
|
|
3c8b76207c | ||
|
|
5605df8e6c | ||
|
|
ff75b975ab | ||
|
|
44f8a3cb74 | ||
|
|
bf7fc52efc | ||
|
|
1049e60736 | ||
|
|
9b6df85e9e | ||
|
|
34d943bd1e | ||
|
|
259ab328a7 | ||
|
|
1fbac30332 | ||
|
|
fa8c663f5e | ||
|
|
526c82cac0 | ||
|
|
21407be376 | ||
|
|
093782330a | ||
|
|
56c52124ec | ||
|
|
5ca6b8b272 | ||
|
|
ab9a0f43b5 | ||
|
|
d193fabfb9 | ||
|
|
289e6efe62 | ||
|
|
e4c93faa33 | ||
|
|
8ae7fff12b | ||
|
|
b6baa38ce0 | ||
|
|
0c3f62d1e0 | ||
|
|
b867ed5d0b | ||
|
|
a71864fd6e | ||
|
|
8e3c372195 | ||
|
|
f4b588c1ab | ||
|
|
db6d28353a | ||
|
|
41f1d4124a | ||
|
|
e904a746d3 | ||
|
|
973d03baa4 | ||
|
|
fe29129777 | ||
|
|
5bf40bb694 | ||
|
|
531784778a | ||
|
|
388ecc4495 | ||
|
|
ed9485810c | ||
|
|
00234c2c63 | ||
|
|
bcaefcb34d | ||
|
|
84af9c0d40 | ||
|
|
0de7c9c43c | ||
|
|
83a91e1985 | ||
|
|
2f3912a1fa | ||
|
|
94b40d706e | ||
|
|
729051c58d | ||
|
|
8bfbf9937f | ||
|
|
3e8ca9011e | ||
|
|
571b61ded7 | ||
|
|
d3414f3b7b | ||
|
|
9dd51374fe | ||
|
|
6a6b048c6b | ||
|
|
b00f06377f | ||
|
|
d89b30cbeb | ||
|
|
ff74c655e1 | ||
|
|
2c2a8762a9 | ||
|
|
1d14043f20 | ||
|
|
abfa9011f7 | ||
|
|
f64b778835 | ||
|
|
972d9b1b63 | ||
|
|
d83bc6c306 | ||
|
|
c4f0937668 | ||
|
|
4361d2ae38 | ||
|
|
62fda4490c | ||
|
|
6673713eb1 | ||
|
|
a851900a50 | ||
|
|
b1f8acb7d0 | ||
|
|
41117feee7 | ||
|
|
78071b119b | ||
|
|
2d3fc0625f | ||
|
|
2704fe6d72 | ||
|
|
560c8b2846 | ||
|
|
7c894720d0 | ||
|
|
ffaec3b947 | ||
|
|
8a6b5dc373 | ||
|
|
5c2cc0d97d | ||
|
|
0e97e3fffa | ||
|
|
51fd6e3676 | ||
|
|
ca6f8417c0 | ||
|
|
002b75b0ab | ||
|
|
a5cf5cb5d2 | ||
|
|
28182e1a5f | ||
|
|
5bf8c8960b |
96 changed files with 47878 additions and 3480 deletions
13
.busted
13
.busted
|
|
@ -1,13 +0,0 @@
|
|||
return {
|
||||
_all = {
|
||||
coverage = false,
|
||||
lpath = 'lua/?.lua;lua/?/init.lua',
|
||||
lua = 'nlua',
|
||||
},
|
||||
default = {
|
||||
verbose = true,
|
||||
},
|
||||
tests = {
|
||||
verbose = true,
|
||||
},
|
||||
}
|
||||
78
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
78
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
name: Bug Report
|
||||
description: Report a bug
|
||||
title: 'bug: '
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Prerequisites
|
||||
options:
|
||||
- label:
|
||||
I have searched [existing
|
||||
issues](https://github.com/barrettruth/cp.nvim/issues)
|
||||
required: true
|
||||
- label: I have updated to the latest version
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 'Neovim version'
|
||||
description: 'Output of `nvim --version`'
|
||||
render: text
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: 'Operating system'
|
||||
placeholder: 'e.g. Arch Linux, macOS 15, Ubuntu 24.04'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: What happened? What did you expect?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Minimal steps to trigger the bug
|
||||
value: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 'Health check'
|
||||
description: 'Output of `:checkhealth cp`'
|
||||
render: text
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Minimal reproduction
|
||||
description: |
|
||||
Save the script below as `repro.lua`, edit if needed, and run:
|
||||
```
|
||||
nvim -u repro.lua
|
||||
```
|
||||
Confirm the bug reproduces with this config before submitting.
|
||||
render: lua
|
||||
value: |
|
||||
vim.env.LAZY_STDPATH = '.repro'
|
||||
load(vim.fn.system('curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua'))()
|
||||
require('lazy.nvim').setup({
|
||||
spec = {
|
||||
{
|
||||
'barrett-ruth/cp.nvim',
|
||||
opts = {},
|
||||
},
|
||||
},
|
||||
})
|
||||
validations:
|
||||
required: true
|
||||
5
.github/ISSUE_TEMPLATE/config.yaml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yaml
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Questions
|
||||
url: https://github.com/barrettruth/cp.nvim/discussions
|
||||
about: Ask questions and discuss ideas
|
||||
30
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
30
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
name: Feature Request
|
||||
description: Suggest a feature
|
||||
title: 'feat: '
|
||||
labels: [enhancement]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Prerequisites
|
||||
options:
|
||||
- label:
|
||||
I have searched [existing
|
||||
issues](https://github.com/barrettruth/cp.nvim/issues)
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Problem
|
||||
description: What problem does this solve?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Proposed solution
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Alternatives considered
|
||||
66
.github/workflows/ci.yml
vendored
66
.github/workflows/ci.yml
vendored
|
|
@ -1,66 +0,0 @@
|
|||
name: ci
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
lua-format:
|
||||
name: Lua Formatting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: JohnnyMorganz/stylua-action@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: latest
|
||||
args: --check .
|
||||
|
||||
lua-lint:
|
||||
name: Lua Linting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Lint with Selene
|
||||
uses: NTBBloodbath/selene-action@v1.0.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --display-style quiet .
|
||||
|
||||
lua-typecheck:
|
||||
name: Lua Type Checking
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run Lua LS Type Check
|
||||
uses: mrcjkb/lua-typecheck-action@v0
|
||||
with:
|
||||
checklevel: Warning
|
||||
directories: lua
|
||||
configpath: .luarc.json
|
||||
|
||||
python-format:
|
||||
name: Python Formatting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
- name: Install ruff
|
||||
run: uv tool install ruff
|
||||
- name: Check Python formatting with ruff
|
||||
run: ruff format --check scrapers/
|
||||
|
||||
python-lint:
|
||||
name: Python Linting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
- name: Install ruff
|
||||
run: uv tool install ruff
|
||||
- name: Lint Python files with ruff
|
||||
run: ruff check scrapers/
|
||||
21
.github/workflows/luarocks.yaml
vendored
Normal file
21
.github/workflows/luarocks.yaml
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
name: luarocks
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
uses: ./.github/workflows/ci.yaml
|
||||
|
||||
publish:
|
||||
needs: ci
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: nvim-neorocks/luarocks-tag-release@v7
|
||||
env:
|
||||
LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }}
|
||||
17
.github/workflows/luarocks.yml
vendored
17
.github/workflows/luarocks.yml
vendored
|
|
@ -1,17 +0,0 @@
|
|||
name: Push to Luarocks
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
luarocks-upload:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: LuaRocks Upload
|
||||
uses: nvim-neorocks/luarocks-tag-release@v7
|
||||
env:
|
||||
LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }}
|
||||
141
.github/workflows/quality.yaml
vendored
Normal file
141
.github/workflows/quality.yaml
vendored
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
name: quality
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
lua: ${{ steps.changes.outputs.lua }}
|
||||
python: ${{ steps.changes.outputs.python }}
|
||||
markdown: ${{ steps.changes.outputs.markdown }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: changes
|
||||
with:
|
||||
filters: |
|
||||
lua:
|
||||
- 'lua/**'
|
||||
- 'spec/**'
|
||||
- 'plugin/**'
|
||||
- 'after/**'
|
||||
- 'ftdetect/**'
|
||||
- '*.lua'
|
||||
- '.luarc.json'
|
||||
- '*.toml'
|
||||
python:
|
||||
- 'scripts/**/.py'
|
||||
- 'scrapers/**/*.py'
|
||||
- 'tests/**/*.py'
|
||||
- 'pyproject.toml'
|
||||
- 'uv.lock'
|
||||
markdown:
|
||||
- '*.md'
|
||||
- 'docs/**/*.md'
|
||||
|
||||
lua-format:
|
||||
name: Lua Format Check
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.lua == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: JohnnyMorganz/stylua-action@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: 2.1.0
|
||||
args: --check .
|
||||
|
||||
lua-lint:
|
||||
name: Lua Lint Check
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.lua == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Lint with Selene
|
||||
uses: NTBBloodbath/selene-action@v1.0.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --display-style quiet .
|
||||
|
||||
lua-typecheck:
|
||||
name: Lua Type Check
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.lua == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run Lua LS Type Check
|
||||
uses: mrcjkb/lua-typecheck-action@v0
|
||||
with:
|
||||
checklevel: Warning
|
||||
directories: lua
|
||||
configpath: .luarc.json
|
||||
|
||||
python-format:
|
||||
name: Python Format Check
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.python == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
- name: Install ruff
|
||||
run: uv tool install ruff
|
||||
- name: Check Python formatting with ruff
|
||||
run: ruff format --check .
|
||||
|
||||
python-lint:
|
||||
name: Python Lint Check
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.python == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
- name: Install ruff
|
||||
run: uv tool install ruff
|
||||
- name: Lint Python files with ruff
|
||||
run: ruff check .
|
||||
|
||||
python-typecheck:
|
||||
name: Python Type Check
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.python == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
- name: Install dependencies with uv
|
||||
run: uv sync --dev
|
||||
- name: Type check Python files with ty
|
||||
run: uvx ty check .
|
||||
|
||||
markdown-format:
|
||||
name: Markdown Format Check
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.markdown == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: Install prettier
|
||||
run: pnpm add -g prettier@3.1.0
|
||||
- name: Check markdown formatting with prettier
|
||||
run: prettier --check .
|
||||
50
.github/workflows/test.yaml
vendored
Normal file
50
.github/workflows/test.yaml
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
name: tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
lua: ${{ steps.changes.outputs.lua }}
|
||||
python: ${{ steps.changes.outputs.python }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: changes
|
||||
with:
|
||||
filters: |
|
||||
lua:
|
||||
- 'lua/**'
|
||||
- 'spec/**'
|
||||
- 'plugin/**'
|
||||
- 'after/**'
|
||||
- 'ftdetect/**'
|
||||
- '*.lua'
|
||||
- '.luarc.json'
|
||||
- 'stylua.toml'
|
||||
- 'selene.toml'
|
||||
python:
|
||||
- 'scripts/**'
|
||||
- 'scrapers/**'
|
||||
- 'tests/**'
|
||||
- 'pyproject.toml'
|
||||
- 'uv.lock'
|
||||
|
||||
python-test:
|
||||
name: Python Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.python == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
- name: Install dependencies
|
||||
run: uv sync --dev
|
||||
- name: Run Python tests
|
||||
run: uv run pytest tests/ -v
|
||||
21
.github/workflows/test.yml
vendored
21
.github/workflows/test.yml
vendored
|
|
@ -1,21 +0,0 @@
|
|||
name: Run tests
|
||||
on:
|
||||
pull_request: ~
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
neovim_version: ['nightly', 'stable']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run tests
|
||||
uses: nvim-neorocks/nvim-busted-action@v1
|
||||
with:
|
||||
nvim_version: ${{ matrix.neovim_version }}
|
||||
16
.gitignore
vendored
16
.gitignore
vendored
|
|
@ -1,7 +1,19 @@
|
|||
.venv/
|
||||
.venv
|
||||
venv
|
||||
doc/tags
|
||||
*.log
|
||||
build
|
||||
io
|
||||
debug
|
||||
venv/
|
||||
create
|
||||
|
||||
|
||||
.*cache*
|
||||
CLAUDE.md
|
||||
__pycache__
|
||||
.claude/
|
||||
|
||||
node_modules/
|
||||
|
||||
.envrc
|
||||
.direnv/
|
||||
|
|
|
|||
14
.luarc.json
14
.luarc.json
|
|
@ -1,16 +1,8 @@
|
|||
{
|
||||
"runtime.version": "Lua 5.1",
|
||||
"runtime.path": [
|
||||
"lua/?.lua",
|
||||
"lua/?/init.lua"
|
||||
],
|
||||
"diagnostics.globals": [
|
||||
"vim"
|
||||
],
|
||||
"workspace.library": [
|
||||
"$VIMRUNTIME/lua",
|
||||
"${3rd}/luv/library"
|
||||
],
|
||||
"runtime.path": ["lua/?.lua", "lua/?/init.lua"],
|
||||
"diagnostics.globals": ["vim"],
|
||||
"workspace.library": ["$VIMRUNTIME/lua", "${3rd}/luv/library"],
|
||||
"workspace.checkThirdParty": false,
|
||||
"completion.callSnippet": "Replace"
|
||||
}
|
||||
|
|
|
|||
36
.pre-commit-config.yaml
Normal file
36
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
minimum_pre_commit_version: '3.5.0'
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/JohnnyMorganz/StyLua
|
||||
rev: v2.3.1
|
||||
hooks:
|
||||
- id: stylua-github
|
||||
name: stylua (Lua formatter)
|
||||
files: \.lua$
|
||||
pass_filenames: true
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.14.3
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
name: ruff (format)
|
||||
files: \.py$
|
||||
- id: ruff
|
||||
name: ruff (lint imports)
|
||||
args: ['--fix', '--select=I']
|
||||
files: \.py$
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v4.0.0-alpha.8
|
||||
hooks:
|
||||
- id: prettier
|
||||
name: prettier
|
||||
files: \.(md|toml|ya?ml|sh)$
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: ty-type-check
|
||||
name: ty (Python type checker)
|
||||
language: system
|
||||
entry: uv run ty check
|
||||
types: [python]
|
||||
8
.prettierignore
Normal file
8
.prettierignore
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
.pytest_cache/
|
||||
node_modules/
|
||||
.venv/
|
||||
build/
|
||||
dist/
|
||||
*.pyc
|
||||
__pycache__/
|
||||
tests/fixtures/
|
||||
17
.prettierrc
Normal file
17
.prettierrc
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"proseWrap": "always",
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"trailingComma": "none",
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.md"],
|
||||
"options": {
|
||||
"parser": "markdown"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
100
README.md
Normal file
100
README.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# cp.nvim
|
||||
|
||||
**The definitive competitive programming environment for Neovim**
|
||||
|
||||
Scrape problems, run tests, and debug solutions across multiple platforms with
|
||||
zero configuration.
|
||||
|
||||
https://github.com/user-attachments/assets/e81d8dfb-578f-4a79-9989-210164fc0148
|
||||
|
||||
## Features
|
||||
|
||||
- **Multi-platform support**: AtCoder, CodeChef, Codeforces, and CSES
|
||||
- **Automatic problem setup**: Scrape test cases and metadata in seconds
|
||||
- **Dual view modes**: Lightweight I/O view for quick feedback, full panel for
|
||||
detailed analysis
|
||||
- **Test case management**: Quickly view, edit, add, & remove test cases
|
||||
- **Rich test output**: 256 color ANSI support for compiler errors and program
|
||||
output
|
||||
- **Language agnostic**: Works with any language
|
||||
- **Diff viewer**: Compare expected vs actual output with 3 diff modes
|
||||
|
||||
## Installation
|
||||
|
||||
Install using your package manager of choice or via
|
||||
[luarocks](https://luarocks.org/modules/barrettruth/cp.nvim):
|
||||
|
||||
```
|
||||
luarocks install cp.nvim
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- GNU [time](https://www.gnu.org/software/time/) and
|
||||
[timeout](https://www.gnu.org/software/coreutils/manual/html_node/timeout-invocation.html)
|
||||
- [uv](https://docs.astral.sh/uv/) or [nix](https://nixos.org/) for problem
|
||||
scraping
|
||||
|
||||
## Quick Start
|
||||
|
||||
cp.nvim follows a simple principle: **solve locally, submit remotely**.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
1. Find a contest or problem
|
||||
2. Set up contests locally
|
||||
|
||||
```
|
||||
:CP codeforces 1848
|
||||
```
|
||||
|
||||
3. Code and test
|
||||
|
||||
```
|
||||
:CP run
|
||||
```
|
||||
|
||||
4. Navigate between problems
|
||||
|
||||
```
|
||||
:CP next
|
||||
:CP prev
|
||||
:CP e1
|
||||
```
|
||||
|
||||
5. Debug and edit test cases
|
||||
|
||||
```
|
||||
:CP edit
|
||||
:CP panel --debug
|
||||
```
|
||||
|
||||
5. Submit on the original website
|
||||
|
||||
## Documentation
|
||||
|
||||
```vim
|
||||
:help cp.nvim
|
||||
```
|
||||
|
||||
See
|
||||
[my config](https://github.com/barrettruth/dots/blob/main/.config/nvim/lua/plugins/cp.lua)
|
||||
for the setup in the video shown above.
|
||||
|
||||
## Motivation
|
||||
|
||||
I could not find a neovim-centric, efficient, dependency-free, flexible, and
|
||||
easily customizable competitive programming workflow that "just works"--so I
|
||||
made it myself. I conferenced with top competitive programmers at Carnegie
|
||||
Mellon Univerity and the University of Virginia and covered their (and my) pain
|
||||
points:
|
||||
|
||||
- Scraping: contests are automatically loaded asynchronously
|
||||
- Test Case Management: test case editor (`:CP edit`)
|
||||
- UI: both `run` and `panel` layouts cover common formats
|
||||
- Extensibility: snippet plugins, compilation, etc. are left to the programmer
|
||||
|
||||
## Similar Projects
|
||||
|
||||
- [competitest.nvim](https://github.com/xeluxee/competitest.nvim)
|
||||
- [assistant.nvim](https://github.com/A7Lavinraj/assistant.nvim)
|
||||
|
|
@ -4,3 +4,4 @@ vim.opt_local.statuscolumn = ''
|
|||
vim.opt_local.signcolumn = 'no'
|
||||
vim.opt_local.wrap = true
|
||||
vim.opt_local.linebreak = true
|
||||
vim.opt_local.foldcolumn = '0'
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
vim.opt_local.number = false
|
||||
vim.opt_local.relativenumber = false
|
||||
vim.opt_local.statuscolumn = ''
|
||||
vim.opt_local.signcolumn = 'no'
|
||||
vim.opt_local.wrap = true
|
||||
vim.opt_local.linebreak = true
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
vim.opt_local.number = false
|
||||
vim.opt_local.relativenumber = false
|
||||
vim.opt_local.statuscolumn = ''
|
||||
vim.opt_local.signcolumn = 'no'
|
||||
vim.opt_local.wrap = true
|
||||
vim.opt_local.linebreak = true
|
||||
vim.opt_local.modifiable = true
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
vim.opt_local.number = false
|
||||
vim.opt_local.relativenumber = false
|
||||
vim.opt_local.statuscolumn = ''
|
||||
vim.opt_local.signcolumn = 'no'
|
||||
vim.opt_local.wrap = true
|
||||
vim.opt_local.linebreak = true
|
||||
vim.opt_local.foldcolumn = '0'
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
if exists("b:current_syntax")
|
||||
finish
|
||||
endif
|
||||
|
||||
syntax match cpOutputCode /^\[code\]:/
|
||||
syntax match cpOutputTime /^\[time\]:/
|
||||
syntax match cpOutputDebug /^\[debug\]:/
|
||||
syntax match cpOutputOkTrue /^\[ok\]:\ze true$/
|
||||
syntax match cpOutputOkFalse /^\[ok\]:\ze false$/
|
||||
|
||||
highlight default link cpOutputCode DiagnosticInfo
|
||||
highlight default link cpOutputTime Comment
|
||||
highlight default link cpOutputDebug Comment
|
||||
highlight default link cpOutputOkTrue DiffAdd
|
||||
highlight default link cpOutputOkFalse DiffDelete
|
||||
|
||||
let b:current_syntax = "cp"
|
||||
|
|
@ -2,7 +2,7 @@ rockspec_format = '3.0'
|
|||
package = 'cp.nvim'
|
||||
version = 'scm-1'
|
||||
|
||||
source = { url = 'git://github.com/barrett-ruth/cp.nvim' }
|
||||
source = { url = 'git://github.com/barrettruth/cp.nvim' }
|
||||
build = { type = 'builtin' }
|
||||
|
||||
test_dependencies = {
|
||||
|
|
|
|||
937
doc/cp.nvim.txt
Normal file
937
doc/cp.nvim.txt
Normal file
|
|
@ -0,0 +1,937 @@
|
|||
*cp.nvim.txt* Competitive programming plugin for Neovim
|
||||
|
||||
Author: Barrett Ruth <br.barrettruth@gmail.com>
|
||||
License: Same terms as Vim itself (see |license|)
|
||||
|
||||
==============================================================================
|
||||
INTRODUCTION *cp.nvim*
|
||||
|
||||
cp.nvim is a competitive programming plugin that automates problem setup,
|
||||
compilation, and testing workflow for online judges.
|
||||
|
||||
Supported platforms (for now!): AtCoder, Codeforces, CSES
|
||||
|
||||
==============================================================================
|
||||
REQUIREMENTS *cp-requirements*
|
||||
|
||||
- Neovim 0.10.0+
|
||||
- Unix-like operating system
|
||||
- uv package manager (https://docs.astral.sh/uv/)
|
||||
|
||||
==============================================================================
|
||||
SETUP *cp-setup*
|
||||
|
||||
Load cp.nvim with your package manager. For example, with lazy.nvim: >lua
|
||||
{ 'barrettruth/cp.nvim' }
|
||||
<
|
||||
The plugin works automatically with no configuration required. For
|
||||
customization, see |cp-config|.
|
||||
|
||||
==============================================================================
|
||||
CONFIGURATION *cp-config*
|
||||
|
||||
Configuration is done via `vim.g.cp`. Set this before using the plugin:
|
||||
>lua
|
||||
vim.g.cp = {
|
||||
languages = {
|
||||
cpp = {
|
||||
extension = 'cc',
|
||||
commands = {
|
||||
build = { 'g++', '-std=c++17', '{source}', '-o', '{binary}',
|
||||
'-fdiagnostics-color=always' },
|
||||
run = { '{binary}' },
|
||||
debug = { 'g++', '-std=c++17', '-fsanitize=address,undefined',
|
||||
'{source}', '-o', '{binary}' },
|
||||
},
|
||||
},
|
||||
python = {
|
||||
extension = 'py',
|
||||
commands = {
|
||||
run = { 'python', '{source}' },
|
||||
debug = { 'python', '{source}' },
|
||||
},
|
||||
},
|
||||
},
|
||||
platforms = {
|
||||
cses = {
|
||||
enabled_languages = { 'cpp', 'python' },
|
||||
default_language = 'cpp',
|
||||
overrides = {
|
||||
cpp = { extension = 'cpp', commands = { build = { ... } } }
|
||||
},
|
||||
},
|
||||
atcoder = {
|
||||
enabled_languages = { 'cpp', 'python' },
|
||||
default_language = 'cpp',
|
||||
},
|
||||
codeforces = {
|
||||
enabled_languages = { 'cpp', 'python' },
|
||||
default_language = 'cpp',
|
||||
},
|
||||
},
|
||||
open_url = true,
|
||||
debug = false,
|
||||
ui = {
|
||||
ansi = true,
|
||||
run = {
|
||||
width = 0.3,
|
||||
next_test_key = '<c-n>', -- or nil to disable
|
||||
prev_test_key = '<c-p>', -- or nil to disable
|
||||
},
|
||||
panel = {
|
||||
diff_modes = { 'side-by-side', 'git', 'vim' },
|
||||
max_output_lines = 50,
|
||||
},
|
||||
diff = {
|
||||
git = {
|
||||
args = { 'diff', '--no-index', '--word-diff=plain',
|
||||
'--word-diff-regex=.', '--no-prefix' },
|
||||
},
|
||||
},
|
||||
picker = 'telescope',
|
||||
},
|
||||
}
|
||||
<
|
||||
|
||||
By default, C++ (g++ with ISO C++17) and Python are preconfigured under
|
||||
'languages'. Platforms select which languages are enabled and which one is
|
||||
the default; per-platform overrides can tweak 'extension' or 'commands'.
|
||||
|
||||
For example, to run CodeForces contests with Python by default:
|
||||
>lua
|
||||
vim.g.cp = {
|
||||
platforms = {
|
||||
codeforces = {
|
||||
default_language = 'python',
|
||||
},
|
||||
},
|
||||
}
|
||||
<
|
||||
Any language is supported provided the proper configuration. For example, to
|
||||
run CSES problems with Rust using the single schema:
|
||||
>lua
|
||||
vim.g.cp = {
|
||||
languages = {
|
||||
rust = {
|
||||
extension = 'rs',
|
||||
commands = {
|
||||
build = { 'rustc', '{source}', '-o', '{binary}' },
|
||||
run = { '{binary}' },
|
||||
},
|
||||
},
|
||||
},
|
||||
platforms = {
|
||||
cses = {
|
||||
enabled_languages = { 'cpp', 'python', 'rust' },
|
||||
default_language = 'rust',
|
||||
},
|
||||
},
|
||||
}
|
||||
<
|
||||
*cp.Config*
|
||||
Fields: ~
|
||||
{languages} (table<string,|CpLanguage|>) Global language registry.
|
||||
Each language provides an {extension} and {commands}.
|
||||
{platforms} (table<string,|CpPlatform|>) Per-platform enablement,
|
||||
default language, and optional overrides.
|
||||
{hooks} (|cp.Hooks|) Hook functions called at various stages.
|
||||
{debug} (boolean, default: false) Show info messages.
|
||||
{scrapers} (string[]) Supported platform ids.
|
||||
{filename} (function, optional)
|
||||
function(contest, contest_id, problem_id, config, language): string
|
||||
Should return full filename with extension.
|
||||
(default: concatenates contest_id and problem_id, lowercased)
|
||||
{ui} (|CpUI|) UI settings: panel, diff backend, picker.
|
||||
{open_url} (boolean) Open the contest & problem url in the browser
|
||||
when the contest is first opened.
|
||||
|
||||
*CpPlatform*
|
||||
Fields: ~
|
||||
{enabled_languages} (string[]) Language ids enabled on this platform.
|
||||
{default_language} (string) One of {enabled_languages}.
|
||||
{overrides} (table<string,|CpPlatformOverrides|>, optional)
|
||||
Per-language overrides of {extension} and/or {commands}.
|
||||
|
||||
*CpLanguage*
|
||||
Fields: ~
|
||||
{extension} (string) File extension without leading dot.
|
||||
{commands} (|CpLangCommands|) Command templates.
|
||||
|
||||
*CpLangCommands*
|
||||
Fields: ~
|
||||
{build} (string[], optional) For compiled languages.
|
||||
Must include {source} and {binary}.
|
||||
{run} (string[], optional) Runtime command.
|
||||
Compiled: must include {binary}.
|
||||
Interpreted: must include {source}.
|
||||
{debug} (string[], optional) Debug variant; same token rules
|
||||
as {build} (compiled) or {run} (interpreted).
|
||||
|
||||
*CpUI*
|
||||
Fields: ~
|
||||
{ansi} (boolean, default: true) Enable ANSI color parsing
|
||||
and highlighting in both I/O view and panel.
|
||||
{run} (|RunConfig|) I/O view configuration.
|
||||
{panel} (|PanelConfig|) Test panel behavior configuration.
|
||||
{diff} (|DiffConfig|) Diff backend configuration.
|
||||
{picker} (string|nil) 'telescope', 'fzf-lua', or nil.
|
||||
|
||||
*RunConfig*
|
||||
Fields: ~
|
||||
{width} (number, default: 0.3) Width of I/O view splits as
|
||||
fraction of screen (0.0 to 1.0).
|
||||
{next_test_key} (string|nil, default: '<c-n>') Keymap to navigate
|
||||
to next test in I/O view. Set to nil to disable.
|
||||
{prev_test_key} (string|nil, default: '<c-p>') Keymap to navigate
|
||||
to previous test in I/O view. Set to nil to disable.
|
||||
{format_verdict} (|VerdictFormatter|, default: nil) Custom verdict line
|
||||
formatter. See |cp-verdict-format|.
|
||||
|
||||
*EditConfig*
|
||||
Fields: ~
|
||||
{next_test_key} (string|nil, default: ']t') Jump to next test.
|
||||
{prev_test_key} (string|nil, default: '[t') Jump to previous test.
|
||||
{delete_test_key} (string|nil, default: 'gd') Delete current test.
|
||||
{add_test_key} (string|nil, default: 'ga') Add new test.
|
||||
{save_and_exit_key} (string|nil, default: 'q') Save and exit editor.
|
||||
All keys are nil-able. Set to nil to disable.
|
||||
|
||||
*cp.PanelConfig*
|
||||
Fields: ~
|
||||
{diff_modes} (string[], default: {'side-by-side', 'git', 'vim'})
|
||||
List of diff modes to cycle through with 't' key.
|
||||
First element is the default mode.
|
||||
Valid modes: 'side-by-side', 'git', 'vim'.
|
||||
{max_output_lines} (number, default: 50) Maximum lines of test output.
|
||||
|
||||
*cp.DiffConfig*
|
||||
Fields: ~
|
||||
{git} (|cp.DiffGitConfig|) Git diff backend configuration.
|
||||
|
||||
*cp.DiffGitConfig*
|
||||
Fields: ~
|
||||
{args} (string[]) Command-line arguments for git diff.
|
||||
Default: { 'diff', '--no-index', '--word-diff=plain',
|
||||
'--word-diff-regex=.', '--no-prefix' }
|
||||
• --no-index: Compare files outside git repository
|
||||
• --word-diff=plain: Character-level diff markers
|
||||
• --word-diff-regex=.: Split on every character
|
||||
• --no-prefix: Remove a/ b/ prefixes from output
|
||||
|
||||
*cp.Hooks*
|
||||
Fields: ~
|
||||
{before_run} (function, optional) Called before test panel opens.
|
||||
function(state: cp.State)
|
||||
{before_debug} (function, optional) Called before debug build/run.
|
||||
function(state: cp.State)
|
||||
{setup_code} (function, optional) Called after source file is opened.
|
||||
function(state: cp.State)
|
||||
{setup_io_input} (function, optional) Called when I/O input buffer created.
|
||||
function(bufnr: integer, state: cp.State)
|
||||
Default: helpers.clearcol (removes line numbers/columns)
|
||||
{setup_io_output} (function, optional) Called when I/O output buffer created.
|
||||
function(bufnr: integer, state: cp.State)
|
||||
Default: helpers.clearcol (removes line numbers/columns)
|
||||
|
||||
Hook functions receive the cp.nvim state object (|cp.State|). See
|
||||
|lua/cp/state.lua| for available methods and fields.
|
||||
|
||||
The I/O buffer hooks are called once when the buffers are first created
|
||||
during problem setup. Use these to customize buffer appearance (e.g.,
|
||||
remove line numbers, set custom options). Access helpers via:
|
||||
>lua
|
||||
local helpers = require('cp').helpers
|
||||
<
|
||||
Example usage:
|
||||
>lua
|
||||
hooks = {
|
||||
setup_code = function(state)
|
||||
print("Setting up " .. state.get_base_name())
|
||||
print("Source file: " .. state.get_source_file())
|
||||
end,
|
||||
setup_io_input = function(bufnr, state)
|
||||
vim.api.nvim_set_option_value('number', false, { buf = bufnr })
|
||||
end
|
||||
}
|
||||
<
|
||||
|
||||
==============================================================================
|
||||
COMMANDS *cp-commands*
|
||||
|
||||
:CP *:CP*
|
||||
cp.nvim uses a single :CP command with intelligent argument parsing:
|
||||
|
||||
Setup Commands ~
|
||||
:CP {platform} {contest_id} [--lang {language}]
|
||||
Full setup: set platform and load contest metadata.
|
||||
Scrapes test cases and creates source file.
|
||||
--lang: Use specific language (default: platform default)
|
||||
Examples: >
|
||||
:CP codeforces 1933
|
||||
:CP codeforces 1933 --lang python
|
||||
<
|
||||
View Commands ~
|
||||
:CP run [all|n|n,m,...] [--debug]
|
||||
Run tests in I/O view (see |cp-io-view|).
|
||||
Lightweight split showing test verdicts.
|
||||
|
||||
Execution modes:
|
||||
• :CP run Combined: single execution with all tests
|
||||
(auto-switches to individual when multiple samples)
|
||||
• :CP run all Individual: N separate executions
|
||||
• :CP run n Individual: run test n only
|
||||
• :CP run n,m,... Individual: run specific tests (e.g. nth and mth)
|
||||
|
||||
--debug: Use debug build (builds to build/<name>.dbg)
|
||||
|
||||
Combined mode runs all test inputs in one execution (matching
|
||||
platform behavior for multi-test problems). When a problem has
|
||||
multiple independent sample test cases, :CP run auto-switches to
|
||||
individual mode to run each sample separately.
|
||||
|
||||
Examples: >
|
||||
:CP run " Combined: all tests, one execution
|
||||
:CP run all " Individual: all tests, N executions
|
||||
:CP run 2 " Individual: test 2 only
|
||||
:CP run 1,3,5 " Individual: tests 1, 3, and 5
|
||||
:CP run all --debug " Individual with debug build
|
||||
<
|
||||
:CP panel [--debug] [n]
|
||||
Open full-screen test panel (see |cp-panel|).
|
||||
Aggregate table with diff modes for detailed analysis.
|
||||
Optional [n] focuses on specific test.
|
||||
--debug: Use debug build (with sanitizers, etc.)
|
||||
Examples: >
|
||||
:CP panel " All tests
|
||||
:CP panel --debug 3 " Test 3, debug build
|
||||
<
|
||||
|
||||
:CP pick [--lang {language}]
|
||||
Launch configured picker for interactive
|
||||
platform/contest selection.
|
||||
--lang: Pre-select language for chosen contest.
|
||||
Example: >
|
||||
:CP pick
|
||||
:CP pick --lang python
|
||||
<
|
||||
|
||||
:CP interact [script]
|
||||
Open an interactive terminal for the current problem.
|
||||
If an executable interactor is provided, runs the compiled
|
||||
binary against the source file (see
|
||||
*cp-interact*). Otherwise, runs the source
|
||||
file. Only valid for interactive problems.
|
||||
|
||||
Navigation Commands ~
|
||||
:CP next [--lang {language}]
|
||||
Navigate to next problem in current contest.
|
||||
Stops at last problem (no wrapping).
|
||||
--lang: Use specific language for next problem.
|
||||
By default, preserves current file's language if
|
||||
enabled for the new problem, otherwise uses platform
|
||||
default.
|
||||
Examples: >
|
||||
:CP next
|
||||
:CP next --lang python
|
||||
<
|
||||
:CP prev [--lang {language}]
|
||||
Navigate to previous problem in current contest.
|
||||
Stops at first problem (no wrapping).
|
||||
--lang: Use specific language for previous problem.
|
||||
By default, preserves current file's language if
|
||||
enabled for the new problem, otherwise uses platform
|
||||
default.
|
||||
Examples: >
|
||||
:CP prev
|
||||
:CP prev --lang cpp
|
||||
<
|
||||
:CP {problem_id} [--lang {language}]
|
||||
Jump to problem {problem_id} in a contest.
|
||||
Requires that a contest has already been set up.
|
||||
--lang: Use specific language for this problem.
|
||||
Examples: >
|
||||
:CP B
|
||||
:CP C --lang python
|
||||
<
|
||||
|
||||
Edit Commands ~
|
||||
:CP edit [n]
|
||||
Open grid test editor showing all test cases.
|
||||
Tests displayed as 2×N grid (2 rows, N columns):
|
||||
• Top row: Test inputs (editable)
|
||||
• Bottom row: Expected outputs (editable)
|
||||
|
||||
Optional [n]: Jump cursor to test n's input buffer
|
||||
|
||||
Changes saved to both cache and disk on exit,
|
||||
taking effect immediately in :CP run and CLI.
|
||||
|
||||
Keybindings (configurable via |EditConfig|):
|
||||
q Save all and exit editor
|
||||
]t Jump to next test column
|
||||
[t Jump to previous test column
|
||||
gd Delete current test column
|
||||
ga Add new test column at end
|
||||
<c-w> Normal window navigation
|
||||
|
||||
Examples: >
|
||||
:CP edit " Edit all tests
|
||||
:CP edit 3 " Edit all, start at test 3
|
||||
<
|
||||
|
||||
State Restoration ~
|
||||
:CP Restore state from current file.
|
||||
Automatically detects platform, contest, problem,
|
||||
and language from cached state. Use this after
|
||||
switching files to restore your CP environment.
|
||||
|
||||
Cache Commands ~
|
||||
:CP cache clear [platform] [contest]
|
||||
Clear cache data at different granularities:
|
||||
• No args: Clear all cached data
|
||||
• [platform]: Clear all data for a platform
|
||||
• [platform] [contest]: Clear specific contest
|
||||
Examples: >
|
||||
:CP cache clear
|
||||
:CP cache clear codeforces
|
||||
:CP cache clear codeforces 1848
|
||||
<
|
||||
:CP cache read
|
||||
View the cache in a pretty-printed lua buffer.
|
||||
Exit with q.
|
||||
|
||||
Template Variables ~
|
||||
*cp-template-vars*
|
||||
Command templates support variable substitution using {variable} syntax:
|
||||
|
||||
• {source} Source file path (e.g. "abc324a.cpp")
|
||||
• {binary} Output binary path (e.g. "build/abc324a.run" or
|
||||
"build/abc324a.dbg" for debug builds)
|
||||
|
||||
Example template: >
|
||||
build = { 'g++', '{source}', '-o', '{binary}', '-std=c++17' }
|
||||
< Would expand to: >
|
||||
g++ abc324a.cpp -o build/abc324a.run -std=c++17
|
||||
<
|
||||
Debug Builds ~
|
||||
*cp-debug-builds*
|
||||
The --debug flag uses the debug command configuration instead of build:
|
||||
|
||||
• Normal build: commands.build → outputs to build/<name>.run
|
||||
• Debug build: commands.debug → outputs to build/<name>.dbg
|
||||
|
||||
Debug builds typically include sanitizers (address, undefined behavior) to
|
||||
catch memory errors, buffer overflows, and other issues. Both binaries
|
||||
coexist, so you can switch between normal and debug mode without
|
||||
recompiling.
|
||||
|
||||
Example debug configuration: >
|
||||
languages = {
|
||||
cpp = {
|
||||
extension = 'cc',
|
||||
commands = {
|
||||
build = { 'g++', '-std=c++17', '{source}', '-o', '{binary}' },
|
||||
run = { '{binary}' },
|
||||
debug = { 'g++', '-std=c++17', '-fsanitize=address,undefined',
|
||||
'{source}', '-o', '{binary}' },
|
||||
}
|
||||
}
|
||||
}
|
||||
<
|
||||
|
||||
==============================================================================
|
||||
MAPPINGS *cp-mappings*
|
||||
|
||||
cp.nvim provides <Plug> mappings for all primary actions. These dispatch
|
||||
through the same code path as |:CP|.
|
||||
|
||||
*<Plug>(cp-run)*
|
||||
<Plug>(cp-run) Run tests in I/O view. Equivalent to :CP run.
|
||||
|
||||
*<Plug>(cp-panel)*
|
||||
<Plug>(cp-panel) Open full-screen test panel. Equivalent to :CP panel.
|
||||
|
||||
*<Plug>(cp-edit)*
|
||||
<Plug>(cp-edit) Open the test case editor. Equivalent to :CP edit.
|
||||
|
||||
*<Plug>(cp-next)*
|
||||
<Plug>(cp-next) Navigate to the next problem. Equivalent to :CP next.
|
||||
|
||||
*<Plug>(cp-prev)*
|
||||
<Plug>(cp-prev) Navigate to the previous problem. Equivalent to :CP prev.
|
||||
|
||||
*<Plug>(cp-pick)*
|
||||
<Plug>(cp-pick) Launch the contest picker. Equivalent to :CP pick.
|
||||
|
||||
*<Plug>(cp-interact)*
|
||||
<Plug>(cp-interact) Open interactive mode. Equivalent to :CP interact.
|
||||
|
||||
Example configuration: >lua
|
||||
vim.keymap.set('n', '<leader>cr', '<Plug>(cp-run)')
|
||||
vim.keymap.set('n', '<leader>cp', '<Plug>(cp-panel)')
|
||||
vim.keymap.set('n', '<leader>ce', '<Plug>(cp-edit)')
|
||||
vim.keymap.set('n', '<leader>cn', '<Plug>(cp-next)')
|
||||
vim.keymap.set('n', '<leader>cN', '<Plug>(cp-prev)')
|
||||
vim.keymap.set('n', '<leader>cc', '<Plug>(cp-pick)')
|
||||
vim.keymap.set('n', '<leader>ci', '<Plug>(cp-interact)')
|
||||
<
|
||||
|
||||
==============================================================================
|
||||
LANGUAGE SELECTION *cp-lang-selection*
|
||||
|
||||
cp.nvim supports multiple languages per problem. Each platform enables specific
|
||||
languages and has a default. You can override the language for any setup or
|
||||
navigation command using the --lang flag.
|
||||
|
||||
Language Selection Behavior ~
|
||||
|
||||
When setting up or navigating to a problem:
|
||||
|
||||
1. Explicit --lang flag takes highest priority
|
||||
2. If no --lang flag, tries to preserve current file's language
|
||||
(only if that language is enabled for the new problem)
|
||||
3. Falls back to platform's default language
|
||||
|
||||
Multiple Solution Files ~
|
||||
|
||||
Different languages create different solution files. For example:
|
||||
1848a.cc (C++ solution)
|
||||
1848a.py (Python solution)
|
||||
|
||||
Both files can exist simultaneously with their own state. Switching between
|
||||
languages means switching between different files.
|
||||
|
||||
Examples ~
|
||||
>
|
||||
:CP codeforces 1848 " Use platform default (likely C++)
|
||||
:CP codeforces 1848 --lang python " Use Python explicitly
|
||||
|
||||
" In 1848a.cc (C++ file):
|
||||
:CP next " Next problem tries to use C++
|
||||
:CP next --lang python " Next problem uses Python
|
||||
|
||||
" In 1848a.py (Python file):
|
||||
:CP next " Next problem tries to use Python
|
||||
:CP next --lang cpp " Next problem switches to C++
|
||||
<
|
||||
Language Validation ~
|
||||
|
||||
If you request a language that isn't enabled for a platform, cp.nvim will show
|
||||
a helpful error message listing available languages for that platform.
|
||||
|
||||
==============================================================================
|
||||
WORKFLOW *cp-workflow*
|
||||
|
||||
For the sake of consistency and simplicity, cp.nvim extracts contest/problem
|
||||
identifiers from URLs. This means that, for example, CodeForces/AtCoder
|
||||
contests are configured by their round id rather than round number. See below.
|
||||
|
||||
==============================================================================
|
||||
PLATFORM-SPECIFIC USAGE *cp-platforms*
|
||||
|
||||
AtCoder ~
|
||||
*cp-atcoder*
|
||||
URL format:
|
||||
https://atcoder.jp/contests/{contest_id}/tasks/{contest_id}_{problem_id}
|
||||
|
||||
Usage examples: >
|
||||
:CP atcoder abc324 " Set up atcoder.jp/contests/abc324
|
||||
:CP atcoder abc324 --lang python " Set up with Python instead of default
|
||||
|
||||
Codeforces ~
|
||||
*cp-codeforces*
|
||||
URL format: https://codeforces.com/contest/{contest_id}/problem/{problem_id}
|
||||
|
||||
Usage examples: >
|
||||
:CP codeforces 1934 " Set up codeforces.com/contest/1934
|
||||
:CP codeforces 1934 --lang cpp " Set up with C++
|
||||
|
||||
CSES ~
|
||||
*cp-cses*
|
||||
URL format: https://cses.fi/problemset/task/{problem_id}
|
||||
|
||||
Usage examples: >
|
||||
:CP cses dynamic_programming " Set up all problems in dp category
|
||||
|
||||
==============================================================================
|
||||
|
||||
COMPLETE WORKFLOW EXAMPLE *cp-example*
|
||||
|
||||
Example: Setting up and solving AtCoder contest ABC324
|
||||
|
||||
1. Browse to https://atcoder.jp/contests/abc324
|
||||
|
||||
2. Set up entire contest (bulk setup): >
|
||||
:CP atcoder abc324
|
||||
< This scrapes all test case data, downloads all test cases,
|
||||
and opens the first problem.
|
||||
|
||||
3. Code your solution, then test: >
|
||||
:CP run
|
||||
< View test verdicts in I/O splits. For detailed analysis: >
|
||||
:CP panel
|
||||
< Navigate tests with <c-n>/<c-p>, exit with q
|
||||
|
||||
4. Move to next problem: >
|
||||
:CP next
|
||||
< This automatically sets up the next problem (likely problem B)
|
||||
|
||||
5. Continue solving problems with :CP next/:CP prev navigation
|
||||
|
||||
6. Try a different language for a problem: >
|
||||
:CP C --lang python
|
||||
< Opens problem C with Python instead of C++
|
||||
|
||||
7. Switch to another file (e.g. previous contest): >
|
||||
:e ~/contests/abc323/a.cpp
|
||||
:CP
|
||||
< Automatically restores abc323 contest context
|
||||
|
||||
8. Submit solutions on AtCoder website
|
||||
|
||||
==============================================================================
|
||||
I/O VIEW *cp-io-view*
|
||||
|
||||
The I/O view provides lightweight test feedback in persistent side splits.
|
||||
Test outputs are concatenated with verdict summaries at the bottom.
|
||||
The |cp-panel| offers more fine-grained analysis with diff modes.
|
||||
|
||||
Execution Modes ~
|
||||
|
||||
The I/O view supports two execution modes:
|
||||
|
||||
Combined Mode (:CP run with single sample)
|
||||
• Single execution with all test inputs concatenated
|
||||
• Matches platform behavior (e.g. Codeforces multi-test format)
|
||||
• Shows one verdict for the entire execution
|
||||
• Input split: All test inputs concatenated
|
||||
• Output split: Single program output + verdict
|
||||
• Used when problem has one sample containing multiple test cases
|
||||
|
||||
Individual Mode (:CP run all / :CP run n / :CP run n,m,...)
|
||||
• Separate execution for each test case
|
||||
• Per-test verdicts for debugging
|
||||
• Input split: Selected test inputs concatenated
|
||||
• Output split: All test outputs concatenated + per-test verdicts
|
||||
• Auto-selected when problem has multiple independent samples
|
||||
|
||||
Layout ~
|
||||
|
||||
The I/O view appears as 30% width splits on the right side: >
|
||||
|
||||
┌──────────────────────────┬─────────────────────────────────────────────┐
|
||||
│ │ Output (Top Split) │
|
||||
│ │ 5 510 │
|
||||
│ │ │
|
||||
│ │ 7 714 │
|
||||
│ Solution Code │ │
|
||||
│ │ Test 1: WA | 212.07/2000 ms | 1/512 MB |...│
|
||||
│ │ Test 2: WA | 81.94/2000 ms | 1/512 MB |...│
|
||||
│ ├─────────────────────────────────────────────┤
|
||||
│ │ Input (Bottom Split) │
|
||||
│ │ 1 2 3 │
|
||||
│ │ │
|
||||
│ │ 4 5 6 │
|
||||
└──────────────────────────┴─────────────────────────────────────────────┘
|
||||
<
|
||||
The output split shows:
|
||||
1. Program output (raw, preserving all formatting)
|
||||
2. Space-aligned verdict summary with:
|
||||
- Test number and status (AC/WA/TLE/MLE/RTE with color highlighting)
|
||||
- Runtime: actual/limit in milliseconds
|
||||
- Memory: actual/limit in megabytes
|
||||
- Exit code (with signal name for crashes)
|
||||
|
||||
Usage ~
|
||||
|
||||
:CP run Combined mode: all tests in one execution
|
||||
:CP run all Individual mode: all tests separately
|
||||
:CP run 3 Individual mode: test 3 only
|
||||
:CP run 1,3,5 Individual mode: specific tests (1, 3, and 5)
|
||||
|
||||
Navigation ~
|
||||
|
||||
While in the I/O view buffers, use the configured keymaps to cycle through tests:
|
||||
<c-n> Next test (default, see |RunConfig|.next_test_key)
|
||||
<c-p> Previous test (default, see |RunConfig|.prev_test_key)
|
||||
|
||||
Buffer Customization ~
|
||||
|
||||
Use the setup_io_input and setup_io_output hooks (see |cp.Hooks|) to customize
|
||||
buffer appearance. By default, line numbers and columns are removed via
|
||||
helpers.clearcol (see |cp-helpers|).
|
||||
|
||||
==============================================================================
|
||||
VERDICT FORMATTING *cp-verdict-format*
|
||||
|
||||
Customize how verdict summaries appear in the I/O view using format_verdict.
|
||||
|
||||
Configuration ~
|
||||
|
||||
Set ui.run.format_verdict to a function that formats verdict data: >lua
|
||||
format_verdict = function(data)
|
||||
return { line = "...", highlights = {...} }
|
||||
end
|
||||
<
|
||||
Format Function ~
|
||||
*VerdictFormatter*
|
||||
Input: |VerdictFormatData| table with test results
|
||||
Output: |VerdictFormatResult| table with formatted line and optional highlights
|
||||
|
||||
*VerdictFormatData*
|
||||
{index} (integer) Test case number
|
||||
{status} (table) { text: string, highlight_group: string }
|
||||
{time_ms} (number) Execution time in milliseconds
|
||||
{time_limit_ms} (number) Time limit in milliseconds
|
||||
{memory_mb} (number) Peak memory usage in megabytes
|
||||
{memory_limit_mb} (number) Memory limit in megabytes
|
||||
{exit_code} (integer) Process exit code
|
||||
{signal} (string|nil) Signal name for crashes (e.g. "SIGSEGV")
|
||||
{time_actual_width} (integer|nil) Dynamic width for time value alignment
|
||||
{time_limit_width} (integer|nil) Dynamic width for time limit alignment
|
||||
{mem_actual_width} (integer|nil) Dynamic width for memory value alignment
|
||||
{mem_limit_width} (integer|nil) Dynamic width for memory limit alignment
|
||||
|
||||
*VerdictFormatResult*
|
||||
{line} (string) The formatted verdict line
|
||||
{highlights} (table[], optional) Highlight regions:
|
||||
{col_start} (integer) Start column (0-indexed)
|
||||
{col_end} (integer) End column (exclusive)
|
||||
{group} (string) Highlight group name
|
||||
|
||||
Examples ~
|
||||
|
||||
Minimal format: >lua
|
||||
format_verdict = function(data)
|
||||
return {
|
||||
line = string.format("#%d %s", data.index, data.status.text)
|
||||
}
|
||||
end
|
||||
<
|
||||
See |cp-helpers| for alignment functions: pad_right, pad_left, center.
|
||||
|
||||
==============================================================================
|
||||
PICKER INTEGRATION *cp-picker*
|
||||
|
||||
When picker integration is enabled in configuration, cp.nvim provides interactive
|
||||
platform and contest selection using telescope.nvim or fzf-lua.
|
||||
|
||||
:CP pick *:CP-pick*
|
||||
Launch configured picker for interactive problem selection.
|
||||
Control Flow: Select Platform → Contest → Code!
|
||||
|
||||
Requires picker = 'telescope' or picker = 'fzf-lua' in configuration.
|
||||
Requires corresponding plugin (telescope.nvim or fzf-lua) to be installed.
|
||||
|
||||
PICKER KEYMAPS *cp-picker-keys*
|
||||
<c-r> Force refresh/update contest list.
|
||||
Useful when contest lists are outdated or incomplete
|
||||
|
||||
==============================================================================
|
||||
PANEL *cp-panel*
|
||||
|
||||
The panel provides full-screen test analysis with diff modes for detailed
|
||||
debugging. Problem time/memory limit constraints are in columns Time/Mem
|
||||
respectively. Used time/memory are in columns Runtime/RSS respectively.
|
||||
|
||||
Access with :CP panel or :CP panel --debug (uses debug build configuration).
|
||||
|
||||
Interface ~
|
||||
|
||||
The panel uses the following table layout: >
|
||||
|
||||
┌─────┬────────┬──────────────┬───────────┬──────────┬──────────┬─────────────┐
|
||||
│ # │ Status │ Runtime (ms) │ Time (ms) │ RSS (MB) │ Mem (MB) │ Exit Code │
|
||||
├─────┼────────┼──────────────┼───────────┼──────────┼──────────┼─────────────┤
|
||||
│ 1 │ AC │ 12.0 │ 2000 │ 123 │ 256 │ 0 │
|
||||
│ >2 │ WA │ 45.70 │ 2000 │ 100 │ 256 │ 1 │
|
||||
├─────┴────────┴──────────────┴───────────┴──────────┴──────────┴─────────────┤
|
||||
│ Input: │
|
||||
│ 5 3 │
|
||||
├─────┬────────┬──────────────┬───────────┬──────────┬──────────┬─────────────┤
|
||||
│ 3 │ TLE │ 9.0 │ 2000 │ 256 │ 256 │ 136 (SIGBUS)│
|
||||
│ 4 │ RTE │ 0.0 │ 2000 │ 256 │ 256 │139 (SIGUSR2)│
|
||||
└─────┴────────┴──────────────┴───────────┴──────────┴──────────┴─────────────┘
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Expected vs Actual │
|
||||
│ 423 │
|
||||
│ 100 │
|
||||
│ hello world │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Status Indicators ~
|
||||
|
||||
Test cases use competitive programming terminology with color highlighting:
|
||||
|
||||
AC Accepted (passed)
|
||||
WA Wrong Answer (output mismatch)
|
||||
TLE Time Limit Exceeded (timeout)
|
||||
MLE Memory Limit Exceeded Error (heuristic)
|
||||
RTE Runtime Error (other non-zero exit code)
|
||||
NA Any other state
|
||||
<
|
||||
|
||||
==============================================================================
|
||||
INTERACTIVE MODE *cp-interact*
|
||||
|
||||
Run interactive problems manually or with an orchestrator. :CP interact is
|
||||
available for interactive problems. Test cases are ignored in interactive mode
|
||||
(no run panel, no diffs).
|
||||
|
||||
When using :CP interact {interactor}, the interactor must be executable
|
||||
(chmod +x). Completion after :CP interact suggests executables in CWD.
|
||||
|
||||
1) Terminal-only ~
|
||||
:CP interact
|
||||
Execute the current program and open an interactive terminal running
|
||||
it directly. Use this for manual testing.
|
||||
|
||||
2) Orchestrated ~
|
||||
:CP interact {interactor}
|
||||
Execute the current program and open an interactive terminal that runs
|
||||
your interactor script against it.
|
||||
{interactor} is an executable file relative to the CWD.
|
||||
Example:
|
||||
:CP interact my-executable-interactor.py
|
||||
|
||||
|
||||
Keymaps ~
|
||||
<c-q> Close the terminal and restore the previous layout.
|
||||
|
||||
==============================================================================
|
||||
ANSI COLORS AND HIGHLIGHTING *cp-ansi*
|
||||
|
||||
cp.nvim provides comprehensive ANSI color support and highlighting for
|
||||
compiler output, program stderr, and diff visualization.
|
||||
|
||||
If you cannot see color highlighting in your config, it is likely due to an
|
||||
erroneous config. Most tools (GCC, Python, Clang, Rustc) color stdout based on
|
||||
whether stdout is connected to a terminal. One can usually get aorund this by
|
||||
leveraging flags to force colored output. For example, to force colors with GCC,
|
||||
alter your config as follows:
|
||||
>lua
|
||||
{
|
||||
commands = {
|
||||
build = {
|
||||
'g++',
|
||||
'-fdiagnostics-color=always',
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
<
|
||||
|
||||
==============================================================================
|
||||
HIGHLIGHT GROUPS *cp-highlights*
|
||||
|
||||
Test Status Groups ~
|
||||
|
||||
All test status groups link to builtin highlight groups, automatically adapting
|
||||
to your colorscheme:
|
||||
|
||||
CpTestAC Links to DiagnosticOk (AC status)
|
||||
CpTestWA Links to DiagnosticError (WA status)
|
||||
CpTestTLE Links to DiagnosticWarn (TLE status)
|
||||
CpTestMLE Links to DiagnosticWarn (MLE status)
|
||||
CpTestRTE Links to DiagnosticHint (RTE status)
|
||||
CpTestNA Links to Comment (pending/unknown status)
|
||||
|
||||
ANSI Color Groups ~
|
||||
|
||||
cp.nvim preserves ANSI colors from compiler output and program stderr using
|
||||
a sophisticated parsing system. Colors are automatically mapped to your
|
||||
terminal colorscheme via vim.g.terminal_color_* variables.
|
||||
|
||||
Diff Highlighting ~
|
||||
|
||||
The git diff backend uses Neovim's built-in highlight groups that automatically
|
||||
adapt to your colorscheme:
|
||||
|
||||
DiffAdd Highlights added text in git diffs
|
||||
DiffDelete Highlights removed text in git diffs
|
||||
|
||||
==============================================================================
|
||||
TERMINAL COLOR INTEGRATION *cp-terminal-colors*
|
||||
|
||||
ANSI colors automatically use the terminal's color palette through Neovim's
|
||||
vim.g.terminal_color_* variables.
|
||||
|
||||
==============================================================================
|
||||
HIGHLIGHT CUSTOMIZATION *cp-highlight-custom*
|
||||
|
||||
Customize highlight groups after your colorscheme loads:
|
||||
>lua
|
||||
vim.api.nvim_create_autocmd('ColorScheme', {
|
||||
callback = function()
|
||||
vim.api.nvim_set_hl(0, 'CpTestAC', { link = 'String' })
|
||||
end
|
||||
})
|
||||
|
||||
==============================================================================
|
||||
HELPERS *cp-helpers*
|
||||
|
||||
The helpers module provides utility functions for buffer customization.
|
||||
Access via:
|
||||
>lua
|
||||
local helpers = require('cp').helpers
|
||||
<
|
||||
Functions ~
|
||||
|
||||
helpers.clearcol({bufnr}) *helpers.clearcol*
|
||||
Remove line numbers, columns, and signs from buffer.
|
||||
Sets:
|
||||
• number = false
|
||||
• relativenumber = false
|
||||
• signcolumn = 'no'
|
||||
• statuscolumn = ''
|
||||
|
||||
Parameters: ~
|
||||
{bufnr} (integer) Buffer handle
|
||||
|
||||
==============================================================================
|
||||
PANEL KEYMAPS *cp-panel-keys*
|
||||
|
||||
<c-n> Navigate to next test case
|
||||
<c-p> Navigate to previous test case
|
||||
t Cycle through configured diff modes (see |cp.PanelConfig|)
|
||||
q Exit panel and restore layout
|
||||
<c-q> Exit interactive terminal and restore layout
|
||||
|
||||
Diff Modes ~
|
||||
|
||||
Three diff modes are available:
|
||||
|
||||
side-by-side Expected and actual output shown side-by-side (default)
|
||||
vim Built-in vim diff (always available)
|
||||
git Character-level git word-diff (requires git, more precise)
|
||||
|
||||
Configure which modes to cycle through via |cp.PanelConfig|.diff_modes.
|
||||
The first element is used as the default mode.
|
||||
|
||||
The git backend shows character-level changes with [-removed-] and {+added+}
|
||||
markers.
|
||||
|
||||
Execution Details ~
|
||||
|
||||
Test cases are executed individually using the same compilation and
|
||||
execution pipeline, but with isolated input/output for
|
||||
precise failure analysis.
|
||||
|
||||
==============================================================================
|
||||
FILE STRUCTURE *cp-files*
|
||||
|
||||
cp.nvim creates the following file structure upon problem setup: >
|
||||
|
||||
{problem_id}.{ext} " Source file
|
||||
build/
|
||||
{problem_id}.run " Compiled binary
|
||||
io/
|
||||
{problem_id}.n.cpin " nth test input
|
||||
{problem_id}.n.cpout " nth test expected output
|
||||
<
|
||||
==============================================================================
|
||||
HEALTH CHECK *cp-health*
|
||||
|
||||
Run |:checkhealth| cp to verify your setup.
|
||||
|
||||
vim:tw=78:ts=8:ft=help:norl:
|
||||
333
doc/cp.txt
333
doc/cp.txt
|
|
@ -1,333 +0,0 @@
|
|||
*cp.txt* Competitive programming plugin for Neovim
|
||||
|
||||
Author: Barrett Ruth <br.barrettruth@gmail.com>
|
||||
License: Same terms as Vim itself (see |license|)
|
||||
|
||||
INTRODUCTION *cp* *cp.nvim*
|
||||
|
||||
cp.nvim is a competitive programming plugin that automates problem setup,
|
||||
compilation, and testing workflow for online judges.
|
||||
|
||||
Supported platforms: AtCoder, Codeforces, CSES
|
||||
Supported languages: C++, Python
|
||||
|
||||
REQUIREMENTS *cp-requirements*
|
||||
|
||||
- Neovim 0.10.0+
|
||||
- uv package manager (https://docs.astral.sh/uv/)
|
||||
- Language runtime/compiler (g++, python3)
|
||||
|
||||
Optional:
|
||||
- LuaSnip for template expansion (https://github.com/L3MON4D3/LuaSnip)
|
||||
|
||||
COMMANDS *cp-commands*
|
||||
|
||||
*:CP*
|
||||
cp.nvim uses a single :CP command with intelligent argument parsing:
|
||||
|
||||
Setup Commands ~
|
||||
|
||||
:CP {platform} {contest_id} {problem_id} [--lang={language}]
|
||||
Full setup: set platform, load contest metadata,
|
||||
and set up specific problem. Scrapes test cases
|
||||
and creates source file.
|
||||
Example: :CP codeforces 1933 a
|
||||
Example: :CP codeforces 1933 a --lang=python
|
||||
|
||||
:CP {platform} {contest_id} Contest setup: set platform and load contest
|
||||
metadata for navigation. Caches problem list.
|
||||
Example: :CP atcoder abc324
|
||||
|
||||
:CP {platform} Platform setup: set platform only.
|
||||
Example: :CP cses
|
||||
|
||||
:CP {problem_id} [--lang={language}]
|
||||
Problem switch: switch to different problem
|
||||
within current contest context.
|
||||
Example: :CP b (switch to problem b)
|
||||
Example: :CP b --lang=python
|
||||
|
||||
Action Commands ~
|
||||
|
||||
:CP test [--debug] Toggle test panel for individual test case
|
||||
debugging. Shows per-test results with three-pane
|
||||
layout for easy Expected/Actual comparison.
|
||||
Use --debug flag to compile with debug flags
|
||||
Requires contest setup first.
|
||||
|
||||
Navigation Commands ~
|
||||
|
||||
:CP next Navigate to next problem in current contest.
|
||||
Stops at last problem (no wrapping).
|
||||
|
||||
:CP prev Navigate to previous problem in current contest.
|
||||
Stops at first problem (no wrapping).
|
||||
|
||||
CONFIGURATION *cp-config*
|
||||
|
||||
cp.nvim works out of the box. No setup required.
|
||||
|
||||
Optional configuration with lazy.nvim: >
|
||||
{
|
||||
'barrett-ruth/cp.nvim',
|
||||
cmd = 'CP',
|
||||
opts = {
|
||||
debug = false,
|
||||
scrapers = {
|
||||
atcoder = true,
|
||||
codeforces = false, -- disable codeforces scraping
|
||||
cses = true,
|
||||
},
|
||||
contests = {
|
||||
codeforces = {
|
||||
cpp = {
|
||||
compile = {
|
||||
'g++', '-std=c++{version}', '-O2', '-Wall', '-Wextra',
|
||||
'-DLOCAL', '{source}', '-o', '{binary}',
|
||||
},
|
||||
run = { '{binary}' },
|
||||
debug = {
|
||||
'g++', '-std=c++{version}', '-g3',
|
||||
'-fsanitize=address,undefined', '-DLOCAL',
|
||||
'{source}', '-o', '{binary}',
|
||||
},
|
||||
version = 23,
|
||||
extension = "cc",
|
||||
},
|
||||
python = {
|
||||
run = { 'python3', '{source}' },
|
||||
debug = { 'python3', '{source}' },
|
||||
extension = "py",
|
||||
},
|
||||
default_language = "cpp",
|
||||
timeout_ms = 2000,
|
||||
},
|
||||
},
|
||||
hooks = {
|
||||
before_run = function(ctx) vim.cmd.w() end,
|
||||
before_debug = function(ctx)
|
||||
-- ctx.problem_id, ctx.platform, ctx.source_file, etc.
|
||||
vim.cmd.w()
|
||||
end,
|
||||
setup_code = function(ctx)
|
||||
vim.wo.foldmethod = "marker"
|
||||
vim.wo.foldmarker = "{{{,}}}"
|
||||
vim.diagnostic.enable(false)
|
||||
end,
|
||||
},
|
||||
snippets = { ... }, -- LuaSnip snippets
|
||||
filename = function(contest, contest_id, problem_id, config, language) ... end,
|
||||
}
|
||||
}
|
||||
<
|
||||
|
||||
*cp.Config*
|
||||
|
||||
Fields: ~
|
||||
• {contests} (`table<string,ContestConfig>`) Contest configurations.
|
||||
• {hooks} (`cp.Hooks`) Hook functions called at various stages.
|
||||
• {snippets} (`table[]`) LuaSnip snippet definitions.
|
||||
• {debug} (`boolean`, default: `false`) Show info messages
|
||||
during operation.
|
||||
• {scrapers} (`table<string,boolean>`) Per-platform scraper control.
|
||||
Default enables all platforms.
|
||||
• {filename}? (`function`) Custom filename generation function.
|
||||
`function(contest, contest_id, problem_id, config, language)`
|
||||
Should return full filename with extension.
|
||||
(default: concats contest_id and problem id)
|
||||
|
||||
*cp.ContestConfig*
|
||||
|
||||
Fields: ~
|
||||
• {cpp} (`LanguageConfig`) C++ language configuration.
|
||||
• {python} (`LanguageConfig`) Python language configuration.
|
||||
• {default_language} (`string`, default: `"cpp"`) Default language when
|
||||
`--lang` not specified.
|
||||
• {timeout_ms} (`number`, default: `2000`) Execution timeout in
|
||||
milliseconds.
|
||||
|
||||
*cp.LanguageConfig*
|
||||
|
||||
Fields: ~
|
||||
• {compile}? (`string[]`) Compile command template with
|
||||
`{version}`, `{source}`, `{binary}` placeholders.
|
||||
• {run} (`string[]`) Run command template.
|
||||
• {debug}? (`string[]`) Debug compile command template.
|
||||
• {version}? (`number`) Language version (e.g. 20, 23 for C++).
|
||||
• {extension} (`string`) File extension (e.g. "cc", "py").
|
||||
• {executable}? (`string`) Executable name for interpreted languages.
|
||||
|
||||
*cp.Hooks*
|
||||
|
||||
Fields: ~
|
||||
• {before_debug}? (`function`) Called before debug compilation.
|
||||
`function(ctx: ProblemContext)`
|
||||
• {setup_code}? (`function`) Called after source file is opened.
|
||||
Used to configure buffer settings.
|
||||
`function(ctx: ProblemContext)`
|
||||
|
||||
WORKFLOW *cp-workflow*
|
||||
|
||||
For the sake of consistency and simplicity, cp.nvim extracts contest/problem identifiers from
|
||||
URLs. This means that, for example, CodeForces/AtCoder contests are configured by
|
||||
their round id rather than round number. See below.
|
||||
|
||||
PLATFORM-SPECIFIC USAGE *cp-platforms*
|
||||
|
||||
AtCoder ~
|
||||
*cp-atcoder*
|
||||
URL format: https://atcoder.jp/contests/abc123/tasks/abc123_a
|
||||
|
||||
In terms of cp.nvim, this corresponds to:
|
||||
- Platform: atcoder
|
||||
- Contest ID: abc123
|
||||
- Problem ID: a
|
||||
|
||||
Usage examples: >
|
||||
:CP atcoder abc123 a " Full setup: problem A from contest ABC123
|
||||
:CP atcoder abc123 " Contest setup: load contest metadata only
|
||||
:CP b " Switch to problem B (if contest loaded)
|
||||
:CP next " Navigate to next problem in contest
|
||||
<
|
||||
Codeforces ~
|
||||
*cp-codeforces*
|
||||
URL format: https://codeforces.com/contest/1234/problem/A
|
||||
|
||||
In terms of cp.nvim, this corresponds to:
|
||||
- Platform: codeforces
|
||||
- Contest ID: 1234
|
||||
- Problem ID: a (lowercase)
|
||||
|
||||
Usage examples: >
|
||||
:CP codeforces 1934 a " Full setup: problem A from contest 1934
|
||||
:CP codeforces 1934 " Contest setup: load contest metadata only
|
||||
:CP c " Switch to problem C (if contest loaded)
|
||||
:CP prev " Navigate to previous problem in contest
|
||||
<
|
||||
CSES ~
|
||||
*cp-cses*
|
||||
URL format: https://cses.fi/problemset/task/1068
|
||||
|
||||
CSES is organized by categories rather than contests. Currently all problems
|
||||
are grouped under "CSES Problem Set" category.
|
||||
|
||||
In terms of cp.nvim, this corresponds to:
|
||||
- Platform: cses
|
||||
- Contest ID: "CSES Problem Set" (category)
|
||||
- Problem ID: 1068 (numeric)
|
||||
|
||||
Usage examples: >
|
||||
:CP cses 1068 " Set up problem 1068 from CSES
|
||||
:CP 1070 " Switch to problem 1070 (if CSES loaded)
|
||||
:CP next " Navigate to next problem in CSES
|
||||
<
|
||||
COMPLETE WORKFLOW EXAMPLE *cp-example*
|
||||
|
||||
Example: Setting up and solving AtCoder contest ABC324
|
||||
|
||||
1. Browse to https://atcoder.jp/contests/abc324
|
||||
2. Set up contest and load metadata: >
|
||||
:CP atcoder abc324
|
||||
< This caches all problems (A, B, C, D, E, F, G) for navigation
|
||||
|
||||
3. Start with problem A: >
|
||||
:CP a
|
||||
< This creates a.cc and scrapes test cases
|
||||
|
||||
4. Code your solution, then test: >
|
||||
:CP test
|
||||
< Navigate with j/k, run specific tests with <enter>
|
||||
Exit test panel with q or :CP test when done
|
||||
|
||||
5. If needed, debug with sanitizers: >
|
||||
:CP test --debug
|
||||
<
|
||||
6. Move to next problem: >
|
||||
:CP next
|
||||
< This automatically sets up problem B
|
||||
|
||||
6. Continue solving problems with :CP next/:CP prev navigation
|
||||
7. Submit solutions on AtCoder website
|
||||
|
||||
Example: Quick setup for single Codeforces problem >
|
||||
:CP codeforces 1933 a " One command setup
|
||||
:CP test " Test immediately
|
||||
<
|
||||
|
||||
TEST PANEL *cp-test*
|
||||
|
||||
The test panel provides individual test case debugging with a three-pane
|
||||
layout showing test list, expected output, and actual output side-by-side.
|
||||
|
||||
Activation ~
|
||||
*:CP-test*
|
||||
:CP test [--debug] Toggle test panel on/off. When activated,
|
||||
replaces current layout with test interface.
|
||||
Automatically compiles and runs all tests.
|
||||
Use --debug flag to compile with debug symbols
|
||||
and sanitizers. Toggle again to restore original
|
||||
layout.
|
||||
|
||||
Interface ~
|
||||
|
||||
The test panel uses a three-pane layout for easy comparison: >
|
||||
|
||||
┌─ Test List ─────────────────────────────────────────────────┐
|
||||
│ 1. PASS 12ms │
|
||||
│> 2. FAIL 45ms │
|
||||
│ │
|
||||
│ ── Input ── │
|
||||
│ 5 3 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
┌─ Expected ──────────────┐ ┌─ Actual ────────────────┐
|
||||
│ 8 │ │ 7 │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
└─────────────────────────┘ └─────────────────────────┘
|
||||
<
|
||||
|
||||
Keymaps ~
|
||||
*cp-test-keys*
|
||||
j / <Down> Navigate to next test case
|
||||
k / <Up> Navigate to previous test case
|
||||
q Exit test panel (restore layout)
|
||||
|
||||
Execution Details ~
|
||||
|
||||
Test cases are executed individually using the same compilation and
|
||||
execution pipeline, but with isolated input/output for
|
||||
precise failure analysis. All tests are automatically run when the
|
||||
panel opens.
|
||||
|
||||
FILE STRUCTURE *cp-files*
|
||||
|
||||
cp.nvim creates the following file structure upon problem setup:
|
||||
|
||||
{problem_id}.{ext} " Source file (e.g. a.cc, b.py)
|
||||
build/
|
||||
{problem_id}.run " Compiled binary
|
||||
io/
|
||||
{problem_id}.cpin " Test input
|
||||
{problem_id}.cpout " Program output
|
||||
{problem_id}.expected " Expected output
|
||||
|
||||
The plugin automatically manages this structure and navigation between problems
|
||||
maintains proper file associations.
|
||||
|
||||
SNIPPETS *cp-snippets*
|
||||
|
||||
cp.nvim integrates with LuaSnip for automatic template expansion. Built-in
|
||||
snippets include basic C++ and Python templates for each contest type.
|
||||
|
||||
Snippet trigger names must EXACTLY match platform names ("codeforces" for
|
||||
CodeForces, "cses" for CSES, etc.).
|
||||
|
||||
Custom snippets can be added via the `snippets` configuration field.
|
||||
|
||||
HEALTH CHECK *cp-health*
|
||||
|
||||
Run |:checkhealth| cp to verify your setup.
|
||||
|
||||
vim:tw=78:ts=8:ft=help:norl:
|
||||
43
flake.lock
generated
Normal file
43
flake.lock
generated
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1771008912,
|
||||
"narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a82ccc39b39b621151d6732718e3e250109076fa",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs",
|
||||
"systems": "systems"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1689347949,
|
||||
"narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default-linux",
|
||||
"rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default-linux",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
72
flake.nix
Normal file
72
flake.nix
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
systems.url = "github:nix-systems/default-linux";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
systems,
|
||||
}:
|
||||
let
|
||||
eachSystem = nixpkgs.lib.genAttrs (import systems);
|
||||
pkgsFor = system: nixpkgs.legacyPackages.${system};
|
||||
|
||||
mkPythonEnv =
|
||||
pkgs:
|
||||
pkgs.python312.withPackages (ps: [
|
||||
ps.backoff
|
||||
ps.beautifulsoup4
|
||||
ps.curl-cffi
|
||||
ps.httpx
|
||||
ps.ndjson
|
||||
ps.pydantic
|
||||
ps.requests
|
||||
]);
|
||||
|
||||
mkPlugin =
|
||||
pkgs:
|
||||
let
|
||||
pythonEnv = mkPythonEnv pkgs;
|
||||
in
|
||||
pkgs.vimUtils.buildVimPlugin {
|
||||
pname = "cp-nvim";
|
||||
version = "0-unstable-${self.shortRev or self.dirtyShortRev or "dev"}";
|
||||
src = self;
|
||||
postPatch = ''
|
||||
substituteInPlace lua/cp/utils.lua \
|
||||
--replace-fail "local _nix_python = nil" \
|
||||
"local _nix_python = '${pythonEnv.interpreter}'"
|
||||
'';
|
||||
nvimSkipModule = [
|
||||
"cp.pickers.telescope"
|
||||
"cp.version"
|
||||
];
|
||||
passthru = { inherit pythonEnv; };
|
||||
meta.description = "Competitive programming plugin for Neovim";
|
||||
};
|
||||
in
|
||||
{
|
||||
overlays.default = final: prev: {
|
||||
vimPlugins = prev.vimPlugins // {
|
||||
cp-nvim = mkPlugin final;
|
||||
};
|
||||
};
|
||||
|
||||
packages = eachSystem (system: {
|
||||
default = mkPlugin (pkgsFor system);
|
||||
pythonEnv = mkPythonEnv (pkgsFor system);
|
||||
});
|
||||
|
||||
devShells = eachSystem (system: {
|
||||
default = (pkgsFor system).mkShell {
|
||||
packages = with (pkgsFor system); [
|
||||
uv
|
||||
python312
|
||||
];
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
vim.filetype.add({
|
||||
extension = {
|
||||
cpin = 'cpin',
|
||||
cpout = 'cpout',
|
||||
},
|
||||
})
|
||||
318
lua/cp/cache.lua
318
lua/cp/cache.lua
|
|
@ -1,71 +1,65 @@
|
|||
---@class CacheData
|
||||
---@field [string] table<string, ContestData>
|
||||
---@class FileState
|
||||
---@field platform string
|
||||
---@field contest_id string
|
||||
---@field problem_id? string
|
||||
---@field language? string
|
||||
|
||||
---@class ContestData
|
||||
---@field problems Problem[]
|
||||
---@field scraped_at string
|
||||
---@field expires_at? number
|
||||
---@field test_cases? CachedTestCase[]
|
||||
---@field test_cases_cached_at? number
|
||||
---@field index_map table<string, number>
|
||||
---@field name string
|
||||
---@field display_name string
|
||||
---@field url string
|
||||
|
||||
---@class ContestSummary
|
||||
---@field display_name string
|
||||
---@field name string
|
||||
---@field id string
|
||||
|
||||
---@class CombinedTest
|
||||
---@field input string
|
||||
---@field expected string
|
||||
|
||||
---@class Problem
|
||||
---@field id string
|
||||
---@field name? string
|
||||
---@field interactive? boolean
|
||||
---@field multi_test? boolean
|
||||
---@field memory_mb? number
|
||||
---@field timeout_ms? number
|
||||
---@field combined_test? CombinedTest
|
||||
---@field test_cases TestCase[]
|
||||
|
||||
---@class CachedTestCase
|
||||
---@class TestCase
|
||||
---@field index? number
|
||||
---@field input string
|
||||
---@field expected? string
|
||||
---@field input? string
|
||||
---@field output? string
|
||||
|
||||
local M = {}
|
||||
|
||||
local logger = require('cp.log')
|
||||
local cache_file = vim.fn.stdpath('data') .. '/cp-nvim.json'
|
||||
local cache_data = {}
|
||||
local loaded = false
|
||||
|
||||
---@param platform string
|
||||
---@return number?
|
||||
local function get_expiry_date(platform)
|
||||
vim.validate({
|
||||
platform = { platform, 'string' },
|
||||
})
|
||||
|
||||
if platform == 'cses' then
|
||||
return os.time() + (30 * 24 * 60 * 60)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---@param contest_data ContestData
|
||||
---@param platform string
|
||||
---@return boolean
|
||||
local function is_cache_valid(contest_data, platform)
|
||||
vim.validate({
|
||||
contest_data = { contest_data, 'table' },
|
||||
platform = { platform, 'string' },
|
||||
})
|
||||
|
||||
if platform ~= 'cses' then
|
||||
return true
|
||||
end
|
||||
|
||||
local expires_at = contest_data.expires_at
|
||||
if not expires_at then
|
||||
return false
|
||||
end
|
||||
|
||||
return os.time() < expires_at
|
||||
end
|
||||
|
||||
--- Load the cache from disk if not done already
|
||||
---@return nil
|
||||
function M.load()
|
||||
if loaded then
|
||||
return
|
||||
end
|
||||
|
||||
if vim.fn.filereadable(cache_file) == 0 then
|
||||
cache_data = {}
|
||||
vim.fn.writefile({}, cache_file)
|
||||
loaded = true
|
||||
return
|
||||
end
|
||||
|
||||
local content = vim.fn.readfile(cache_file)
|
||||
if #content == 0 then
|
||||
cache_data = {}
|
||||
loaded = true
|
||||
return
|
||||
end
|
||||
|
||||
|
|
@ -73,61 +67,84 @@ function M.load()
|
|||
if ok then
|
||||
cache_data = decoded
|
||||
else
|
||||
cache_data = {}
|
||||
logger.log('Could not decode json in cache file', vim.log.levels.ERROR)
|
||||
end
|
||||
loaded = true
|
||||
end
|
||||
|
||||
--- Save the cache to disk, overwriting existing contents
|
||||
---@return nil
|
||||
function M.save()
|
||||
vim.fn.mkdir(vim.fn.fnamemodify(cache_file, ':h'), 'p')
|
||||
local encoded = vim.json.encode(cache_data)
|
||||
vim.fn.writefile(vim.split(encoded, '\n'), cache_file)
|
||||
vim.schedule(function()
|
||||
vim.fn.mkdir(vim.fn.fnamemodify(cache_file, ':h'), 'p')
|
||||
|
||||
local encoded = vim.json.encode(cache_data)
|
||||
local lines = vim.split(encoded, '\n')
|
||||
vim.fn.writefile(lines, cache_file)
|
||||
end)
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@return ContestData?
|
||||
---@return ContestData
|
||||
function M.get_contest_data(platform, contest_id)
|
||||
vim.validate({
|
||||
platform = { platform, 'string' },
|
||||
contest_id = { contest_id, 'string' },
|
||||
})
|
||||
|
||||
cache_data[platform] = cache_data[platform] or {}
|
||||
cache_data[platform][contest_id] = cache_data[platform][contest_id] or {}
|
||||
return cache_data[platform][contest_id]
|
||||
end
|
||||
|
||||
---Get all cached contest IDs for a platform
|
||||
---@param platform string
|
||||
---@return string[]
|
||||
function M.get_cached_contest_ids(platform)
|
||||
vim.validate({
|
||||
platform = { platform, 'string' },
|
||||
})
|
||||
|
||||
if not cache_data[platform] then
|
||||
return nil
|
||||
return {}
|
||||
end
|
||||
|
||||
local contest_data = cache_data[platform][contest_id]
|
||||
if not contest_data then
|
||||
return nil
|
||||
local contest_ids = {}
|
||||
for contest_id, _ in pairs(cache_data[platform]) do
|
||||
table.insert(contest_ids, contest_id)
|
||||
end
|
||||
|
||||
if not is_cache_valid(contest_data, platform) then
|
||||
return nil
|
||||
end
|
||||
|
||||
return contest_data
|
||||
table.sort(contest_ids)
|
||||
return contest_ids
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@param problems Problem[]
|
||||
function M.set_contest_data(platform, contest_id, problems)
|
||||
---@param url string
|
||||
function M.set_contest_data(platform, contest_id, problems, url)
|
||||
vim.validate({
|
||||
platform = { platform, 'string' },
|
||||
contest_id = { contest_id, 'string' },
|
||||
problems = { problems, 'table' },
|
||||
url = { url, 'string' },
|
||||
})
|
||||
|
||||
if not cache_data[platform] then
|
||||
cache_data[platform] = {}
|
||||
cache_data[platform] = cache_data[platform] or {}
|
||||
local prev = cache_data[platform][contest_id] or {}
|
||||
|
||||
local out = {
|
||||
name = prev.name,
|
||||
display_name = prev.display_name,
|
||||
problems = problems,
|
||||
index_map = {},
|
||||
url = url,
|
||||
}
|
||||
for i, p in ipairs(out.problems) do
|
||||
out.index_map[p.id] = i
|
||||
end
|
||||
|
||||
cache_data[platform][contest_id] = {
|
||||
problems = problems,
|
||||
scraped_at = os.date('%Y-%m-%d'),
|
||||
expires_at = get_expiry_date(platform),
|
||||
}
|
||||
|
||||
cache_data[platform][contest_id] = out
|
||||
M.save()
|
||||
end
|
||||
|
||||
|
|
@ -148,7 +165,7 @@ end
|
|||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@param problem_id? string
|
||||
---@return CachedTestCase[]?
|
||||
---@return TestCase[]
|
||||
function M.get_test_cases(platform, contest_id, problem_id)
|
||||
vim.validate({
|
||||
platform = { platform, 'string' },
|
||||
|
|
@ -156,36 +173,171 @@ function M.get_test_cases(platform, contest_id, problem_id)
|
|||
problem_id = { problem_id, { 'string', 'nil' }, true },
|
||||
})
|
||||
|
||||
local problem_key = problem_id and (contest_id .. '_' .. problem_id) or contest_id
|
||||
if not cache_data[platform] or not cache_data[platform][problem_key] then
|
||||
return nil
|
||||
if
|
||||
not cache_data[platform]
|
||||
or not cache_data[platform][contest_id]
|
||||
or not cache_data[platform][contest_id].problems
|
||||
or not cache_data[platform][contest_id].index_map
|
||||
then
|
||||
return {}
|
||||
end
|
||||
return cache_data[platform][problem_key].test_cases
|
||||
|
||||
local index = cache_data[platform][contest_id].index_map[problem_id]
|
||||
return cache_data[platform][contest_id].problems[index].test_cases or {}
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@param problem_id? string
|
||||
---@param test_cases CachedTestCase[]
|
||||
function M.set_test_cases(platform, contest_id, problem_id, test_cases)
|
||||
---@return CombinedTest?
|
||||
function M.get_combined_test(platform, contest_id, problem_id)
|
||||
if
|
||||
not cache_data[platform]
|
||||
or not cache_data[platform][contest_id]
|
||||
or not cache_data[platform][contest_id].problems
|
||||
or not cache_data[platform][contest_id].index_map
|
||||
then
|
||||
return nil
|
||||
end
|
||||
|
||||
local index = cache_data[platform][contest_id].index_map[problem_id]
|
||||
return cache_data[platform][contest_id].problems[index].combined_test
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@param problem_id string
|
||||
---@param combined_test? CombinedTest
|
||||
---@param test_cases TestCase[]
|
||||
---@param timeout_ms number
|
||||
---@param memory_mb number
|
||||
---@param interactive boolean
|
||||
---@param multi_test boolean
|
||||
function M.set_test_cases(
|
||||
platform,
|
||||
contest_id,
|
||||
problem_id,
|
||||
combined_test,
|
||||
test_cases,
|
||||
timeout_ms,
|
||||
memory_mb,
|
||||
interactive,
|
||||
multi_test
|
||||
)
|
||||
vim.validate({
|
||||
platform = { platform, 'string' },
|
||||
contest_id = { contest_id, 'string' },
|
||||
problem_id = { problem_id, { 'string', 'nil' }, true },
|
||||
combined_test = { combined_test, { 'table', 'nil' }, true },
|
||||
test_cases = { test_cases, 'table' },
|
||||
timeout_ms = { timeout_ms, { 'number', 'nil' }, true },
|
||||
memory_mb = { memory_mb, { 'number', 'nil' }, true },
|
||||
interactive = { interactive, { 'boolean', 'nil' }, true },
|
||||
multi_test = { multi_test, { 'boolean', 'nil' }, true },
|
||||
})
|
||||
|
||||
local problem_key = problem_id and (contest_id .. '_' .. problem_id) or contest_id
|
||||
if not cache_data[platform] then
|
||||
cache_data[platform] = {}
|
||||
end
|
||||
if not cache_data[platform][problem_key] then
|
||||
cache_data[platform][problem_key] = {}
|
||||
end
|
||||
local index = cache_data[platform][contest_id].index_map[problem_id]
|
||||
|
||||
cache_data[platform][contest_id].problems[index].combined_test = combined_test
|
||||
cache_data[platform][contest_id].problems[index].test_cases = test_cases
|
||||
cache_data[platform][contest_id].problems[index].timeout_ms = timeout_ms
|
||||
cache_data[platform][contest_id].problems[index].memory_mb = memory_mb
|
||||
cache_data[platform][contest_id].problems[index].interactive = interactive
|
||||
cache_data[platform][contest_id].problems[index].multi_test = multi_test
|
||||
|
||||
cache_data[platform][problem_key].test_cases = test_cases
|
||||
cache_data[platform][problem_key].test_cases_cached_at = os.time()
|
||||
M.save()
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@param problem_id? string
|
||||
---@return number?, number?
|
||||
function M.get_constraints(platform, contest_id, problem_id)
|
||||
vim.validate({
|
||||
platform = { platform, 'string' },
|
||||
contest_id = { contest_id, 'string' },
|
||||
problem_id = { problem_id, { 'string', 'nil' }, true },
|
||||
})
|
||||
|
||||
local index = cache_data[platform][contest_id].index_map[problem_id]
|
||||
|
||||
local problem_data = cache_data[platform][contest_id].problems[index]
|
||||
return problem_data.timeout_ms, problem_data.memory_mb
|
||||
end
|
||||
|
||||
---@param file_path string
|
||||
---@return FileState|nil
|
||||
function M.get_file_state(file_path)
|
||||
M.load()
|
||||
cache_data.file_states = cache_data.file_states or {}
|
||||
return cache_data.file_states[file_path]
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@param problem_id string
|
||||
---@param language string|nil
|
||||
function M.set_file_state(path, platform, contest_id, problem_id, language)
|
||||
M.load()
|
||||
cache_data.file_states = cache_data.file_states or {}
|
||||
cache_data.file_states[path] = {
|
||||
platform = platform,
|
||||
contest_id = contest_id,
|
||||
problem_id = problem_id,
|
||||
language = language,
|
||||
}
|
||||
M.save()
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@return ContestSummary[]
|
||||
function M.get_contest_summaries(platform)
|
||||
local contest_list = {}
|
||||
for contest_id, contest_data in pairs(cache_data[platform] or {}) do
|
||||
table.insert(contest_list, {
|
||||
id = contest_id,
|
||||
name = contest_data.name,
|
||||
display_name = contest_data.display_name,
|
||||
})
|
||||
end
|
||||
return contest_list
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@param contests ContestSummary[]
|
||||
function M.set_contest_summaries(platform, contests)
|
||||
cache_data[platform] = cache_data[platform] or {}
|
||||
for _, contest in ipairs(contests) do
|
||||
cache_data[platform][contest.id] = cache_data[platform][contest.id] or {}
|
||||
cache_data[platform][contest.id].display_name = contest.display_name
|
||||
cache_data[platform][contest.id].name = contest.name
|
||||
end
|
||||
|
||||
M.save()
|
||||
end
|
||||
|
||||
function M.clear_all()
|
||||
cache_data = {}
|
||||
M.save()
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
function M.clear_platform(platform)
|
||||
if cache_data[platform] then
|
||||
cache_data[platform] = nil
|
||||
end
|
||||
|
||||
M.save()
|
||||
end
|
||||
|
||||
---@return string
|
||||
function M.get_data_pretty()
|
||||
M.load()
|
||||
|
||||
return vim.inspect(cache_data)
|
||||
end
|
||||
|
||||
M._cache = cache_data
|
||||
|
||||
return M
|
||||
|
|
|
|||
74
lua/cp/commands/cache.lua
Normal file
74
lua/cp/commands/cache.lua
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
local M = {}
|
||||
|
||||
local cache = require('cp.cache')
|
||||
local constants = require('cp.constants')
|
||||
local logger = require('cp.log')
|
||||
|
||||
local platforms = constants.PLATFORMS
|
||||
|
||||
--- Dispatch any `:CP cache ...` command
|
||||
---@param cmd table
|
||||
---@return nil
|
||||
function M.handle_cache_command(cmd)
|
||||
if cmd.subcommand == 'read' then
|
||||
local data = cache.get_data_pretty()
|
||||
local name = 'cp.nvim://cache.lua'
|
||||
|
||||
local existing = vim.fn.bufnr(name)
|
||||
local buf
|
||||
if existing ~= -1 then
|
||||
buf = existing
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, vim.split(data, '\n'))
|
||||
else
|
||||
buf = vim.api.nvim_create_buf(true, true)
|
||||
vim.api.nvim_buf_set_name(buf, name)
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, vim.split(data, '\n'))
|
||||
vim.bo[buf].filetype = 'lua'
|
||||
vim.bo[buf].buftype = 'nofile'
|
||||
vim.bo[buf].bufhidden = 'wipe'
|
||||
vim.bo[buf].swapfile = false
|
||||
vim.api.nvim_buf_set_keymap(
|
||||
buf,
|
||||
'n',
|
||||
'q',
|
||||
'<cmd>bd!<cr>',
|
||||
{ nowait = true, noremap = true, silent = true }
|
||||
)
|
||||
end
|
||||
|
||||
vim.api.nvim_set_current_buf(buf)
|
||||
elseif cmd.subcommand == 'clear' then
|
||||
cache.load()
|
||||
if cmd.platform and cmd.contest then
|
||||
if vim.tbl_contains(platforms, cmd.platform) then
|
||||
cache.clear_contest_data(cmd.platform, cmd.contest)
|
||||
logger.log(
|
||||
("Cache cleared for %s contest '%s'"):format(
|
||||
constants.PLATFORM_DISPLAY_NAMES[cmd.platform],
|
||||
cmd.contest
|
||||
),
|
||||
vim.log.levels.INFO,
|
||||
true
|
||||
)
|
||||
else
|
||||
logger.log(("Unknown platform '%s'."):format(cmd.platform), vim.log.levels.ERROR)
|
||||
end
|
||||
elseif cmd.platform then
|
||||
if vim.tbl_contains(platforms, cmd.platform) then
|
||||
cache.clear_platform(cmd.platform)
|
||||
logger.log(
|
||||
("Cache cleared for platform '%s'"):format(constants.PLATFORM_DISPLAY_NAMES[cmd.platform]),
|
||||
vim.log.levels.INFO,
|
||||
true
|
||||
)
|
||||
else
|
||||
logger.log(("Unknown platform '%s'."):format(cmd.platform), vim.log.levels.ERROR)
|
||||
end
|
||||
else
|
||||
cache.clear_all()
|
||||
logger.log('Cache cleared', vim.log.levels.INFO, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
327
lua/cp/commands/init.lua
Normal file
327
lua/cp/commands/init.lua
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
local M = {}
|
||||
|
||||
local constants = require('cp.constants')
|
||||
local logger = require('cp.log')
|
||||
local state = require('cp.state')
|
||||
|
||||
local platforms = constants.PLATFORMS
|
||||
local actions = constants.ACTIONS
|
||||
|
||||
---@class ParsedCommand
|
||||
---@field type string
|
||||
---@field error string?
|
||||
---@field action? string
|
||||
---@field message? string
|
||||
---@field contest? string
|
||||
---@field platform? string
|
||||
---@field problem_id? string
|
||||
---@field interactor_cmd? string
|
||||
---@field test_index? integer
|
||||
---@field test_indices? integer[]
|
||||
---@field mode? string
|
||||
---@field debug? boolean
|
||||
---@field language? string
|
||||
---@field subcommand? string
|
||||
|
||||
--- Turn raw args into normalized structure to later dispatch
|
||||
---@param args string[] The raw command-line mode args
|
||||
---@return ParsedCommand
|
||||
local function parse_command(args)
|
||||
if vim.tbl_isempty(args) then
|
||||
return {
|
||||
type = 'restore_from_file',
|
||||
}
|
||||
end
|
||||
|
||||
local first = args[1]
|
||||
|
||||
if vim.tbl_contains(actions, first) then
|
||||
if first == 'cache' then
|
||||
local subcommand = args[2]
|
||||
if not subcommand then
|
||||
return { type = 'error', message = 'cache command requires subcommand' }
|
||||
end
|
||||
if vim.tbl_contains({ 'clear', 'read' }, subcommand) then
|
||||
local platform = args[3]
|
||||
local contest = args[4]
|
||||
return {
|
||||
type = 'cache',
|
||||
subcommand = subcommand,
|
||||
platform = platform,
|
||||
contest = contest,
|
||||
}
|
||||
else
|
||||
return { type = 'error', message = 'unknown cache subcommand: ' .. subcommand }
|
||||
end
|
||||
elseif first == 'interact' then
|
||||
local inter = args[2]
|
||||
if inter and inter ~= '' then
|
||||
return { type = 'action', action = 'interact', interactor_cmd = inter }
|
||||
else
|
||||
return { type = 'action', action = 'interact' }
|
||||
end
|
||||
elseif first == 'edit' then
|
||||
local test_index = nil
|
||||
if #args >= 2 then
|
||||
local idx = tonumber(args[2])
|
||||
if not idx then
|
||||
return {
|
||||
type = 'error',
|
||||
message = ("Invalid argument '%s': expected test number"):format(args[2]),
|
||||
}
|
||||
end
|
||||
if idx < 1 or idx ~= math.floor(idx) then
|
||||
return { type = 'error', message = ("'%s' is not a valid test index"):format(idx) }
|
||||
end
|
||||
test_index = idx
|
||||
end
|
||||
return { type = 'action', action = 'edit', test_index = test_index }
|
||||
elseif first == 'run' or first == 'panel' then
|
||||
local debug = false
|
||||
local test_indices = nil
|
||||
local mode = 'combined'
|
||||
|
||||
if #args == 2 then
|
||||
if args[2] == '--debug' then
|
||||
debug = true
|
||||
elseif args[2] == 'all' then
|
||||
mode = 'individual'
|
||||
else
|
||||
if args[2]:find(',') then
|
||||
local indices = {}
|
||||
for num in args[2]:gmatch('[^,]+') do
|
||||
local idx = tonumber(num)
|
||||
if not idx or idx < 1 or idx ~= math.floor(idx) then
|
||||
return {
|
||||
type = 'error',
|
||||
message = ("Invalid test index '%s' in list"):format(num),
|
||||
}
|
||||
end
|
||||
table.insert(indices, idx)
|
||||
end
|
||||
if #indices == 0 then
|
||||
return { type = 'error', message = 'No valid test indices provided' }
|
||||
end
|
||||
test_indices = indices
|
||||
mode = 'individual'
|
||||
else
|
||||
local idx = tonumber(args[2])
|
||||
if not idx then
|
||||
return {
|
||||
type = 'error',
|
||||
message = ("Invalid argument '%s': expected test number(s), 'all', or --debug"):format(
|
||||
args[2]
|
||||
),
|
||||
}
|
||||
end
|
||||
if idx < 1 or idx ~= math.floor(idx) then
|
||||
return { type = 'error', message = ("'%s' is not a valid test index"):format(idx) }
|
||||
end
|
||||
test_indices = { idx }
|
||||
mode = 'individual'
|
||||
end
|
||||
end
|
||||
elseif #args == 3 then
|
||||
if args[2] == 'all' then
|
||||
mode = 'individual'
|
||||
if args[3] ~= '--debug' then
|
||||
return {
|
||||
type = 'error',
|
||||
message = ("Invalid argument '%s': expected --debug"):format(args[3]),
|
||||
}
|
||||
end
|
||||
debug = true
|
||||
elseif args[2]:find(',') then
|
||||
local indices = {}
|
||||
for num in args[2]:gmatch('[^,]+') do
|
||||
local idx = tonumber(num)
|
||||
if not idx or idx < 1 or idx ~= math.floor(idx) then
|
||||
return {
|
||||
type = 'error',
|
||||
message = ("Invalid test index '%s' in list"):format(num),
|
||||
}
|
||||
end
|
||||
table.insert(indices, idx)
|
||||
end
|
||||
if #indices == 0 then
|
||||
return { type = 'error', message = 'No valid test indices provided' }
|
||||
end
|
||||
if args[3] ~= '--debug' then
|
||||
return {
|
||||
type = 'error',
|
||||
message = ("Invalid argument '%s': expected --debug"):format(args[3]),
|
||||
}
|
||||
end
|
||||
test_indices = indices
|
||||
mode = 'individual'
|
||||
debug = true
|
||||
else
|
||||
local idx = tonumber(args[2])
|
||||
if not idx then
|
||||
return {
|
||||
type = 'error',
|
||||
message = ("Invalid argument '%s': expected test number"):format(args[2]),
|
||||
}
|
||||
end
|
||||
if idx < 1 or idx ~= math.floor(idx) then
|
||||
return { type = 'error', message = ("'%s' is not a valid test index"):format(idx) }
|
||||
end
|
||||
if args[3] ~= '--debug' then
|
||||
return {
|
||||
type = 'error',
|
||||
message = ("Invalid argument '%s': expected --debug"):format(args[3]),
|
||||
}
|
||||
end
|
||||
test_indices = { idx }
|
||||
mode = 'individual'
|
||||
debug = true
|
||||
end
|
||||
elseif #args > 3 then
|
||||
return {
|
||||
type = 'error',
|
||||
message = 'Too many arguments. Usage: :CP '
|
||||
.. first
|
||||
.. ' [all|test_num[,test_num...]] [--debug]',
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
type = 'action',
|
||||
action = first,
|
||||
test_indices = test_indices,
|
||||
debug = debug,
|
||||
mode = mode,
|
||||
}
|
||||
else
|
||||
local language = nil
|
||||
if #args >= 3 and args[2] == '--lang' then
|
||||
language = args[3]
|
||||
elseif #args >= 2 and args[2] ~= nil and args[2]:sub(1, 2) ~= '--' then
|
||||
return {
|
||||
type = 'error',
|
||||
message = ("Unknown argument '%s' for action '%s'"):format(args[2], first),
|
||||
}
|
||||
end
|
||||
return { type = 'action', action = first, language = language }
|
||||
end
|
||||
end
|
||||
|
||||
if vim.tbl_contains(platforms, first) then
|
||||
if #args == 1 then
|
||||
return {
|
||||
type = 'error',
|
||||
message = 'Too few arguments - specify a contest.',
|
||||
}
|
||||
elseif #args == 2 then
|
||||
return {
|
||||
type = 'contest_setup',
|
||||
platform = first,
|
||||
contest = args[2],
|
||||
}
|
||||
elseif #args == 4 and args[3] == '--lang' then
|
||||
return {
|
||||
type = 'contest_setup',
|
||||
platform = first,
|
||||
contest = args[2],
|
||||
language = args[4],
|
||||
}
|
||||
else
|
||||
return {
|
||||
type = 'error',
|
||||
message = 'Invalid arguments. Usage: :CP <platform> <contest> [--lang <language>]',
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
if #args == 1 then
|
||||
return {
|
||||
type = 'problem_jump',
|
||||
problem_id = first,
|
||||
}
|
||||
elseif #args == 3 and args[2] == '--lang' then
|
||||
return {
|
||||
type = 'problem_jump',
|
||||
problem_id = first,
|
||||
language = args[3],
|
||||
}
|
||||
end
|
||||
|
||||
return { type = 'error', message = 'Unknown command or no contest context.' }
|
||||
end
|
||||
|
||||
--- Core logic for handling `:CP ...` commands
|
||||
---@return nil
|
||||
function M.handle_command(opts)
|
||||
local cmd = parse_command(opts.fargs)
|
||||
|
||||
if cmd.type == 'error' then
|
||||
logger.log(cmd.message, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
if cmd.type == 'restore_from_file' then
|
||||
local restore = require('cp.restore')
|
||||
restore.restore_from_current_file()
|
||||
elseif cmd.type == 'action' then
|
||||
local setup = require('cp.setup')
|
||||
local ui = require('cp.ui.views')
|
||||
|
||||
if cmd.action == 'interact' then
|
||||
ui.toggle_interactive(cmd.interactor_cmd)
|
||||
elseif cmd.action == 'run' then
|
||||
ui.run_io_view(cmd.test_indices, cmd.debug, cmd.mode)
|
||||
elseif cmd.action == 'panel' then
|
||||
ui.toggle_panel({
|
||||
debug = cmd.debug,
|
||||
test_index = cmd.test_indices and cmd.test_indices[1] or nil,
|
||||
})
|
||||
elseif cmd.action == 'next' then
|
||||
setup.navigate_problem(1, cmd.language)
|
||||
elseif cmd.action == 'prev' then
|
||||
setup.navigate_problem(-1, cmd.language)
|
||||
elseif cmd.action == 'pick' then
|
||||
local picker = require('cp.commands.picker')
|
||||
picker.handle_pick_action(cmd.language)
|
||||
elseif cmd.action == 'edit' then
|
||||
local edit = require('cp.ui.edit')
|
||||
edit.toggle_edit(cmd.test_index)
|
||||
end
|
||||
elseif cmd.type == 'problem_jump' then
|
||||
local platform = state.get_platform()
|
||||
local contest_id = state.get_contest_id()
|
||||
local problem_id = cmd.problem_id
|
||||
|
||||
if not (platform and contest_id) then
|
||||
logger.log('No contest is currently active.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local cache = require('cp.cache')
|
||||
cache.load()
|
||||
local contest_data = cache.get_contest_data(platform, contest_id)
|
||||
|
||||
if not (contest_data and contest_data.index_map and contest_data.index_map[problem_id]) then
|
||||
logger.log(
|
||||
("%s contest '%s' has no problem '%s'."):format(
|
||||
constants.PLATFORM_DISPLAY_NAMES[platform],
|
||||
contest_id,
|
||||
problem_id
|
||||
),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
local setup = require('cp.setup')
|
||||
setup.setup_contest(platform, contest_id, problem_id, cmd.language)
|
||||
elseif cmd.type == 'cache' then
|
||||
local cache_commands = require('cp.commands.cache')
|
||||
cache_commands.handle_cache_command(cmd)
|
||||
elseif cmd.type == 'contest_setup' then
|
||||
local setup = require('cp.setup')
|
||||
setup.setup_contest(cmd.platform, cmd.contest, nil, cmd.language)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
60
lua/cp/commands/picker.lua
Normal file
60
lua/cp/commands/picker.lua
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
local M = {}
|
||||
|
||||
local config_module = require('cp.config')
|
||||
local logger = require('cp.log')
|
||||
|
||||
--- Dispatch `:CP pick` to appropriate picker
|
||||
---@param language? string
|
||||
---@return nil
|
||||
function M.handle_pick_action(language)
|
||||
local config = config_module.get_config()
|
||||
|
||||
if not (config.ui and config.ui.picker) then
|
||||
logger.log(
|
||||
'No picker configured. Set ui.picker = "{telescope,fzf-lua}" in your config.',
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
local picker
|
||||
|
||||
local picker_name = config.ui.picker
|
||||
if picker_name == 'telescope' then
|
||||
local ok = pcall(require, 'telescope')
|
||||
if not ok then
|
||||
logger.log(
|
||||
'telescope.nvim is not available. Install telescope.nvim xor change your picker config.',
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
local ok_cp, telescope_picker = pcall(require, 'cp.pickers.telescope')
|
||||
if not ok_cp then
|
||||
logger.log('Failed to load telescope integration.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
picker = telescope_picker
|
||||
elseif picker_name == 'fzf-lua' then
|
||||
local ok, _ = pcall(require, 'fzf-lua')
|
||||
if not ok then
|
||||
logger.log(
|
||||
'fzf-lua is not available. Install fzf-lua or change your picker config',
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
local ok_cp, fzf_picker = pcall(require, 'cp.pickers.fzf_lua')
|
||||
if not ok_cp then
|
||||
logger.log('Failed to load fzf-lua integration.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
picker = fzf_picker
|
||||
end
|
||||
|
||||
picker.pick(language)
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -1,169 +1,532 @@
|
|||
---@class LanguageConfig
|
||||
---@field compile? string[] Compile command template
|
||||
---@field run string[] Run command template
|
||||
---@field debug? string[] Debug command template
|
||||
---@field executable? string Executable name
|
||||
---@field version? number Language version
|
||||
---@field extension string File extension
|
||||
-- lua/cp/config.lua
|
||||
---@class CpLangCommands
|
||||
---@field build? string[]
|
||||
---@field run? string[]
|
||||
---@field debug? string[]
|
||||
|
||||
---@class PartialLanguageConfig
|
||||
---@field compile? string[] Compile command template
|
||||
---@field run? string[] Run command template
|
||||
---@field debug? string[] Debug command template
|
||||
---@field executable? string Executable name
|
||||
---@field version? number Language version
|
||||
---@field extension? string File extension
|
||||
---@class CpLanguage
|
||||
---@field extension string
|
||||
---@field commands CpLangCommands
|
||||
|
||||
---@class ContestConfig
|
||||
---@field cpp LanguageConfig
|
||||
---@field python LanguageConfig
|
||||
---@class CpPlatformOverrides
|
||||
---@field extension? string
|
||||
---@field commands? CpLangCommands
|
||||
|
||||
---@class CpPlatform
|
||||
---@field enabled_languages string[]
|
||||
---@field default_language string
|
||||
---@field timeout_ms number
|
||||
---@field overrides? table<string, CpPlatformOverrides>
|
||||
|
||||
---@class PartialContestConfig
|
||||
---@field cpp? PartialLanguageConfig
|
||||
---@field python? PartialLanguageConfig
|
||||
---@field default_language? string
|
||||
---@field timeout_ms? number
|
||||
---@class PanelConfig
|
||||
---@field diff_modes string[]
|
||||
---@field max_output_lines integer
|
||||
|
||||
---@class DiffGitConfig
|
||||
---@field args string[]
|
||||
|
||||
---@class DiffConfig
|
||||
---@field git DiffGitConfig
|
||||
|
||||
---@class Hooks
|
||||
---@field before_run? fun(ctx: ProblemContext)
|
||||
---@field before_debug? fun(ctx: ProblemContext)
|
||||
---@field setup_code? fun(ctx: ProblemContext)
|
||||
---@field before_run? fun(state: cp.State)
|
||||
---@field before_debug? fun(state: cp.State)
|
||||
---@field setup_code? fun(state: cp.State)
|
||||
---@field setup_io_input? fun(bufnr: integer, state: cp.State)
|
||||
---@field setup_io_output? fun(bufnr: integer, state: cp.State)
|
||||
|
||||
---@class VerdictFormatData
|
||||
---@field index integer
|
||||
---@field status { text: string, highlight_group: string }
|
||||
---@field time_ms number
|
||||
---@field time_limit_ms number
|
||||
---@field memory_mb number
|
||||
---@field memory_limit_mb number
|
||||
---@field exit_code integer
|
||||
---@field signal string|nil
|
||||
---@field time_actual_width? integer
|
||||
---@field time_limit_width? integer
|
||||
---@field mem_actual_width? integer
|
||||
---@field mem_limit_width? integer
|
||||
|
||||
---@class VerdictHighlight
|
||||
---@field col_start integer
|
||||
---@field col_end integer
|
||||
---@field group string
|
||||
|
||||
---@class VerdictFormatResult
|
||||
---@field line string
|
||||
---@field highlights? VerdictHighlight[]
|
||||
|
||||
---@alias VerdictFormatter fun(data: VerdictFormatData): VerdictFormatResult
|
||||
|
||||
---@class RunConfig
|
||||
---@field width number
|
||||
---@field next_test_key string|nil
|
||||
---@field prev_test_key string|nil
|
||||
---@field format_verdict VerdictFormatter
|
||||
|
||||
---@class EditConfig
|
||||
---@field next_test_key string|nil
|
||||
---@field prev_test_key string|nil
|
||||
---@field delete_test_key string|nil
|
||||
---@field add_test_key string|nil
|
||||
---@field save_and_exit_key string|nil
|
||||
|
||||
---@class CpUI
|
||||
---@field ansi boolean
|
||||
---@field run RunConfig
|
||||
---@field edit EditConfig
|
||||
---@field panel PanelConfig
|
||||
---@field diff DiffConfig
|
||||
---@field picker string|nil
|
||||
|
||||
---@class cp.Config
|
||||
---@field contests table<string, ContestConfig>
|
||||
---@field snippets table[]
|
||||
---@field languages table<string, CpLanguage>
|
||||
---@field platforms table<string, CpPlatform>
|
||||
---@field hooks Hooks
|
||||
---@field debug boolean
|
||||
---@field scrapers table<string, boolean>
|
||||
---@field open_url boolean
|
||||
---@field scrapers string[]
|
||||
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
|
||||
---@field ui CpUI
|
||||
---@field runtime { effective: table<string, table<string, CpLanguage>> } -- computed
|
||||
|
||||
---@class cp.UserConfig
|
||||
---@field contests? table<string, PartialContestConfig>
|
||||
---@field snippets? table[]
|
||||
---@field hooks? Hooks
|
||||
---@field debug? boolean
|
||||
---@field scrapers? table<string, boolean>
|
||||
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
|
||||
---@class cp.PartialConfig: cp.Config
|
||||
|
||||
local M = {}
|
||||
local constants = require('cp.constants')
|
||||
|
||||
local constants = require('cp.constants')
|
||||
local helpers = require('cp.helpers')
|
||||
local utils = require('cp.utils')
|
||||
|
||||
-- defaults per the new single schema
|
||||
---@type cp.Config
|
||||
M.defaults = {
|
||||
contests = {},
|
||||
snippets = {},
|
||||
open_url = false,
|
||||
languages = {
|
||||
cpp = {
|
||||
extension = 'cc',
|
||||
commands = {
|
||||
build = { 'g++', '-std=c++17', '{source}', '-o', '{binary}' },
|
||||
run = { '{binary}' },
|
||||
debug = {
|
||||
'g++',
|
||||
'-std=c++17',
|
||||
'-fsanitize=address,undefined',
|
||||
'{source}',
|
||||
'-o',
|
||||
'{binary}',
|
||||
},
|
||||
},
|
||||
},
|
||||
python = {
|
||||
extension = 'py',
|
||||
commands = {
|
||||
run = { 'python', '{source}' },
|
||||
debug = { 'python', '{source}' },
|
||||
},
|
||||
},
|
||||
},
|
||||
platforms = {
|
||||
codeforces = {
|
||||
enabled_languages = { 'cpp', 'python' },
|
||||
default_language = 'cpp',
|
||||
overrides = {
|
||||
-- example override, safe to keep empty initially
|
||||
},
|
||||
},
|
||||
atcoder = {
|
||||
enabled_languages = { 'cpp', 'python' },
|
||||
default_language = 'cpp',
|
||||
},
|
||||
codechef = {
|
||||
enabled_languages = { 'cpp', 'python' },
|
||||
default_language = 'cpp',
|
||||
},
|
||||
cses = {
|
||||
enabled_languages = { 'cpp', 'python' },
|
||||
default_language = 'cpp',
|
||||
},
|
||||
},
|
||||
hooks = {
|
||||
before_run = nil,
|
||||
before_debug = nil,
|
||||
setup_code = nil,
|
||||
setup_io_input = helpers.clearcol,
|
||||
setup_io_output = helpers.clearcol,
|
||||
},
|
||||
debug = false,
|
||||
scrapers = constants.PLATFORMS,
|
||||
filename = nil,
|
||||
ui = {
|
||||
ansi = true,
|
||||
run = {
|
||||
width = 0.3,
|
||||
next_test_key = '<c-n>',
|
||||
prev_test_key = '<c-p>',
|
||||
format_verdict = helpers.default_verdict_formatter,
|
||||
},
|
||||
edit = {
|
||||
next_test_key = ']t',
|
||||
prev_test_key = '[t',
|
||||
delete_test_key = 'gd',
|
||||
add_test_key = 'ga',
|
||||
save_and_exit_key = 'q',
|
||||
},
|
||||
panel = { diff_modes = { 'side-by-side', 'git', 'vim' }, max_output_lines = 50 },
|
||||
diff = {
|
||||
git = {
|
||||
args = { 'diff', '--no-index', '--word-diff=plain', '--word-diff-regex=.', '--no-prefix' },
|
||||
},
|
||||
},
|
||||
picker = nil,
|
||||
},
|
||||
runtime = { effective = {} },
|
||||
}
|
||||
|
||||
---@param user_config cp.UserConfig|nil
|
||||
---@return cp.Config
|
||||
function M.setup(user_config)
|
||||
local function is_string_list(t)
|
||||
if type(t) ~= 'table' then
|
||||
return false
|
||||
end
|
||||
for _, v in ipairs(t) do
|
||||
if type(v) ~= 'string' then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function has_tokens(cmd, required)
|
||||
if type(cmd) ~= 'table' then
|
||||
return false
|
||||
end
|
||||
local s = table.concat(cmd, ' ')
|
||||
for _, tok in ipairs(required) do
|
||||
if not s:find(vim.pesc(tok), 1, true) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function validate_language(id, lang)
|
||||
vim.validate({
|
||||
user_config = { user_config, { 'table', 'nil' }, true },
|
||||
extension = { lang.extension, 'string' },
|
||||
commands = { lang.commands, { 'table' } },
|
||||
})
|
||||
|
||||
if user_config then
|
||||
vim.validate({
|
||||
contests = { user_config.contests, { 'table', 'nil' }, true },
|
||||
snippets = { user_config.snippets, { 'table', 'nil' }, true },
|
||||
hooks = { user_config.hooks, { 'table', 'nil' }, true },
|
||||
debug = { user_config.debug, { 'boolean', 'nil' }, true },
|
||||
scrapers = { user_config.scrapers, { 'table', 'nil' }, true },
|
||||
filename = { user_config.filename, { 'function', 'nil' }, true },
|
||||
})
|
||||
if not lang.commands.run then
|
||||
error(('[cp.nvim] languages.%s.commands.run is required'):format(id))
|
||||
end
|
||||
|
||||
if user_config.hooks then
|
||||
vim.validate({
|
||||
before_run = {
|
||||
user_config.hooks.before_run,
|
||||
{ 'function', 'nil' },
|
||||
true,
|
||||
},
|
||||
before_debug = {
|
||||
user_config.hooks.before_debug,
|
||||
{ 'function', 'nil' },
|
||||
true,
|
||||
},
|
||||
setup_code = {
|
||||
user_config.hooks.setup_code,
|
||||
{ 'function', 'nil' },
|
||||
true,
|
||||
},
|
||||
})
|
||||
if lang.commands.build ~= nil then
|
||||
vim.validate({ build = { lang.commands.build, { 'table' } } })
|
||||
if not has_tokens(lang.commands.build, { '{source}', '{binary}' }) then
|
||||
error(('[cp.nvim] languages.%s.commands.build must include {source} and {binary}'):format(id))
|
||||
end
|
||||
|
||||
if user_config.contests then
|
||||
for contest_name, contest_config in pairs(user_config.contests) do
|
||||
for lang_name, lang_config in pairs(contest_config) do
|
||||
if type(lang_config) == 'table' and lang_config.extension then
|
||||
if
|
||||
not vim.tbl_contains(
|
||||
vim.tbl_keys(constants.filetype_to_language),
|
||||
lang_config.extension
|
||||
)
|
||||
then
|
||||
error(
|
||||
("Invalid extension '%s' for language '%s' in contest '%s'. Valid extensions: %s"):format(
|
||||
lang_config.extension,
|
||||
lang_name,
|
||||
contest_name,
|
||||
table.concat(vim.tbl_keys(constants.filetype_to_language), ', ')
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
for _, k in ipairs({ 'run', 'debug' }) do
|
||||
if lang.commands[k] then
|
||||
if not has_tokens(lang.commands[k], { '{binary}' }) then
|
||||
error(('[cp.nvim] languages.%s.commands.%s must include {binary}'):format(id, k))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if user_config.scrapers then
|
||||
for contest_name, enabled in pairs(user_config.scrapers) do
|
||||
if not vim.tbl_contains(constants.PLATFORMS, contest_name) then
|
||||
error(
|
||||
("Invalid contest '%s' in scrapers config. Valid contests: %s"):format(
|
||||
contest_name,
|
||||
table.concat(constants.PLATFORMS, ', ')
|
||||
)
|
||||
)
|
||||
end
|
||||
if type(enabled) ~= 'boolean' then
|
||||
error(
|
||||
("Scraper setting for '%s' must be boolean, got %s"):format(contest_name, type(enabled))
|
||||
)
|
||||
else
|
||||
for _, k in ipairs({ 'run', 'debug' }) do
|
||||
if lang.commands[k] then
|
||||
if not has_tokens(lang.commands[k], { '{source}' }) then
|
||||
error(('[cp.nvim] languages.%s.commands.%s must include {source}'):format(id, k))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local config = vim.tbl_deep_extend('force', M.defaults, user_config or {})
|
||||
return config
|
||||
local function merge_lang(base, ov)
|
||||
if not ov then
|
||||
return base
|
||||
end
|
||||
local out = vim.deepcopy(base)
|
||||
if ov.extension then
|
||||
out.extension = ov.extension
|
||||
end
|
||||
if ov.commands then
|
||||
out.commands = vim.tbl_deep_extend('force', out.commands or {}, ov.commands or {})
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
---@param cfg cp.Config
|
||||
local function build_runtime(cfg)
|
||||
cfg.runtime = cfg.runtime or { effective = {} }
|
||||
for plat, p in pairs(cfg.platforms) do
|
||||
vim.validate({
|
||||
enabled_languages = { p.enabled_languages, is_string_list, 'string[]' },
|
||||
default_language = { p.default_language, 'string' },
|
||||
})
|
||||
for _, lid in ipairs(p.enabled_languages) do
|
||||
if not cfg.languages[lid] then
|
||||
error(("[cp.nvim] platform %s references unknown language '%s'"):format(plat, lid))
|
||||
end
|
||||
end
|
||||
if not vim.tbl_contains(p.enabled_languages, p.default_language) then
|
||||
error(
|
||||
("[cp.nvim] platform %s default_language '%s' not in enabled_languages"):format(
|
||||
plat,
|
||||
p.default_language
|
||||
)
|
||||
)
|
||||
end
|
||||
cfg.runtime.effective[plat] = {}
|
||||
for _, lid in ipairs(p.enabled_languages) do
|
||||
local base = cfg.languages[lid]
|
||||
validate_language(lid, base)
|
||||
local eff = merge_lang(base, p.overrides and p.overrides[lid] or nil)
|
||||
validate_language(lid, eff)
|
||||
cfg.runtime.effective[plat][lid] = eff
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param user_config cp.PartialConfig|nil
|
||||
---@return cp.Config
|
||||
function M.setup(user_config)
|
||||
vim.validate({ user_config = { user_config, { 'table', 'nil' }, true } })
|
||||
local defaults = vim.deepcopy(M.defaults)
|
||||
if user_config and user_config.platforms then
|
||||
for plat in pairs(defaults.platforms) do
|
||||
if not user_config.platforms[plat] then
|
||||
defaults.platforms[plat] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
local cfg = vim.tbl_deep_extend('force', defaults, user_config or {})
|
||||
|
||||
if not next(cfg.languages) then
|
||||
error('[cp.nvim] At least one language must be configured')
|
||||
end
|
||||
|
||||
if not next(cfg.platforms) then
|
||||
error('[cp.nvim] At least one platform must be configured')
|
||||
end
|
||||
|
||||
vim.validate({
|
||||
hooks = { cfg.hooks, { 'table' } },
|
||||
ui = { cfg.ui, { 'table' } },
|
||||
debug = { cfg.debug, { 'boolean', 'nil' }, true },
|
||||
open_url = { cfg.open_url, { 'boolean', 'nil' }, true },
|
||||
filename = { cfg.filename, { 'function', 'nil' }, true },
|
||||
scrapers = {
|
||||
cfg.scrapers,
|
||||
function(v)
|
||||
if type(v) ~= 'table' then
|
||||
return false
|
||||
end
|
||||
for _, s in ipairs(v) do
|
||||
if not vim.tbl_contains(constants.PLATFORMS, s) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end,
|
||||
('one of {%s}'):format(table.concat(constants.PLATFORMS, ',')),
|
||||
},
|
||||
before_run = { cfg.hooks.before_run, { 'function', 'nil' }, true },
|
||||
before_debug = { cfg.hooks.before_debug, { 'function', 'nil' }, true },
|
||||
setup_code = { cfg.hooks.setup_code, { 'function', 'nil' }, true },
|
||||
setup_io_input = { cfg.hooks.setup_io_input, { 'function', 'nil' }, true },
|
||||
setup_io_output = { cfg.hooks.setup_io_output, { 'function', 'nil' }, true },
|
||||
})
|
||||
|
||||
local layouts = require('cp.ui.layouts')
|
||||
vim.validate({
|
||||
ansi = { cfg.ui.ansi, 'boolean' },
|
||||
diff_modes = {
|
||||
cfg.ui.panel.diff_modes,
|
||||
function(v)
|
||||
if type(v) ~= 'table' then
|
||||
return false
|
||||
end
|
||||
for _, mode in ipairs(v) do
|
||||
if not layouts.DIFF_MODES[mode] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end,
|
||||
('one of {%s}'):format(table.concat(vim.tbl_keys(layouts.DIFF_MODES), ',')),
|
||||
},
|
||||
max_output_lines = {
|
||||
cfg.ui.panel.max_output_lines,
|
||||
function(v)
|
||||
return type(v) == 'number' and v > 0 and v == math.floor(v)
|
||||
end,
|
||||
'positive integer',
|
||||
},
|
||||
git = { cfg.ui.diff.git, { 'table' } },
|
||||
git_args = { cfg.ui.diff.git.args, is_string_list, 'string[]' },
|
||||
width = {
|
||||
cfg.ui.run.width,
|
||||
function(v)
|
||||
return type(v) == 'number' and v > 0 and v <= 1
|
||||
end,
|
||||
'decimal between 0 and 1',
|
||||
},
|
||||
next_test_key = {
|
||||
cfg.ui.run.next_test_key,
|
||||
function(v)
|
||||
return v == nil or (type(v) == 'string' and #v > 0)
|
||||
end,
|
||||
'nil or non-empty string',
|
||||
},
|
||||
prev_test_key = {
|
||||
cfg.ui.run.prev_test_key,
|
||||
function(v)
|
||||
return v == nil or (type(v) == 'string' and #v > 0)
|
||||
end,
|
||||
'nil or non-empty string',
|
||||
},
|
||||
format_verdict = {
|
||||
cfg.ui.run.format_verdict,
|
||||
'function',
|
||||
},
|
||||
edit_next_test_key = {
|
||||
cfg.ui.edit.next_test_key,
|
||||
function(v)
|
||||
return v == nil or (type(v) == 'string' and #v > 0)
|
||||
end,
|
||||
'nil or non-empty string',
|
||||
},
|
||||
edit_prev_test_key = {
|
||||
cfg.ui.edit.prev_test_key,
|
||||
function(v)
|
||||
return v == nil or (type(v) == 'string' and #v > 0)
|
||||
end,
|
||||
'nil or non-empty string',
|
||||
},
|
||||
delete_test_key = {
|
||||
cfg.ui.edit.delete_test_key,
|
||||
function(v)
|
||||
return v == nil or (type(v) == 'string' and #v > 0)
|
||||
end,
|
||||
'nil or non-empty string',
|
||||
},
|
||||
add_test_key = {
|
||||
cfg.ui.edit.add_test_key,
|
||||
function(v)
|
||||
return v == nil or (type(v) == 'string' and #v > 0)
|
||||
end,
|
||||
'nil or non-empty string',
|
||||
},
|
||||
save_and_exit_key = {
|
||||
cfg.ui.edit.save_and_exit_key,
|
||||
function(v)
|
||||
return v == nil or (type(v) == 'string' and #v > 0)
|
||||
end,
|
||||
'nil or non-empty string',
|
||||
},
|
||||
picker = {
|
||||
cfg.ui.picker,
|
||||
function(v)
|
||||
return v == nil or v == 'telescope' or v == 'fzf-lua'
|
||||
end,
|
||||
"nil, 'telescope', or 'fzf-lua'",
|
||||
},
|
||||
})
|
||||
|
||||
for id, lang in pairs(cfg.languages) do
|
||||
validate_language(id, lang)
|
||||
end
|
||||
|
||||
build_runtime(cfg)
|
||||
|
||||
local ok, err = utils.check_required_runtime()
|
||||
if not ok then
|
||||
error('[cp.nvim] ' .. err)
|
||||
end
|
||||
|
||||
return cfg
|
||||
end
|
||||
|
||||
local current_config = nil
|
||||
|
||||
function M.set_current_config(config)
|
||||
current_config = config
|
||||
end
|
||||
|
||||
function M.get_config()
|
||||
return current_config or M.defaults
|
||||
end
|
||||
|
||||
---Validate and get effective language config for a platform
|
||||
---@param platform_id string
|
||||
---@param language_id string
|
||||
---@return { valid: boolean, effective?: CpLanguage, extension?: string, error?: string }
|
||||
function M.get_language_for_platform(platform_id, language_id)
|
||||
local cfg = M.get_config()
|
||||
|
||||
if not cfg.platforms[platform_id] then
|
||||
return { valid = false, error = string.format("Unknown platform '%s'", platform_id) }
|
||||
end
|
||||
|
||||
local platform = cfg.platforms[platform_id]
|
||||
|
||||
if not cfg.languages[language_id] then
|
||||
local available = table.concat(platform.enabled_languages, ', ')
|
||||
return {
|
||||
valid = false,
|
||||
error = string.format("Unknown language '%s'. Available: [%s]", language_id, available),
|
||||
}
|
||||
end
|
||||
|
||||
if not vim.tbl_contains(platform.enabled_languages, language_id) then
|
||||
local available = table.concat(platform.enabled_languages, ', ')
|
||||
return {
|
||||
valid = false,
|
||||
error = string.format(
|
||||
"Language '%s' not enabled for %s. Available: [%s]",
|
||||
language_id,
|
||||
platform_id,
|
||||
available
|
||||
),
|
||||
}
|
||||
end
|
||||
|
||||
local platform_effective = cfg.runtime.effective[platform_id]
|
||||
if not platform_effective then
|
||||
return {
|
||||
valid = false,
|
||||
error = string.format(
|
||||
'No runtime config for platform %s (plugin not initialized)',
|
||||
platform_id
|
||||
),
|
||||
}
|
||||
end
|
||||
|
||||
local effective = platform_effective[language_id]
|
||||
if not effective then
|
||||
return {
|
||||
valid = false,
|
||||
error = string.format('No effective config for %s/%s', platform_id, language_id),
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
valid = true,
|
||||
effective = effective,
|
||||
extension = effective.extension,
|
||||
}
|
||||
end
|
||||
|
||||
---@param contest_id string
|
||||
---@param problem_id? string
|
||||
---@return string
|
||||
local function default_filename(contest_id, problem_id)
|
||||
vim.validate({
|
||||
contest_id = { contest_id, 'string' },
|
||||
problem_id = { problem_id, { 'string', 'nil' }, true },
|
||||
})
|
||||
|
||||
if problem_id then
|
||||
return (contest_id .. problem_id):lower()
|
||||
else
|
||||
return contest_id:lower()
|
||||
end
|
||||
return contest_id:lower()
|
||||
end
|
||||
|
||||
M.default_filename = default_filename
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
local M = {}
|
||||
|
||||
M.PLATFORMS = { 'atcoder', 'codeforces', 'cses' }
|
||||
M.ACTIONS = { 'test', 'next', 'prev' }
|
||||
M.PLATFORMS = { 'atcoder', 'codechef', 'codeforces', 'cses' }
|
||||
M.ACTIONS = { 'run', 'panel', 'next', 'prev', 'pick', 'cache', 'interact', 'edit' }
|
||||
|
||||
M.PLATFORM_DISPLAY_NAMES = {
|
||||
atcoder = 'AtCoder',
|
||||
codechef = 'CodeChef',
|
||||
codeforces = 'CodeForces',
|
||||
cses = 'CSES',
|
||||
}
|
||||
|
||||
M.CPP = 'cpp'
|
||||
M.PYTHON = 'python'
|
||||
|
||||
---@type table<string, string>
|
||||
M.filetype_to_language = {
|
||||
cc = M.CPP,
|
||||
cxx = M.CPP,
|
||||
python = M.PYTHON,
|
||||
cpp = M.CPP,
|
||||
py = M.PYTHON,
|
||||
py3 = M.PYTHON,
|
||||
}
|
||||
|
||||
---@type table<string, string>
|
||||
|
|
@ -21,6 +25,12 @@ M.canonical_filetypes = {
|
|||
[M.PYTHON] = 'python',
|
||||
}
|
||||
|
||||
---@type table<string, string>
|
||||
M.canonical_filetype_to_extension = {
|
||||
[M.CPP] = 'cc',
|
||||
[M.PYTHON] = 'py',
|
||||
}
|
||||
|
||||
---@type table<number, string>
|
||||
M.signal_codes = {
|
||||
[128] = 'SIGILL',
|
||||
|
|
|
|||
|
|
@ -1,296 +0,0 @@
|
|||
---@class ExecuteResult
|
||||
---@field stdout string
|
||||
---@field stderr string
|
||||
---@field code integer
|
||||
---@field time_ms number
|
||||
---@field timed_out boolean
|
||||
|
||||
local M = {}
|
||||
local logger = require('cp.log')
|
||||
|
||||
local constants = require('cp.constants')
|
||||
local filetype_to_language = constants.filetype_to_language
|
||||
|
||||
---@param source_file string
|
||||
---@param contest_config table
|
||||
---@return string
|
||||
local function get_language_from_file(source_file, contest_config)
|
||||
vim.validate({
|
||||
source_file = { source_file, 'string' },
|
||||
contest_config = { contest_config, 'table' },
|
||||
})
|
||||
|
||||
local extension = vim.fn.fnamemodify(source_file, ':e')
|
||||
local language = filetype_to_language[extension] or contest_config.default_language
|
||||
logger.log(('detected language: %s (extension: %s)'):format(language, extension))
|
||||
return language
|
||||
end
|
||||
|
||||
---@param cmd_template string[]
|
||||
---@param substitutions table<string, string>
|
||||
---@return string[]
|
||||
local function substitute_template(cmd_template, substitutions)
|
||||
vim.validate({
|
||||
cmd_template = { cmd_template, 'table' },
|
||||
substitutions = { substitutions, 'table' },
|
||||
})
|
||||
|
||||
local result = {}
|
||||
for _, arg in ipairs(cmd_template) do
|
||||
local substituted = arg
|
||||
for key, value in pairs(substitutions) do
|
||||
substituted = substituted:gsub('{' .. key .. '}', value)
|
||||
end
|
||||
table.insert(result, substituted)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
---@param cmd_template string[]
|
||||
---@param executable? string
|
||||
---@param substitutions table<string, string>
|
||||
---@return string[]
|
||||
local function build_command(cmd_template, executable, substitutions)
|
||||
vim.validate({
|
||||
cmd_template = { cmd_template, 'table' },
|
||||
executable = { executable, { 'string', 'nil' }, true },
|
||||
substitutions = { substitutions, 'table' },
|
||||
})
|
||||
|
||||
local cmd = substitute_template(cmd_template, substitutions)
|
||||
if executable then
|
||||
table.insert(cmd, 1, executable)
|
||||
end
|
||||
return cmd
|
||||
end
|
||||
|
||||
local function ensure_directories()
|
||||
vim.system({ 'mkdir', '-p', 'build', 'io' }):wait()
|
||||
end
|
||||
|
||||
---@param language_config table
|
||||
---@param substitutions table<string, string>
|
||||
---@return {code: integer, stderr: string}
|
||||
function M.compile_generic(language_config, substitutions)
|
||||
vim.validate({
|
||||
language_config = { language_config, 'table' },
|
||||
substitutions = { substitutions, 'table' },
|
||||
})
|
||||
|
||||
if not language_config.compile then
|
||||
logger.log('no compilation step required')
|
||||
return { code = 0, stderr = '' }
|
||||
end
|
||||
|
||||
local compile_cmd = substitute_template(language_config.compile, substitutions)
|
||||
logger.log(('compiling: %s'):format(table.concat(compile_cmd, ' ')))
|
||||
|
||||
local start_time = vim.uv.hrtime()
|
||||
local result = vim.system(compile_cmd, { text = true }):wait()
|
||||
local compile_time = (vim.uv.hrtime() - start_time) / 1000000
|
||||
|
||||
if result.code == 0 then
|
||||
logger.log(('compilation successful (%.1fms)'):format(compile_time))
|
||||
else
|
||||
logger.log(
|
||||
('compilation failed (%.1fms): %s'):format(compile_time, result.stderr),
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
---@param cmd string[]
|
||||
---@param input_data string
|
||||
---@param timeout_ms integer
|
||||
---@return ExecuteResult
|
||||
local function execute_command(cmd, input_data, timeout_ms)
|
||||
vim.validate({
|
||||
cmd = { cmd, 'table' },
|
||||
input_data = { input_data, 'string' },
|
||||
timeout_ms = { timeout_ms, 'number' },
|
||||
})
|
||||
|
||||
logger.log(('executing: %s'):format(table.concat(cmd, ' ')))
|
||||
|
||||
local start_time = vim.uv.hrtime()
|
||||
|
||||
local result = vim
|
||||
.system(cmd, {
|
||||
stdin = input_data,
|
||||
timeout = timeout_ms,
|
||||
text = true,
|
||||
})
|
||||
:wait()
|
||||
|
||||
local end_time = vim.uv.hrtime()
|
||||
local execution_time = (end_time - start_time) / 1000000
|
||||
|
||||
local actual_code = result.code or 0
|
||||
|
||||
if result.code == 124 then
|
||||
logger.log(('execution timed out after %.1fms'):format(execution_time), vim.log.levels.WARN)
|
||||
elseif actual_code ~= 0 then
|
||||
logger.log(
|
||||
('execution failed (exit code %d, %.1fms)'):format(actual_code, execution_time),
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
else
|
||||
logger.log(('execution successful (%.1fms)'):format(execution_time))
|
||||
end
|
||||
|
||||
return {
|
||||
stdout = result.stdout or '',
|
||||
stderr = result.stderr or '',
|
||||
code = actual_code,
|
||||
time_ms = execution_time,
|
||||
timed_out = result.code == 124,
|
||||
}
|
||||
end
|
||||
|
||||
---@param exec_result ExecuteResult
|
||||
---@param expected_file string
|
||||
---@param is_debug boolean
|
||||
---@return string
|
||||
local function format_output(exec_result, expected_file, is_debug)
|
||||
vim.validate({
|
||||
exec_result = { exec_result, 'table' },
|
||||
expected_file = { expected_file, 'string' },
|
||||
is_debug = { is_debug, 'boolean' },
|
||||
})
|
||||
|
||||
local output_lines = { exec_result.stdout }
|
||||
local metadata_lines = {}
|
||||
|
||||
if exec_result.timed_out then
|
||||
table.insert(metadata_lines, '[code]: 124 (TIMEOUT)')
|
||||
elseif exec_result.code >= 128 then
|
||||
local signal_name = constants.signal_codes[exec_result.code] or 'SIGNAL'
|
||||
table.insert(metadata_lines, ('[code]: %d (%s)'):format(exec_result.code, signal_name))
|
||||
else
|
||||
table.insert(metadata_lines, ('[code]: %d'):format(exec_result.code))
|
||||
end
|
||||
|
||||
table.insert(metadata_lines, ('[time]: %.2f ms'):format(exec_result.time_ms))
|
||||
table.insert(metadata_lines, ('[debug]: %s'):format(is_debug and 'true' or 'false'))
|
||||
|
||||
if vim.fn.filereadable(expected_file) == 1 and exec_result.code == 0 then
|
||||
local expected_content = vim.fn.readfile(expected_file)
|
||||
local actual_lines = vim.split(exec_result.stdout, '\n')
|
||||
|
||||
while #actual_lines > 0 and actual_lines[#actual_lines] == '' do
|
||||
table.remove(actual_lines)
|
||||
end
|
||||
|
||||
local ok = #actual_lines == #expected_content
|
||||
if ok then
|
||||
for i, line in ipairs(actual_lines) do
|
||||
if line ~= expected_content[i] then
|
||||
ok = false
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(metadata_lines, ('[ok]: %s'):format(ok and 'true' or 'false'))
|
||||
end
|
||||
|
||||
return table.concat(output_lines, '') .. '\n' .. table.concat(metadata_lines, '\n')
|
||||
end
|
||||
|
||||
---@param ctx ProblemContext
|
||||
---@param contest_config ContestConfig
|
||||
---@param is_debug? boolean
|
||||
---@return boolean success
|
||||
function M.compile_problem(ctx, contest_config, is_debug)
|
||||
vim.validate({
|
||||
ctx = { ctx, 'table' },
|
||||
contest_config = { contest_config, 'table' },
|
||||
})
|
||||
|
||||
local language = get_language_from_file(ctx.source_file, contest_config)
|
||||
local language_config = contest_config[language]
|
||||
|
||||
if not language_config then
|
||||
logger.log('No configuration for language: ' .. language, vim.log.levels.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
local substitutions = {
|
||||
source = ctx.source_file,
|
||||
binary = ctx.binary_file,
|
||||
version = tostring(language_config.version),
|
||||
}
|
||||
|
||||
local compile_cmd = (is_debug and language_config.debug) and language_config.debug
|
||||
or language_config.compile
|
||||
if compile_cmd then
|
||||
language_config.compile = compile_cmd
|
||||
local compile_result = M.compile_generic(language_config, substitutions)
|
||||
if compile_result.code ~= 0 then
|
||||
logger.log(
|
||||
'compilation failed: ' .. (compile_result.stderr or 'unknown error'),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return false
|
||||
end
|
||||
logger.log(('compilation successful (%s)'):format(is_debug and 'debug mode' or 'test mode'))
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function M.run_problem(ctx, contest_config, is_debug)
|
||||
vim.validate({
|
||||
ctx = { ctx, 'table' },
|
||||
contest_config = { contest_config, 'table' },
|
||||
is_debug = { is_debug, 'boolean' },
|
||||
})
|
||||
|
||||
ensure_directories()
|
||||
|
||||
local language = get_language_from_file(ctx.source_file, contest_config)
|
||||
local language_config = contest_config[language]
|
||||
|
||||
if not language_config then
|
||||
vim.fn.writefile({ 'Error: No configuration for language: ' .. language }, ctx.output_file)
|
||||
return
|
||||
end
|
||||
|
||||
local substitutions = {
|
||||
source = ctx.source_file,
|
||||
binary = ctx.binary_file,
|
||||
version = tostring(language_config.version),
|
||||
}
|
||||
|
||||
local compile_cmd = is_debug and language_config.debug or language_config.compile
|
||||
if compile_cmd then
|
||||
local compile_result = M.compile_generic(language_config, substitutions)
|
||||
if compile_result.code ~= 0 then
|
||||
vim.fn.writefile({ compile_result.stderr }, ctx.output_file)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local input_data = ''
|
||||
if vim.fn.filereadable(ctx.input_file) == 1 then
|
||||
input_data = table.concat(vim.fn.readfile(ctx.input_file), '\n') .. '\n'
|
||||
end
|
||||
|
||||
local run_cmd = build_command(language_config.run, language_config.executable, substitutions)
|
||||
local exec_result = execute_command(run_cmd, input_data, contest_config.timeout_ms)
|
||||
local formatted_output = format_output(exec_result, ctx.expected_file, is_debug)
|
||||
|
||||
local output_buf = vim.fn.bufnr(ctx.output_file)
|
||||
if output_buf ~= -1 then
|
||||
vim.api.nvim_buf_set_lines(output_buf, 0, -1, false, vim.split(formatted_output, '\n'))
|
||||
vim.api.nvim_buf_call(output_buf, function()
|
||||
vim.cmd.write()
|
||||
end)
|
||||
else
|
||||
vim.fn.writefile(vim.split(formatted_output, '\n'), ctx.output_file)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -1,95 +1,77 @@
|
|||
local M = {}
|
||||
|
||||
local function check_nvim_version()
|
||||
local utils = require('cp.utils')
|
||||
|
||||
local function check()
|
||||
vim.health.start('cp.nvim [required] ~')
|
||||
|
||||
utils.setup_python_env()
|
||||
|
||||
if vim.fn.has('nvim-0.10.0') == 1 then
|
||||
vim.health.ok('Neovim 0.10.0+ detected')
|
||||
else
|
||||
vim.health.error('cp.nvim requires Neovim 0.10.0+')
|
||||
end
|
||||
end
|
||||
|
||||
local function check_uv()
|
||||
if vim.fn.executable('uv') == 1 then
|
||||
vim.health.ok('uv executable found')
|
||||
local uname = vim.loop.os_uname()
|
||||
if uname.sysname == 'Windows_NT' then
|
||||
vim.health.error('Windows is not supported')
|
||||
end
|
||||
|
||||
local result = vim.system({ 'uv', '--version' }, { text = true }):wait()
|
||||
if result.code == 0 then
|
||||
vim.health.info('uv version: ' .. result.stdout:gsub('\n', ''))
|
||||
if utils.is_nix_build() then
|
||||
local source = utils.is_nix_discovered() and 'runtime discovery' or 'flake install'
|
||||
vim.health.ok('Nix Python environment detected (' .. source .. ')')
|
||||
local py = utils.get_nix_python()
|
||||
vim.health.info('Python: ' .. py)
|
||||
local r = vim.system({ py, '--version' }, { text = true }):wait()
|
||||
if r.code == 0 then
|
||||
vim.health.info('Python version: ' .. r.stdout:gsub('\n', ''))
|
||||
end
|
||||
else
|
||||
vim.health.warn('uv not found - install from https://docs.astral.sh/uv/ for problem scraping')
|
||||
end
|
||||
end
|
||||
|
||||
local function check_python_env()
|
||||
local plugin_path = debug.getinfo(1, 'S').source:sub(2)
|
||||
plugin_path = vim.fn.fnamemodify(plugin_path, ':h:h:h')
|
||||
local venv_dir = plugin_path .. '/.venv'
|
||||
|
||||
if vim.fn.isdirectory(venv_dir) == 1 then
|
||||
vim.health.ok('Python virtual environment found at ' .. venv_dir)
|
||||
else
|
||||
vim.health.warn('Python virtual environment not set up - run :CP command to initialize')
|
||||
end
|
||||
end
|
||||
|
||||
local function check_scrapers()
|
||||
local plugin_path = debug.getinfo(1, 'S').source:sub(2)
|
||||
plugin_path = vim.fn.fnamemodify(plugin_path, ':h:h:h')
|
||||
|
||||
local scrapers = { 'atcoder.py', 'codeforces.py', 'cses.py' }
|
||||
for _, scraper in ipairs(scrapers) do
|
||||
local scraper_path = plugin_path .. '/scrapers/' .. scraper
|
||||
if vim.fn.filereadable(scraper_path) == 1 then
|
||||
vim.health.ok('Scraper found: ' .. scraper)
|
||||
else
|
||||
vim.health.error('Missing scraper: ' .. scraper)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function check_luasnip()
|
||||
local has_luasnip, luasnip = pcall(require, 'luasnip')
|
||||
if has_luasnip then
|
||||
vim.health.ok('LuaSnip integration available')
|
||||
local snippet_count = #luasnip.get_snippets('all')
|
||||
vim.health.info('LuaSnip snippets loaded: ' .. snippet_count)
|
||||
else
|
||||
vim.health.info('LuaSnip not available - template expansion will be limited')
|
||||
end
|
||||
end
|
||||
|
||||
local function check_config()
|
||||
vim.health.ok('Plugin ready')
|
||||
|
||||
local cp = require('cp')
|
||||
local context = cp.get_current_context()
|
||||
if context.platform then
|
||||
local info = context.platform
|
||||
if context.contest_id then
|
||||
info = info .. ' ' .. context.contest_id
|
||||
if context.problem_id then
|
||||
info = info .. ' ' .. context.problem_id
|
||||
if vim.fn.executable('uv') == 1 then
|
||||
vim.health.ok('uv executable found')
|
||||
local r = vim.system({ 'uv', '--version' }, { text = true }):wait()
|
||||
if r.code == 0 then
|
||||
vim.health.info('uv version: ' .. r.stdout:gsub('\n', ''))
|
||||
end
|
||||
else
|
||||
vim.health.warn('uv not found (install https://docs.astral.sh/uv/ for scraping)')
|
||||
end
|
||||
vim.health.info('Current context: ' .. info)
|
||||
|
||||
if vim.fn.executable('nix') == 1 then
|
||||
vim.health.info('nix available but Python environment not resolved via nix')
|
||||
end
|
||||
|
||||
local plugin_path = utils.get_plugin_path()
|
||||
local venv_dir = plugin_path .. '/.venv'
|
||||
if vim.fn.isdirectory(venv_dir) == 1 then
|
||||
vim.health.ok('Python virtual environment found at ' .. venv_dir)
|
||||
else
|
||||
vim.health.info('Python virtual environment not set up (created on first scrape)')
|
||||
end
|
||||
end
|
||||
|
||||
local time_cap = utils.time_capability()
|
||||
if time_cap.ok then
|
||||
vim.health.ok('GNU time found: ' .. time_cap.path)
|
||||
else
|
||||
vim.health.info('No contest context set')
|
||||
vim.health.error('GNU time not found: ' .. (time_cap.reason or ''))
|
||||
end
|
||||
|
||||
local timeout_cap = utils.timeout_capability()
|
||||
if timeout_cap.ok then
|
||||
vim.health.ok('GNU timeout found: ' .. timeout_cap.path)
|
||||
else
|
||||
vim.health.error('GNU timeout not found: ' .. (timeout_cap.reason or ''))
|
||||
end
|
||||
end
|
||||
|
||||
function M.check()
|
||||
local version = require('cp.version')
|
||||
vim.health.start('cp.nvim health check')
|
||||
|
||||
vim.health.start('cp.nvim health check ~')
|
||||
vim.health.info('Version: ' .. version.version)
|
||||
|
||||
check_nvim_version()
|
||||
check_uv()
|
||||
check_python_env()
|
||||
check_scrapers()
|
||||
check_luasnip()
|
||||
check_config()
|
||||
check()
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
101
lua/cp/helpers.lua
Normal file
101
lua/cp/helpers.lua
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
local M = {}
|
||||
|
||||
---@param bufnr integer
|
||||
function M.clearcol(bufnr)
|
||||
for _, win in ipairs(vim.fn.win_findbuf(bufnr)) do
|
||||
vim.wo[win].signcolumn = 'no'
|
||||
vim.wo[win].statuscolumn = ''
|
||||
vim.wo[win].number = false
|
||||
vim.wo[win].relativenumber = false
|
||||
end
|
||||
end
|
||||
|
||||
---Pad text on the right (left-align text within width)
|
||||
---@param text string
|
||||
---@param width integer
|
||||
---@return string
|
||||
function M.pad_right(text, width)
|
||||
local pad = width - #text
|
||||
if pad <= 0 then
|
||||
return text
|
||||
end
|
||||
return text .. string.rep(' ', pad)
|
||||
end
|
||||
|
||||
---Pad text on the left (right-align text within width)
|
||||
---@param text string
|
||||
---@param width integer
|
||||
---@return string
|
||||
function M.pad_left(text, width)
|
||||
local pad = width - #text
|
||||
if pad <= 0 then
|
||||
return text
|
||||
end
|
||||
return string.rep(' ', pad) .. text
|
||||
end
|
||||
|
||||
---Center text within width
|
||||
---@param text string
|
||||
---@param width integer
|
||||
---@return string
|
||||
function M.center(text, width)
|
||||
local pad = width - #text
|
||||
if pad <= 0 then
|
||||
return text
|
||||
end
|
||||
local left = math.ceil(pad / 2)
|
||||
return string.rep(' ', left) .. text .. string.rep(' ', pad - left)
|
||||
end
|
||||
|
||||
---Default verdict formatter for I/O view
|
||||
---@param data VerdictFormatData
|
||||
---@return VerdictFormatResult
|
||||
function M.default_verdict_formatter(data)
|
||||
local time_actual = string.format('%.2f', data.time_ms)
|
||||
local time_limit = tostring(data.time_limit_ms)
|
||||
local mem_actual = string.format('%.0f', data.memory_mb)
|
||||
local mem_limit = string.format('%.0f', data.memory_limit_mb)
|
||||
local exit_str = data.signal and string.format('%d (%s)', data.exit_code, data.signal)
|
||||
or tostring(data.exit_code)
|
||||
|
||||
local time_actual_w = data.time_actual_width or 6
|
||||
local time_limit_w = data.time_limit_width or 4
|
||||
local mem_actual_w = data.mem_actual_width or 3
|
||||
local mem_limit_w = data.mem_limit_width or 3
|
||||
|
||||
local test_num_part = 'Test ' .. data.index .. ':'
|
||||
local status_part = M.pad_right(data.status.text, 3)
|
||||
local time_part = M.pad_left(time_actual, time_actual_w)
|
||||
.. '/'
|
||||
.. M.pad_left(time_limit, time_limit_w)
|
||||
.. ' ms'
|
||||
local mem_part = M.pad_left(mem_actual, mem_actual_w)
|
||||
.. '/'
|
||||
.. M.pad_left(mem_limit, mem_limit_w)
|
||||
.. ' MB'
|
||||
local exit_part = 'exit: ' .. exit_str
|
||||
|
||||
local line = test_num_part
|
||||
.. ' '
|
||||
.. status_part
|
||||
.. ' | '
|
||||
.. time_part
|
||||
.. ' | '
|
||||
.. mem_part
|
||||
.. ' | '
|
||||
.. exit_part
|
||||
|
||||
local highlights = {}
|
||||
local status_pos = line:find(data.status.text, 1, true)
|
||||
if status_pos then
|
||||
table.insert(highlights, {
|
||||
col_start = status_pos - 1,
|
||||
col_end = status_pos - 1 + #data.status.text,
|
||||
group = data.status.highlight_group,
|
||||
})
|
||||
end
|
||||
|
||||
return { line = line, highlights = highlights }
|
||||
end
|
||||
|
||||
return M
|
||||
709
lua/cp/init.lua
709
lua/cp/init.lua
|
|
@ -1,701 +1,54 @@
|
|||
local M = {}
|
||||
|
||||
local cache = require('cp.cache')
|
||||
local config_module = require('cp.config')
|
||||
local helpers = require('cp.helpers')
|
||||
local logger = require('cp.log')
|
||||
local problem = require('cp.problem')
|
||||
local scrape = require('cp.scrape')
|
||||
local snippets = require('cp.snippets')
|
||||
|
||||
if not vim.fn.has('nvim-0.10.0') then
|
||||
vim.notify('[cp.nvim]: requires nvim-0.10.0+', vim.log.levels.ERROR)
|
||||
M.helpers = helpers
|
||||
|
||||
if vim.fn.has('nvim-0.10.0') == 0 then
|
||||
logger.log('Requires nvim-0.10.0+', vim.log.levels.ERROR)
|
||||
return {}
|
||||
end
|
||||
|
||||
local user_config = {}
|
||||
local config = config_module.setup(user_config)
|
||||
logger.set_config(config)
|
||||
local snippets_initialized = false
|
||||
local initialized = false
|
||||
|
||||
local state = {
|
||||
platform = nil,
|
||||
contest_id = nil,
|
||||
problem_id = nil,
|
||||
saved_layout = nil,
|
||||
saved_session = nil,
|
||||
test_cases = nil,
|
||||
test_states = {},
|
||||
test_panel_active = false,
|
||||
}
|
||||
|
||||
local constants = require('cp.constants')
|
||||
local platforms = constants.PLATFORMS
|
||||
local actions = constants.ACTIONS
|
||||
|
||||
local function set_platform(platform)
|
||||
if not vim.tbl_contains(platforms, platform) then
|
||||
logger.log(
|
||||
('unknown platform. Available: [%s]'):format(table.concat(platforms, ', ')),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
local function ensure_initialized()
|
||||
if initialized then
|
||||
return true
|
||||
end
|
||||
local user_config = vim.g.cp or {}
|
||||
local ok, result = pcall(config_module.setup, user_config)
|
||||
if not ok then
|
||||
local msg = tostring(result):gsub('^.+:%d+: ', '')
|
||||
vim.notify(msg, vim.log.levels.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
state.platform = platform
|
||||
vim.fn.mkdir('build', 'p')
|
||||
vim.fn.mkdir('io', 'p')
|
||||
config_module.set_current_config(result)
|
||||
initialized = true
|
||||
return true
|
||||
end
|
||||
|
||||
---@param contest_id string
|
||||
---@param problem_id? string
|
||||
---@param language? string
|
||||
local function setup_problem(contest_id, problem_id, language)
|
||||
if not state.platform then
|
||||
logger.log('no platform set. run :CP <platform> <contest> first', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local problem_name = state.platform == 'cses' and contest_id or (contest_id .. (problem_id or ''))
|
||||
logger.log(('setting up problem: %s'):format(problem_name))
|
||||
|
||||
local ctx = problem.create_context(state.platform, contest_id, problem_id, config, language)
|
||||
|
||||
if vim.tbl_contains(config.scrapers, state.platform) then
|
||||
local metadata_result = scrape.scrape_contest_metadata(state.platform, contest_id)
|
||||
if not metadata_result.success then
|
||||
logger.log(
|
||||
'failed to load contest metadata: ' .. (metadata_result.error or 'unknown error'),
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
local cached_test_cases = cache.get_test_cases(state.platform, contest_id, problem_id)
|
||||
if cached_test_cases then
|
||||
state.test_cases = cached_test_cases
|
||||
end
|
||||
|
||||
if vim.tbl_contains(config.scrapers, state.platform) then
|
||||
local scrape_result = scrape.scrape_problem(ctx)
|
||||
|
||||
if not scrape_result.success then
|
||||
logger.log(
|
||||
'scraping failed: ' .. (scrape_result.error or 'unknown error'),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
local test_count = scrape_result.test_count or 0
|
||||
logger.log(('scraped %d test case(s) for %s'):format(test_count, scrape_result.problem_id))
|
||||
state.test_cases = scrape_result.test_cases
|
||||
|
||||
if scrape_result.test_cases then
|
||||
cache.set_test_cases(state.platform, contest_id, problem_id, scrape_result.test_cases)
|
||||
end
|
||||
else
|
||||
logger.log(('scraping disabled for %s'):format(state.platform))
|
||||
state.test_cases = nil
|
||||
end
|
||||
|
||||
vim.cmd('silent only')
|
||||
|
||||
state.contest_id = contest_id
|
||||
state.problem_id = problem_id
|
||||
|
||||
vim.cmd.e(ctx.source_file)
|
||||
local source_buf = vim.api.nvim_get_current_buf()
|
||||
|
||||
if vim.api.nvim_buf_get_lines(source_buf, 0, -1, true)[1] == '' then
|
||||
local has_luasnip, luasnip = pcall(require, 'luasnip')
|
||||
if has_luasnip then
|
||||
local filetype = vim.api.nvim_get_option_value('filetype', { buf = source_buf })
|
||||
local language_name = constants.filetype_to_language[filetype]
|
||||
local canonical_language = constants.canonical_filetypes[language_name] or language_name
|
||||
local prefixed_trigger = ('cp.nvim/%s.%s'):format(state.platform, canonical_language)
|
||||
|
||||
vim.api.nvim_buf_set_lines(0, 0, -1, false, { prefixed_trigger })
|
||||
vim.api.nvim_win_set_cursor(0, { 1, #prefixed_trigger })
|
||||
vim.cmd.startinsert({ bang = true })
|
||||
|
||||
vim.schedule(function()
|
||||
if luasnip.expandable() then
|
||||
luasnip.expand()
|
||||
else
|
||||
vim.api.nvim_buf_set_lines(0, 0, 1, false, { '' })
|
||||
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||
end
|
||||
vim.cmd.stopinsert()
|
||||
end)
|
||||
else
|
||||
vim.api.nvim_input(('i%s<c-space><esc>'):format(state.platform))
|
||||
end
|
||||
end
|
||||
|
||||
if config.hooks and config.hooks.setup_code then
|
||||
config.hooks.setup_code(ctx)
|
||||
end
|
||||
|
||||
logger.log(('switched to problem %s'):format(ctx.problem_name))
|
||||
end
|
||||
|
||||
local function get_current_problem()
|
||||
local filename = vim.fn.expand('%:t:r')
|
||||
if filename == '' then
|
||||
logger.log('no file open', vim.log.levels.ERROR)
|
||||
return nil
|
||||
end
|
||||
return filename
|
||||
end
|
||||
|
||||
local function toggle_test_panel(is_debug)
|
||||
if state.test_panel_active then
|
||||
if state.saved_session then
|
||||
vim.cmd(('source %s'):format(state.saved_session))
|
||||
vim.fn.delete(state.saved_session)
|
||||
state.saved_session = nil
|
||||
end
|
||||
state.test_panel_active = false
|
||||
logger.log('test panel closed')
|
||||
return
|
||||
end
|
||||
|
||||
if not state.platform then
|
||||
logger.log(
|
||||
'No contest configured. Use :CP <platform> <contest> <problem> to set up first.',
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
local problem_id = get_current_problem()
|
||||
if not problem_id then
|
||||
return
|
||||
end
|
||||
|
||||
local ctx = problem.create_context(state.platform, state.contest_id, state.problem_id, config)
|
||||
local test_module = require('cp.test')
|
||||
|
||||
if not test_module.load_test_cases(ctx, state) then
|
||||
logger.log('no test cases found', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
state.saved_session = vim.fn.tempname()
|
||||
vim.cmd(('mksession! %s'):format(state.saved_session))
|
||||
|
||||
vim.cmd('silent only')
|
||||
|
||||
local tab_buf = vim.api.nvim_create_buf(false, true)
|
||||
local expected_buf = vim.api.nvim_create_buf(false, true)
|
||||
local actual_buf = vim.api.nvim_create_buf(false, true)
|
||||
|
||||
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = tab_buf })
|
||||
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = expected_buf })
|
||||
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = actual_buf })
|
||||
|
||||
local main_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(main_win, tab_buf)
|
||||
vim.api.nvim_set_option_value('filetype', 'cptest', { buf = tab_buf })
|
||||
|
||||
vim.cmd.split()
|
||||
vim.api.nvim_win_set_buf(0, actual_buf)
|
||||
vim.api.nvim_set_option_value('filetype', 'cptest', { buf = actual_buf })
|
||||
|
||||
vim.cmd.vsplit()
|
||||
vim.api.nvim_win_set_buf(0, expected_buf)
|
||||
vim.api.nvim_set_option_value('filetype', 'cptest', { buf = expected_buf })
|
||||
|
||||
local expected_win = vim.fn.bufwinid(expected_buf)
|
||||
local actual_win = vim.fn.bufwinid(actual_buf)
|
||||
|
||||
local test_windows = {
|
||||
tab_win = main_win,
|
||||
actual_win = actual_win,
|
||||
expected_win = expected_win,
|
||||
}
|
||||
local test_buffers = {
|
||||
tab_buf = tab_buf,
|
||||
expected_buf = expected_buf,
|
||||
actual_buf = actual_buf,
|
||||
}
|
||||
|
||||
local function render_test_tabs()
|
||||
local test_state = test_module.get_test_panel_state()
|
||||
local tab_lines = {}
|
||||
|
||||
local max_status_width = 0
|
||||
local max_code_width = 0
|
||||
local max_time_width = 0
|
||||
|
||||
for _, test_case in ipairs(test_state.test_cases) do
|
||||
local status_text = test_case.status == 'pending' and '' or string.upper(test_case.status)
|
||||
max_status_width = math.max(max_status_width, #status_text)
|
||||
|
||||
if test_case.code then
|
||||
max_code_width = math.max(max_code_width, #tostring(test_case.code))
|
||||
end
|
||||
|
||||
if test_case.time_ms then
|
||||
local time_text = string.format('%.0fms', test_case.time_ms)
|
||||
max_time_width = math.max(max_time_width, #time_text)
|
||||
end
|
||||
end
|
||||
|
||||
for i, test_case in ipairs(test_state.test_cases) do
|
||||
local prefix = i == test_state.current_index and '> ' or ' '
|
||||
local tab = string.format('%s%d.', prefix, i)
|
||||
|
||||
if test_case.ok ~= nil then
|
||||
tab = tab .. string.format(' [ok:%-5s]', tostring(test_case.ok))
|
||||
end
|
||||
|
||||
if test_case.code then
|
||||
tab = tab .. string.format(' [code:%-' .. max_code_width .. 's]', tostring(test_case.code))
|
||||
end
|
||||
|
||||
if test_case.time_ms then
|
||||
local time_text = string.format('%.0fms', test_case.time_ms)
|
||||
tab = tab .. string.format(' [time:%-' .. max_time_width .. 's]', time_text)
|
||||
end
|
||||
|
||||
if test_case.signal then
|
||||
tab = tab .. string.format(' [%s]', test_case.signal)
|
||||
end
|
||||
|
||||
table.insert(tab_lines, tab)
|
||||
end
|
||||
|
||||
local current_test = test_state.test_cases[test_state.current_index]
|
||||
if current_test then
|
||||
table.insert(tab_lines, '')
|
||||
table.insert(tab_lines, 'Input:')
|
||||
for _, line in ipairs(vim.split(current_test.input, '\n', { plain = true, trimempty = true })) do
|
||||
table.insert(tab_lines, line)
|
||||
end
|
||||
end
|
||||
|
||||
return tab_lines
|
||||
end
|
||||
|
||||
local function update_expected_pane()
|
||||
local test_state = test_module.get_test_panel_state()
|
||||
local current_test = test_state.test_cases[test_state.current_index]
|
||||
|
||||
if not current_test then
|
||||
return
|
||||
end
|
||||
|
||||
local expected_text = current_test.expected
|
||||
local expected_lines = vim.split(expected_text, '\n', { plain = true, trimempty = true })
|
||||
|
||||
vim.api.nvim_buf_set_lines(test_buffers.expected_buf, 0, -1, false, expected_lines)
|
||||
|
||||
if vim.fn.has('nvim-0.8.0') == 1 then
|
||||
vim.api.nvim_set_option_value('winbar', 'Expected', { win = test_windows.expected_win })
|
||||
end
|
||||
end
|
||||
|
||||
local function update_actual_pane()
|
||||
local test_state = test_module.get_test_panel_state()
|
||||
local current_test = test_state.test_cases[test_state.current_index]
|
||||
|
||||
if not current_test then
|
||||
return
|
||||
end
|
||||
|
||||
local actual_lines = {}
|
||||
local enable_diff = false
|
||||
|
||||
if current_test.actual then
|
||||
actual_lines = vim.split(current_test.actual, '\n', { plain = true, trimempty = true })
|
||||
enable_diff = current_test.status == 'fail'
|
||||
else
|
||||
actual_lines = { '(not run yet)' }
|
||||
end
|
||||
|
||||
vim.api.nvim_buf_set_lines(test_buffers.actual_buf, 0, -1, false, actual_lines)
|
||||
|
||||
if vim.fn.has('nvim-0.8.0') == 1 then
|
||||
vim.api.nvim_set_option_value('winbar', 'Actual', { win = test_windows.actual_win })
|
||||
end
|
||||
|
||||
vim.api.nvim_set_option_value('diff', enable_diff, { win = test_windows.expected_win })
|
||||
vim.api.nvim_set_option_value('diff', enable_diff, { win = test_windows.actual_win })
|
||||
|
||||
if enable_diff then
|
||||
vim.api.nvim_win_call(test_windows.expected_win, function()
|
||||
vim.cmd.diffthis()
|
||||
end)
|
||||
vim.api.nvim_win_call(test_windows.actual_win, function()
|
||||
vim.cmd.diffthis()
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
local function refresh_test_panel()
|
||||
if not test_buffers.tab_buf or not vim.api.nvim_buf_is_valid(test_buffers.tab_buf) then
|
||||
return
|
||||
end
|
||||
|
||||
local tab_lines = render_test_tabs()
|
||||
vim.api.nvim_buf_set_lines(test_buffers.tab_buf, 0, -1, false, tab_lines)
|
||||
|
||||
update_expected_pane()
|
||||
update_actual_pane()
|
||||
end
|
||||
|
||||
local function navigate_test_case(delta)
|
||||
local test_state = test_module.get_test_panel_state()
|
||||
if #test_state.test_cases == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
test_state.current_index = test_state.current_index + delta
|
||||
if test_state.current_index < 1 then
|
||||
test_state.current_index = #test_state.test_cases
|
||||
elseif test_state.current_index > #test_state.test_cases then
|
||||
test_state.current_index = 1
|
||||
end
|
||||
|
||||
refresh_test_panel()
|
||||
end
|
||||
|
||||
vim.keymap.set('n', '<c-n>', function()
|
||||
navigate_test_case(1)
|
||||
end, { buffer = test_buffers.tab_buf, silent = true })
|
||||
vim.keymap.set('n', '<c-p>', function()
|
||||
navigate_test_case(-1)
|
||||
end, { buffer = test_buffers.tab_buf, silent = true })
|
||||
|
||||
for _, buf in pairs(test_buffers) do
|
||||
vim.keymap.set('n', 'q', function()
|
||||
toggle_test_panel()
|
||||
end, { buffer = buf, silent = true })
|
||||
end
|
||||
|
||||
if is_debug and config.hooks and config.hooks.before_debug then
|
||||
config.hooks.before_debug(ctx)
|
||||
end
|
||||
|
||||
local execute_module = require('cp.execute')
|
||||
local contest_config = config.contests[state.platform]
|
||||
if execute_module.compile_problem(ctx, contest_config, is_debug) then
|
||||
test_module.run_all_test_cases(ctx, contest_config)
|
||||
end
|
||||
|
||||
refresh_test_panel()
|
||||
|
||||
vim.api.nvim_set_current_win(test_windows.tab_win)
|
||||
|
||||
state.test_panel_active = true
|
||||
state.test_buffers = test_buffers
|
||||
state.test_windows = test_windows
|
||||
local test_state = test_module.get_test_panel_state()
|
||||
logger.log(string.format('test panel opened (%d test cases)', #test_state.test_cases))
|
||||
end
|
||||
|
||||
---@param delta number 1 for next, -1 for prev
|
||||
---@param language? string
|
||||
local function navigate_problem(delta, language)
|
||||
if not state.platform or not state.contest_id then
|
||||
logger.log('no contest set. run :CP <platform> <contest> first', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
cache.load()
|
||||
local contest_data = cache.get_contest_data(state.platform, state.contest_id)
|
||||
if not contest_data or not contest_data.problems then
|
||||
logger.log(
|
||||
'no contest metadata found. set up a problem first to cache contest data',
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
local problems = contest_data.problems
|
||||
local current_problem_id
|
||||
|
||||
if state.platform == 'cses' then
|
||||
current_problem_id = state.contest_id
|
||||
else
|
||||
current_problem_id = state.problem_id
|
||||
end
|
||||
|
||||
if not current_problem_id then
|
||||
logger.log('no current problem set', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local current_index = nil
|
||||
for i, prob in ipairs(problems) do
|
||||
if prob.id == current_problem_id then
|
||||
current_index = i
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not current_index then
|
||||
logger.log('current problem not found in contest', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local new_index = current_index + delta
|
||||
|
||||
if new_index < 1 or new_index > #problems then
|
||||
local direction = delta > 0 and 'next' or 'previous'
|
||||
logger.log(('no %s problem available'):format(direction), vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
local new_problem = problems[new_index]
|
||||
|
||||
if state.platform == 'cses' then
|
||||
setup_problem(new_problem.id, nil, language)
|
||||
else
|
||||
setup_problem(state.contest_id, new_problem.id, language)
|
||||
end
|
||||
end
|
||||
|
||||
local function parse_command(args)
|
||||
if #args == 0 then
|
||||
return {
|
||||
type = 'error',
|
||||
message = 'Usage: :CP <platform> <contest> [problem] [--lang=<language>] | :CP <action> | :CP <problem>',
|
||||
}
|
||||
end
|
||||
|
||||
local language = nil
|
||||
local debug = false
|
||||
|
||||
for i, arg in ipairs(args) do
|
||||
local lang_match = arg:match('^--lang=(.+)$')
|
||||
if lang_match then
|
||||
language = lang_match
|
||||
elseif arg == '--lang' then
|
||||
if i + 1 <= #args then
|
||||
language = args[i + 1]
|
||||
else
|
||||
return { type = 'error', message = '--lang requires a value' }
|
||||
end
|
||||
elseif arg == '--debug' then
|
||||
debug = true
|
||||
end
|
||||
end
|
||||
|
||||
local filtered_args = vim.tbl_filter(function(arg)
|
||||
return not (arg:match('^--lang') or arg == language or arg == '--debug')
|
||||
end, args)
|
||||
|
||||
local first = filtered_args[1]
|
||||
|
||||
if vim.tbl_contains(actions, first) then
|
||||
return { type = 'action', action = first, language = language, debug = debug }
|
||||
end
|
||||
|
||||
if vim.tbl_contains(platforms, first) then
|
||||
if #filtered_args == 1 then
|
||||
return {
|
||||
type = 'platform_only',
|
||||
platform = first,
|
||||
language = language,
|
||||
}
|
||||
elseif #filtered_args == 2 then
|
||||
if first == 'cses' then
|
||||
return {
|
||||
type = 'cses_problem',
|
||||
platform = first,
|
||||
problem = filtered_args[2],
|
||||
language = language,
|
||||
}
|
||||
else
|
||||
return {
|
||||
type = 'contest_setup',
|
||||
platform = first,
|
||||
contest = filtered_args[2],
|
||||
language = language,
|
||||
}
|
||||
end
|
||||
elseif #filtered_args == 3 then
|
||||
return {
|
||||
type = 'full_setup',
|
||||
platform = first,
|
||||
contest = filtered_args[2],
|
||||
problem = filtered_args[3],
|
||||
language = language,
|
||||
}
|
||||
else
|
||||
return { type = 'error', message = 'Too many arguments' }
|
||||
end
|
||||
end
|
||||
|
||||
if state.platform and state.contest_id then
|
||||
cache.load()
|
||||
local contest_data = cache.get_contest_data(state.platform, state.contest_id)
|
||||
if contest_data and contest_data.problems then
|
||||
local problem_ids = vim.tbl_map(function(prob)
|
||||
return prob.id
|
||||
end, contest_data.problems)
|
||||
if vim.tbl_contains(problem_ids, first) then
|
||||
return { type = 'problem_switch', problem = first, language = language }
|
||||
end
|
||||
end
|
||||
return {
|
||||
type = 'error',
|
||||
message = ("invalid subcommand '%s'"):format(first),
|
||||
}
|
||||
end
|
||||
|
||||
return { type = 'error', message = 'Unknown command or no contest context' }
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.handle_command(opts)
|
||||
local cmd = parse_command(opts.fargs)
|
||||
|
||||
if cmd.type == 'error' then
|
||||
logger.log(cmd.message, vim.log.levels.ERROR)
|
||||
if not ensure_initialized() then
|
||||
return
|
||||
end
|
||||
|
||||
if cmd.type == 'action' then
|
||||
if cmd.action == 'test' then
|
||||
toggle_test_panel(cmd.debug)
|
||||
elseif cmd.action == 'next' then
|
||||
navigate_problem(1, cmd.language)
|
||||
elseif cmd.action == 'prev' then
|
||||
navigate_problem(-1, cmd.language)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if cmd.type == 'platform_only' then
|
||||
set_platform(cmd.platform)
|
||||
return
|
||||
end
|
||||
|
||||
if cmd.type == 'contest_setup' then
|
||||
if set_platform(cmd.platform) then
|
||||
state.contest_id = cmd.contest
|
||||
if vim.tbl_contains(config.scrapers, cmd.platform) then
|
||||
local metadata_result = scrape.scrape_contest_metadata(cmd.platform, cmd.contest)
|
||||
if not metadata_result.success then
|
||||
logger.log(
|
||||
'failed to load contest metadata: ' .. (metadata_result.error or 'unknown error'),
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
else
|
||||
logger.log(
|
||||
('loaded %d problems for %s %s'):format(
|
||||
#metadata_result.problems,
|
||||
cmd.platform,
|
||||
cmd.contest
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if cmd.type == 'full_setup' then
|
||||
if set_platform(cmd.platform) then
|
||||
state.contest_id = cmd.contest
|
||||
local problem_ids = {}
|
||||
local has_metadata = false
|
||||
|
||||
if vim.tbl_contains(config.scrapers, cmd.platform) then
|
||||
local metadata_result = scrape.scrape_contest_metadata(cmd.platform, cmd.contest)
|
||||
if not metadata_result.success then
|
||||
logger.log(
|
||||
'failed to load contest metadata: ' .. (metadata_result.error or 'unknown error'),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
logger.log(
|
||||
('loaded %d problems for %s %s'):format(
|
||||
#metadata_result.problems,
|
||||
cmd.platform,
|
||||
cmd.contest
|
||||
)
|
||||
)
|
||||
problem_ids = vim.tbl_map(function(prob)
|
||||
return prob.id
|
||||
end, metadata_result.problems)
|
||||
has_metadata = true
|
||||
else
|
||||
cache.load()
|
||||
local contest_data = cache.get_contest_data(cmd.platform, cmd.contest)
|
||||
if contest_data and contest_data.problems then
|
||||
problem_ids = vim.tbl_map(function(prob)
|
||||
return prob.id
|
||||
end, contest_data.problems)
|
||||
has_metadata = true
|
||||
end
|
||||
end
|
||||
|
||||
if has_metadata and not vim.tbl_contains(problem_ids, cmd.problem) then
|
||||
logger.log(
|
||||
("Invalid problem '%s' for contest %s %s"):format(cmd.problem, cmd.platform, cmd.contest),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
setup_problem(cmd.contest, cmd.problem, cmd.language)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if cmd.type == 'cses_problem' then
|
||||
if set_platform(cmd.platform) then
|
||||
if vim.tbl_contains(config.scrapers, cmd.platform) then
|
||||
local metadata_result = scrape.scrape_contest_metadata(cmd.platform, '')
|
||||
if not metadata_result.success then
|
||||
logger.log(
|
||||
'failed to load contest metadata: ' .. (metadata_result.error or 'unknown error'),
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
end
|
||||
end
|
||||
setup_problem(cmd.problem, nil, cmd.language)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if cmd.type == 'problem_switch' then
|
||||
if state.platform == 'cses' then
|
||||
setup_problem(cmd.problem, nil, cmd.language)
|
||||
else
|
||||
setup_problem(state.contest_id, cmd.problem, cmd.language)
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
function M.setup(opts)
|
||||
opts = opts or {}
|
||||
user_config = opts
|
||||
config = config_module.setup(user_config)
|
||||
logger.set_config(config)
|
||||
if not snippets_initialized then
|
||||
snippets.setup(config)
|
||||
snippets_initialized = true
|
||||
end
|
||||
end
|
||||
|
||||
function M.get_current_context()
|
||||
return {
|
||||
platform = state.platform,
|
||||
contest_id = state.contest_id,
|
||||
problem_id = state.problem_id,
|
||||
}
|
||||
local commands = require('cp.commands')
|
||||
commands.handle_command(opts)
|
||||
end
|
||||
|
||||
function M.is_initialized()
|
||||
return true
|
||||
return initialized
|
||||
end
|
||||
|
||||
---@deprecated Use `vim.g.cp` instead
|
||||
function M.setup(user_config)
|
||||
vim.deprecate('require("cp").setup()', 'vim.g.cp', 'v0.7.7', 'cp.nvim', false)
|
||||
|
||||
if user_config then
|
||||
vim.g.cp = vim.tbl_deep_extend('force', vim.g.cp or {}, user_config)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
local M = {}
|
||||
|
||||
local config = nil
|
||||
|
||||
function M.set_config(user_config)
|
||||
config = user_config
|
||||
end
|
||||
|
||||
function M.log(msg, level)
|
||||
function M.log(msg, level, override)
|
||||
local debug = require('cp.config').get_config().debug or false
|
||||
level = level or vim.log.levels.INFO
|
||||
if not config or config.debug or level >= vim.log.levels.WARN then
|
||||
vim.notify(('[cp.nvim]: %s'):format(msg), level)
|
||||
if level >= vim.log.levels.WARN or override or debug then
|
||||
vim.schedule(function()
|
||||
vim.notify(('[cp.nvim]: %s'):format(msg), level)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
92
lua/cp/pickers/fzf_lua.lua
Normal file
92
lua/cp/pickers/fzf_lua.lua
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
local picker_utils = require('cp.pickers')
|
||||
|
||||
local M = {}
|
||||
|
||||
local function contest_picker(platform, refresh, language)
|
||||
local constants = require('cp.constants')
|
||||
local platform_display_name = constants.PLATFORM_DISPLAY_NAMES[platform]
|
||||
local fzf = require('fzf-lua')
|
||||
local contests = picker_utils.get_platform_contests(platform, refresh)
|
||||
|
||||
if vim.tbl_isempty(contests) then
|
||||
vim.notify(
|
||||
("No contests found for platform '%s'"):format(platform_display_name),
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
local entries = vim.tbl_map(function(contest)
|
||||
return contest.display_name
|
||||
end, contests)
|
||||
|
||||
return fzf.fzf_exec(entries, {
|
||||
prompt = ('Select Contest (%s)> '):format(platform_display_name),
|
||||
fzf_opts = {
|
||||
['--header'] = 'ctrl-r: refresh',
|
||||
},
|
||||
actions = {
|
||||
['default'] = function(selected)
|
||||
if vim.tbl_isempty(selected) then
|
||||
return
|
||||
end
|
||||
|
||||
local selected_name = selected[1]
|
||||
local contest = nil
|
||||
for _, c in ipairs(contests) do
|
||||
if c.display_name == selected_name then
|
||||
contest = c
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if contest then
|
||||
local cp = require('cp')
|
||||
local fargs = { platform, contest.id }
|
||||
if language then
|
||||
table.insert(fargs, '--lang')
|
||||
table.insert(fargs, language)
|
||||
end
|
||||
cp.handle_command({ fargs = fargs })
|
||||
end
|
||||
end,
|
||||
['ctrl-r'] = function()
|
||||
contest_picker(platform, true, language)
|
||||
end,
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
function M.pick(language)
|
||||
local fzf = require('fzf-lua')
|
||||
local platforms = picker_utils.get_platforms()
|
||||
local entries = vim.tbl_map(function(platform)
|
||||
return platform.display_name
|
||||
end, platforms)
|
||||
|
||||
return fzf.fzf_exec(entries, {
|
||||
prompt = 'Select Platform> ',
|
||||
actions = {
|
||||
['default'] = function(selected)
|
||||
if vim.tbl_isempty(selected) then
|
||||
return
|
||||
end
|
||||
|
||||
local selected_name = selected[1]
|
||||
local platform = nil
|
||||
for _, p in ipairs(platforms) do
|
||||
if p.display_name == selected_name then
|
||||
platform = p
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if platform then
|
||||
contest_picker(platform.id, false, language)
|
||||
end
|
||||
end,
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
68
lua/cp/pickers/init.lua
Normal file
68
lua/cp/pickers/init.lua
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
local M = {}
|
||||
|
||||
local cache = require('cp.cache')
|
||||
local constants = require('cp.constants')
|
||||
local logger = require('cp.log')
|
||||
local scraper = require('cp.scraper')
|
||||
|
||||
---@class cp.PlatformItem
|
||||
---@field id string Platform identifier (e.g. "codeforces", "atcoder", "cses")
|
||||
---@field display_name string Human-readable platform name (e.g. "Codeforces", "AtCoder", "CSES")
|
||||
|
||||
---@class cp.ContestItem
|
||||
---@field id string Contest identifier (e.g. "1951", "abc324", "sorting")
|
||||
---@field name string Full contest name (e.g. "Educational Codeforces Round 168")
|
||||
---@field display_name string Formatted display name for picker
|
||||
|
||||
---@class cp.ProblemItem
|
||||
---@field id string Problem identifier (e.g. "a", "b", "c")
|
||||
---@field name string Problem name (e.g. "Two Permutations", "Painting Walls")
|
||||
---@field display_name string Formatted display name for picker
|
||||
|
||||
---@return cp.PlatformItem[]
|
||||
function M.get_platforms()
|
||||
local config = require('cp.config').get_config()
|
||||
local result = {}
|
||||
for _, platform in ipairs(constants.PLATFORMS) do
|
||||
if config.platforms[platform] then
|
||||
table.insert(result, {
|
||||
id = platform,
|
||||
display_name = constants.PLATFORM_DISPLAY_NAMES[platform] or platform,
|
||||
})
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@param refresh? boolean
|
||||
---@return cp.ContestItem[]
|
||||
function M.get_platform_contests(platform, refresh)
|
||||
cache.load()
|
||||
local picker_contests = cache.get_contest_summaries(platform)
|
||||
|
||||
if refresh or vim.tbl_isempty(picker_contests) then
|
||||
logger.log(
|
||||
('Loading %s contests...'):format(constants.PLATFORM_DISPLAY_NAMES[platform]),
|
||||
vim.log.levels.INFO,
|
||||
true
|
||||
)
|
||||
|
||||
local contests = scraper.scrape_contest_list(platform)
|
||||
cache.set_contest_summaries(platform, contests)
|
||||
picker_contests = cache.get_contest_summaries(platform)
|
||||
|
||||
logger.log(
|
||||
('Loaded %d %s contests.'):format(
|
||||
#picker_contests,
|
||||
constants.PLATFORM_DISPLAY_NAMES[platform]
|
||||
),
|
||||
vim.log.levels.INFO,
|
||||
true
|
||||
)
|
||||
end
|
||||
|
||||
return picker_contests
|
||||
end
|
||||
|
||||
return M
|
||||
99
lua/cp/pickers/telescope.lua
Normal file
99
lua/cp/pickers/telescope.lua
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
local finders = require('telescope.finders')
|
||||
local pickers = require('telescope.pickers')
|
||||
local conf = require('telescope.config').values
|
||||
local action_state = require('telescope.actions.state')
|
||||
local actions = require('telescope.actions')
|
||||
|
||||
local picker_utils = require('cp.pickers')
|
||||
|
||||
local M = {}
|
||||
|
||||
local function contest_picker(opts, platform, refresh, language)
|
||||
local constants = require('cp.constants')
|
||||
local platform_display_name = constants.PLATFORM_DISPLAY_NAMES[platform]
|
||||
local contests = picker_utils.get_platform_contests(platform, refresh)
|
||||
|
||||
if vim.tbl_isempty(contests) then
|
||||
vim.notify(
|
||||
('No contests found for platform: %s'):format(platform_display_name),
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
pickers
|
||||
.new(opts, {
|
||||
prompt_title = ('Select Contest (%s)'):format(platform_display_name),
|
||||
results_title = '<c-r> refresh',
|
||||
finder = finders.new_table({
|
||||
results = contests,
|
||||
entry_maker = function(entry)
|
||||
return {
|
||||
value = entry,
|
||||
display = entry.display_name,
|
||||
ordinal = entry.display_name,
|
||||
}
|
||||
end,
|
||||
}),
|
||||
sorter = conf.generic_sorter(opts),
|
||||
attach_mappings = function(prompt_bufnr, map)
|
||||
actions.select_default:replace(function()
|
||||
local selection = action_state.get_selected_entry()
|
||||
actions.close(prompt_bufnr)
|
||||
|
||||
if selection then
|
||||
local cp = require('cp')
|
||||
local fargs = { platform, selection.value.id }
|
||||
if language then
|
||||
table.insert(fargs, '--lang')
|
||||
table.insert(fargs, language)
|
||||
end
|
||||
cp.handle_command({ fargs = fargs })
|
||||
end
|
||||
end)
|
||||
|
||||
map('i', '<c-r>', function()
|
||||
actions.close(prompt_bufnr)
|
||||
contest_picker(opts, platform, true, language)
|
||||
end)
|
||||
|
||||
return true
|
||||
end,
|
||||
})
|
||||
:find()
|
||||
end
|
||||
|
||||
function M.pick(language)
|
||||
local opts = {}
|
||||
local platforms = picker_utils.get_platforms()
|
||||
|
||||
pickers
|
||||
.new(opts, {
|
||||
prompt_title = 'Select Platform',
|
||||
finder = finders.new_table({
|
||||
results = platforms,
|
||||
entry_maker = function(entry)
|
||||
return {
|
||||
value = entry,
|
||||
display = entry.display_name,
|
||||
ordinal = entry.display_name,
|
||||
}
|
||||
end,
|
||||
}),
|
||||
sorter = conf.generic_sorter(opts),
|
||||
attach_mappings = function(prompt_bufnr)
|
||||
actions.select_default:replace(function()
|
||||
local selection = action_state.get_selected_entry()
|
||||
actions.close(prompt_bufnr)
|
||||
|
||||
if selection then
|
||||
contest_picker(opts, selection.value.id, false, language)
|
||||
end
|
||||
end)
|
||||
return true
|
||||
end,
|
||||
})
|
||||
:find()
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
---@class ProblemContext
|
||||
---@field contest string Contest name (e.g. "atcoder", "codeforces")
|
||||
---@field contest_id string Contest ID (e.g. "abc123", "1933")
|
||||
---@field problem_id? string Problem ID for AtCoder/Codeforces (e.g. "a", "b")
|
||||
---@field source_file string Source filename (e.g. "abc123a.cpp")
|
||||
---@field binary_file string Binary output path (e.g. "build/abc123a.run")
|
||||
---@field input_file string Input test file path (e.g. "io/abc123a.in")
|
||||
---@field output_file string Output file path (e.g. "io/abc123a.out")
|
||||
---@field expected_file string Expected output path (e.g. "io/abc123a.expected")
|
||||
---@field problem_name string Canonical problem identifier (e.g. "abc123a")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param contest string
|
||||
---@param contest_id string
|
||||
---@param problem_id? string
|
||||
---@param config cp.Config
|
||||
---@param language? string
|
||||
---@return ProblemContext
|
||||
function M.create_context(contest, contest_id, problem_id, config, language)
|
||||
vim.validate({
|
||||
contest = { contest, 'string' },
|
||||
contest_id = { contest_id, 'string' },
|
||||
problem_id = { problem_id, { 'string', 'nil' }, true },
|
||||
config = { config, 'table' },
|
||||
language = { language, { 'string', 'nil' }, true },
|
||||
})
|
||||
|
||||
local contest_config = config.contests[contest]
|
||||
if not contest_config then
|
||||
error(("No contest config found for '%s'"):format(contest))
|
||||
end
|
||||
|
||||
local target_language = language or contest_config.default_language
|
||||
local language_config = contest_config[target_language]
|
||||
if not language_config then
|
||||
error(("No language config found for '%s' in contest '%s'"):format(target_language, contest))
|
||||
end
|
||||
if not language_config.extension then
|
||||
error(
|
||||
("No extension configured for language '%s' in contest '%s'"):format(target_language, contest)
|
||||
)
|
||||
end
|
||||
|
||||
local base_name
|
||||
if config.filename then
|
||||
local source_file = config.filename(contest, contest_id, problem_id, config, language)
|
||||
base_name = vim.fn.fnamemodify(source_file, ':t:r')
|
||||
else
|
||||
local default_filename = require('cp.config').default_filename
|
||||
base_name = default_filename(contest_id, problem_id)
|
||||
end
|
||||
|
||||
local source_file = base_name .. '.' .. language_config.extension
|
||||
|
||||
return {
|
||||
contest = contest,
|
||||
contest_id = contest_id,
|
||||
problem_id = problem_id,
|
||||
source_file = source_file,
|
||||
binary_file = ('build/%s.run'):format(base_name),
|
||||
input_file = ('io/%s.cpin'):format(base_name),
|
||||
output_file = ('io/%s.cpout'):format(base_name),
|
||||
expected_file = ('io/%s.expected'):format(base_name),
|
||||
problem_name = base_name,
|
||||
}
|
||||
end
|
||||
|
||||
return M
|
||||
30
lua/cp/restore.lua
Normal file
30
lua/cp/restore.lua
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
local M = {}
|
||||
|
||||
local cache = require('cp.cache')
|
||||
local logger = require('cp.log')
|
||||
local state = require('cp.state')
|
||||
|
||||
---@return boolean
|
||||
function M.restore_from_current_file()
|
||||
cache.load()
|
||||
|
||||
local current_file = (vim.uv.fs_realpath(vim.fn.expand('%:p')) or vim.fn.expand('%:p'))
|
||||
local file_state = cache.get_file_state(current_file)
|
||||
if not file_state then
|
||||
logger.log('No cached state found for current file.', vim.log.levels.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
local setup = require('cp.setup')
|
||||
state.set_problem_id(file_state.problem_id)
|
||||
setup.setup_contest(
|
||||
file_state.platform,
|
||||
file_state.contest_id,
|
||||
file_state.problem_id,
|
||||
file_state.language
|
||||
)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
return M
|
||||
213
lua/cp/runner/execute.lua
Normal file
213
lua/cp/runner/execute.lua
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
---@class ExecuteResult
|
||||
---@field stdout string
|
||||
---@field code integer
|
||||
---@field time_ms number
|
||||
---@field tled boolean
|
||||
---@field mled boolean
|
||||
---@field peak_mb number
|
||||
---@field signal string|nil
|
||||
|
||||
---@class SubstitutableCommand
|
||||
---@field source string substituted via '{source}'
|
||||
---@field binary string substitued via '{binary}'
|
||||
|
||||
local M = {}
|
||||
local constants = require('cp.constants')
|
||||
local logger = require('cp.log')
|
||||
local utils = require('cp.utils')
|
||||
|
||||
---@param cmd_template string[]
|
||||
---@param substitutions SubstitutableCommand
|
||||
---@return string[] string normalized with substitutions
|
||||
local function substitute_template(cmd_template, substitutions)
|
||||
local out = {}
|
||||
for _, arg in ipairs(cmd_template) do
|
||||
if arg == '{source}' and substitutions.source then
|
||||
table.insert(out, substitutions.source)
|
||||
elseif arg == '{binary}' and substitutions.binary then
|
||||
table.insert(out, substitutions.binary)
|
||||
else
|
||||
table.insert(out, arg)
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function M.build_command(cmd_template, substitutions)
|
||||
return substitute_template(cmd_template, substitutions)
|
||||
end
|
||||
|
||||
---@param compile_cmd string[]
|
||||
---@param substitutions SubstitutableCommand
|
||||
---@param on_complete fun(r: {code: integer, stdout: string})
|
||||
function M.compile(compile_cmd, substitutions, on_complete)
|
||||
local cmd = substitute_template(compile_cmd, substitutions)
|
||||
local sh = table.concat(cmd, ' ') .. ' 2>&1'
|
||||
logger.log('compile: ' .. sh)
|
||||
|
||||
local t0 = vim.uv.hrtime()
|
||||
vim.system({ 'sh', '-c', sh }, { text = false }, function(r)
|
||||
local dt = (vim.uv.hrtime() - t0) / 1e6
|
||||
local ansi = require('cp.ui.ansi')
|
||||
r.stdout = ansi.bytes_to_string(r.stdout or '')
|
||||
|
||||
if r.code == 0 then
|
||||
logger.log(('Compilation successful in %.1fms.'):format(dt), vim.log.levels.INFO)
|
||||
else
|
||||
logger.log(('Compilation failed in %.1fms.'):format(dt))
|
||||
end
|
||||
|
||||
vim.schedule(function()
|
||||
on_complete(r)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
local function parse_and_strip_time_v(output)
|
||||
local s = output or ''
|
||||
local last_i, from = nil, 1
|
||||
while true do
|
||||
local i = string.find(s, 'Command being timed:', from, true)
|
||||
if not i then
|
||||
break
|
||||
end
|
||||
last_i, from = i, i + 1
|
||||
end
|
||||
if not last_i then
|
||||
return s, 0
|
||||
end
|
||||
|
||||
local tab_before_marker = s:find('\t[^\t]*Command being timed:', 1)
|
||||
local k
|
||||
if tab_before_marker then
|
||||
k = tab_before_marker - 1
|
||||
else
|
||||
k = last_i - 1
|
||||
while k >= 1 do
|
||||
local ch = s:sub(k, k)
|
||||
if ch == '\n' then
|
||||
break
|
||||
end
|
||||
k = k - 1
|
||||
end
|
||||
end
|
||||
|
||||
local head = s:sub(1, k)
|
||||
local tail = s:sub(last_i)
|
||||
|
||||
local peak_kb = 0.0
|
||||
for line in tail:gmatch('[^\n]+') do
|
||||
local kb = line:match('Maximum resident set size %(kbytes%):%s*(%d+)')
|
||||
if kb then
|
||||
peak_kb = tonumber(kb) or 0
|
||||
end
|
||||
end
|
||||
|
||||
local peak_mb = peak_kb / 1024.0
|
||||
return head, peak_mb
|
||||
end
|
||||
|
||||
---@param on_complete fun(result: ExecuteResult)
|
||||
function M.run(cmd, stdin, timeout_ms, memory_mb, on_complete)
|
||||
local time_bin = utils.time_path()
|
||||
local timeout_bin = utils.timeout_path()
|
||||
|
||||
local prog = table.concat(cmd, ' ')
|
||||
local pre = {
|
||||
('ulimit -v %d'):format(memory_mb * 1024),
|
||||
}
|
||||
local prefix = table.concat(pre, '; ') .. '; '
|
||||
local sec = math.ceil(timeout_ms / 1000)
|
||||
local timeout_prefix = ('%s -k 1s %ds '):format(timeout_bin, sec)
|
||||
local sh = prefix .. timeout_prefix .. ('%s -v sh -c %q 2>&1'):format(time_bin, prog)
|
||||
logger.log('run: ' .. sh)
|
||||
|
||||
local t0 = vim.uv.hrtime()
|
||||
vim.system({ 'sh', '-c', sh }, { stdin = stdin, text = true }, function(r)
|
||||
local dt = (vim.uv.hrtime() - t0) / 1e6
|
||||
|
||||
local code = r.code or 0
|
||||
local raw = r.stdout or ''
|
||||
local cleaned, peak_mb = parse_and_strip_time_v(raw)
|
||||
local tled = code == 124
|
||||
|
||||
local signal = nil
|
||||
if code >= 128 then
|
||||
signal = constants.signal_codes[code]
|
||||
end
|
||||
|
||||
local lower = (cleaned or ''):lower()
|
||||
local oom_hint = lower:find('std::bad_alloc', 1, true)
|
||||
or lower:find('cannot allocate memory', 1, true)
|
||||
or lower:find('out of memory', 1, true)
|
||||
or lower:find('oom', 1, true)
|
||||
or lower:find('enomem', 1, true)
|
||||
local near_cap = peak_mb >= (0.90 * memory_mb)
|
||||
|
||||
local mled = (peak_mb >= memory_mb) or near_cap or (oom_hint ~= nil and not tled)
|
||||
|
||||
if tled then
|
||||
logger.log(('Execution timed out in %.1fms.'):format(dt))
|
||||
elseif mled then
|
||||
logger.log(('Execution memory limit exceeded in %.1fms.'):format(dt))
|
||||
elseif code ~= 0 then
|
||||
logger.log(('Execution failed in %.1fms (exit code %d).'):format(dt, code))
|
||||
else
|
||||
logger.log(('Execution successful in %.1fms.'):format(dt))
|
||||
end
|
||||
|
||||
vim.schedule(function()
|
||||
on_complete({
|
||||
stdout = cleaned,
|
||||
code = code,
|
||||
time_ms = dt,
|
||||
tled = tled,
|
||||
mled = mled,
|
||||
peak_mb = peak_mb,
|
||||
signal = signal,
|
||||
})
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
---@param debug boolean?
|
||||
---@param on_complete fun(result: {success: boolean, output: string?})
|
||||
function M.compile_problem(debug, on_complete)
|
||||
local state = require('cp.state')
|
||||
local config = require('cp.config').get_config()
|
||||
local platform = state.get_platform()
|
||||
local language = state.get_language() or config.platforms[platform].default_language
|
||||
local eff = config.runtime.effective[platform][language]
|
||||
|
||||
local source_file = state.get_source_file()
|
||||
if source_file then
|
||||
local buf = vim.fn.bufnr(source_file)
|
||||
if buf ~= -1 and vim.api.nvim_buf_is_loaded(buf) and vim.bo[buf].modified then
|
||||
vim.api.nvim_buf_call(buf, function()
|
||||
vim.cmd.write({ mods = { silent = true, noautocmd = true } })
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
local compile_config = (debug and eff.commands.debug) or eff.commands.build
|
||||
|
||||
if not compile_config then
|
||||
on_complete({ success = true, output = nil })
|
||||
return
|
||||
end
|
||||
|
||||
require('cp.utils').ensure_dirs()
|
||||
|
||||
local binary = debug and state.get_debug_file() or state.get_binary_file()
|
||||
local substitutions = { source = state.get_source_file(), binary = binary }
|
||||
|
||||
M.compile(compile_config, substitutions, function(r)
|
||||
if r.code ~= 0 then
|
||||
on_complete({ success = false, output = r.stdout or 'unknown error' })
|
||||
else
|
||||
on_complete({ success = true, output = nil })
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
338
lua/cp/runner/run.lua
Normal file
338
lua/cp/runner/run.lua
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
---@class RanTestCase
|
||||
---@field index number
|
||||
---@field input string
|
||||
---@field expected string
|
||||
---@field status "pending"|"pass"|"fail"|"running"|"tle"|"mle"
|
||||
---@field actual string?
|
||||
---@field actual_highlights? Highlight[]
|
||||
---@field time_ms number?
|
||||
---@field error string?
|
||||
---@field stderr string?
|
||||
---@field selected boolean
|
||||
---@field code number?
|
||||
---@field ok boolean?
|
||||
---@field signal string?
|
||||
---@field tled boolean?
|
||||
---@field mled boolean?
|
||||
---@field rss_mb number
|
||||
|
||||
---@class ProblemConstraints
|
||||
---@field timeout_ms number
|
||||
---@field memory_mb number
|
||||
|
||||
---@class PanelState
|
||||
---@field test_cases RanTestCase[]
|
||||
---@field current_index number
|
||||
---@field buffer number?
|
||||
---@field namespace number?
|
||||
---@field is_active boolean
|
||||
---@field saved_layout table?
|
||||
---@field constraints ProblemConstraints?
|
||||
|
||||
local M = {}
|
||||
local cache = require('cp.cache')
|
||||
local config = require('cp.config').get_config()
|
||||
local constants = require('cp.constants')
|
||||
local execute = require('cp.runner.execute')
|
||||
local logger = require('cp.log')
|
||||
local state = require('cp.state')
|
||||
|
||||
---@type PanelState
|
||||
local panel_state = {
|
||||
test_cases = {},
|
||||
current_index = 1,
|
||||
buffer = nil,
|
||||
namespace = nil,
|
||||
is_active = false,
|
||||
saved_layout = nil,
|
||||
constraints = nil,
|
||||
}
|
||||
|
||||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@param problem_id string|nil
|
||||
---@return ProblemConstraints|nil
|
||||
local function load_constraints_from_cache(platform, contest_id, problem_id)
|
||||
cache.load()
|
||||
local timeout_ms, memory_mb = cache.get_constraints(platform, contest_id, problem_id)
|
||||
if timeout_ms and memory_mb then
|
||||
return { timeout_ms = timeout_ms, memory_mb = memory_mb }
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Normalize raw problem output to a "canonical" version
|
||||
--- Usually, most contests ignore leading/trailing whitespace and empty lines
|
||||
---@param lines string
|
||||
local function normalize_lines(lines)
|
||||
local normalized = {}
|
||||
for _, line in
|
||||
ipairs(vim.tbl_values(vim.split(((lines or ''):gsub('\r', '')), '\n', { plain = true })))
|
||||
do
|
||||
local trimmed_line = vim.trim(line)
|
||||
if trimmed_line ~= '' then
|
||||
table.insert(normalized, trimmed_line)
|
||||
end
|
||||
end
|
||||
return table.concat(normalized, '\n')
|
||||
end
|
||||
|
||||
---@param test_cases TestCase[]
|
||||
---@return RanTestCase[]
|
||||
local function create_sentinal_panel_data(test_cases)
|
||||
local out = {}
|
||||
for i, tc in ipairs(test_cases) do
|
||||
out[i] = {
|
||||
index = tc.index or i,
|
||||
input = tc.input or '',
|
||||
expected = tc.expected or '',
|
||||
status = 'pending',
|
||||
selected = false,
|
||||
}
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
---@param cmd string[]
|
||||
---@return string[]
|
||||
local function build_command(cmd, substitutions)
|
||||
return execute.build_command(cmd, substitutions)
|
||||
end
|
||||
|
||||
---@param test_case RanTestCase
|
||||
---@param debug boolean?
|
||||
---@param on_complete fun(result: { status: "pass"|"fail"|"tle"|"mle", actual: string, actual_highlights: Highlight[], error: string, stderr: string, time_ms: number, code: integer, ok: boolean, signal: string?, tled: boolean, mled: boolean, rss_mb: number })
|
||||
local function run_single_test_case(test_case, debug, on_complete)
|
||||
local source_file = state.get_source_file()
|
||||
|
||||
local binary_file = debug and state.get_debug_file() or state.get_binary_file()
|
||||
local substitutions = { source = source_file, binary = binary_file }
|
||||
|
||||
local platform_config = config.platforms[state.get_platform() or '']
|
||||
local language = state.get_language() or platform_config.default_language
|
||||
local eff = config.runtime.effective[state.get_platform() or ''][language]
|
||||
local run_template = eff and eff.commands and eff.commands.run or {}
|
||||
local cmd = build_command(run_template, substitutions)
|
||||
local stdin_content = (test_case.input or '') .. '\n'
|
||||
local timeout_ms = (panel_state.constraints and panel_state.constraints.timeout_ms) or 0
|
||||
local memory_mb = panel_state.constraints and panel_state.constraints.memory_mb or 0
|
||||
|
||||
execute.run(cmd, stdin_content, timeout_ms, memory_mb, function(r)
|
||||
local ansi = require('cp.ui.ansi')
|
||||
local out = r.stdout or ''
|
||||
local highlights = {}
|
||||
if out ~= '' then
|
||||
if config.ui.ansi then
|
||||
local parsed = ansi.parse_ansi_text(out)
|
||||
out = table.concat(parsed.lines, '\n')
|
||||
highlights = parsed.highlights
|
||||
else
|
||||
out = out:gsub('\027%[[%d;]*[a-zA-Z]', '')
|
||||
end
|
||||
end
|
||||
|
||||
local max_lines = config.ui.panel.max_output_lines
|
||||
local lines = vim.split(out, '\n')
|
||||
if #lines > max_lines then
|
||||
local trimmed = {}
|
||||
for i = 1, max_lines do
|
||||
table.insert(trimmed, lines[i])
|
||||
end
|
||||
table.insert(trimmed, string.format('... (output trimmed after %d lines)', max_lines))
|
||||
out = table.concat(trimmed, '\n')
|
||||
end
|
||||
|
||||
local expected = test_case.expected or ''
|
||||
local ok = normalize_lines(out) == normalize_lines(expected)
|
||||
|
||||
local signal = r.signal
|
||||
if not signal and r.code and r.code >= 128 then
|
||||
signal = constants.signal_codes[r.code]
|
||||
end
|
||||
|
||||
local status
|
||||
if r.tled then
|
||||
status = 'tle'
|
||||
elseif r.mled then
|
||||
status = 'mle'
|
||||
elseif ok then
|
||||
status = 'pass'
|
||||
else
|
||||
status = 'fail'
|
||||
end
|
||||
|
||||
on_complete({
|
||||
status = status,
|
||||
actual = out,
|
||||
actual_highlights = highlights,
|
||||
error = (r.code ~= 0 and not ok) and out or '',
|
||||
stderr = '',
|
||||
time_ms = r.time_ms,
|
||||
code = r.code,
|
||||
ok = ok,
|
||||
signal = signal,
|
||||
tled = r.tled or false,
|
||||
mled = r.mled or false,
|
||||
rss_mb = r.peak_mb or 0,
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function M.load_test_cases()
|
||||
local tcs = cache.get_test_cases(
|
||||
state.get_platform() or '',
|
||||
state.get_contest_id() or '',
|
||||
state.get_problem_id()
|
||||
)
|
||||
|
||||
panel_state.test_cases = create_sentinal_panel_data(tcs)
|
||||
panel_state.current_index = 1
|
||||
panel_state.constraints = load_constraints_from_cache(
|
||||
state.get_platform() or '',
|
||||
state.get_contest_id() or '',
|
||||
state.get_problem_id()
|
||||
)
|
||||
|
||||
logger.log(('Loaded %d test case(s)'):format(#tcs), vim.log.levels.INFO)
|
||||
return #tcs > 0
|
||||
end
|
||||
|
||||
---@param debug boolean?
|
||||
---@param on_complete fun(result: RanTestCase?)
|
||||
function M.run_combined_test(debug, on_complete)
|
||||
local combined = cache.get_combined_test(
|
||||
state.get_platform() or '',
|
||||
state.get_contest_id() or '',
|
||||
state.get_problem_id()
|
||||
)
|
||||
|
||||
if not combined then
|
||||
logger.log('No combined test found', vim.log.levels.ERROR)
|
||||
on_complete(nil)
|
||||
return
|
||||
end
|
||||
|
||||
local ran_test = {
|
||||
index = 1,
|
||||
input = combined.input,
|
||||
expected = combined.expected,
|
||||
status = 'running',
|
||||
actual = nil,
|
||||
time_ms = nil,
|
||||
code = nil,
|
||||
ok = nil,
|
||||
signal = nil,
|
||||
tled = false,
|
||||
mled = false,
|
||||
rss_mb = 0,
|
||||
selected = true,
|
||||
}
|
||||
|
||||
run_single_test_case(ran_test, debug, function(result)
|
||||
on_complete(result)
|
||||
end)
|
||||
end
|
||||
|
||||
---@param index number
|
||||
---@param debug boolean?
|
||||
---@param on_complete fun(success: boolean)
|
||||
function M.run_test_case(index, debug, on_complete)
|
||||
local tc = panel_state.test_cases[index]
|
||||
if not tc then
|
||||
on_complete(false)
|
||||
return
|
||||
end
|
||||
|
||||
tc.status = 'running'
|
||||
run_single_test_case(tc, debug, function(r)
|
||||
tc.status = r.status
|
||||
tc.actual = r.actual
|
||||
tc.actual_highlights = r.actual_highlights
|
||||
tc.error = r.error
|
||||
tc.stderr = r.stderr
|
||||
tc.time_ms = r.time_ms
|
||||
tc.code = r.code
|
||||
tc.ok = r.ok
|
||||
tc.signal = r.signal
|
||||
tc.tled = r.tled
|
||||
tc.mled = r.mled
|
||||
tc.rss_mb = r.rss_mb
|
||||
|
||||
on_complete(true)
|
||||
end)
|
||||
end
|
||||
|
||||
---@param indices? integer[]
|
||||
---@param debug boolean?
|
||||
---@param on_each? fun(index: integer, total: integer)
|
||||
---@param on_done fun(results: RanTestCase[])
|
||||
function M.run_all_test_cases(indices, debug, on_each, on_done)
|
||||
local to_run = indices
|
||||
if not to_run then
|
||||
to_run = {}
|
||||
for i = 1, #panel_state.test_cases do
|
||||
to_run[i] = i
|
||||
end
|
||||
end
|
||||
|
||||
local function run_next(pos)
|
||||
if pos > #to_run then
|
||||
logger.log(
|
||||
('Finished %s %d test cases.'):format(debug and 'debugging' or 'running', #to_run),
|
||||
vim.log.levels.INFO,
|
||||
true
|
||||
)
|
||||
on_done(panel_state.test_cases)
|
||||
return
|
||||
end
|
||||
|
||||
M.run_test_case(to_run[pos], debug, function()
|
||||
if on_each then
|
||||
on_each(pos, #to_run)
|
||||
end
|
||||
run_next(pos + 1)
|
||||
end)
|
||||
end
|
||||
|
||||
run_next(1)
|
||||
end
|
||||
|
||||
---@return PanelState
|
||||
function M.get_panel_state()
|
||||
return panel_state
|
||||
end
|
||||
|
||||
---@param output string|nil
|
||||
---@return nil
|
||||
function M.handle_compilation_failure(output)
|
||||
local ansi = require('cp.ui.ansi')
|
||||
|
||||
local txt
|
||||
local hl = {}
|
||||
|
||||
if config.ui.ansi then
|
||||
local p = ansi.parse_ansi_text(output or '')
|
||||
txt = table.concat(p.lines, '\n')
|
||||
hl = p.highlights
|
||||
else
|
||||
txt = (output or ''):gsub('\027%[[%d;]*[a-zA-Z]', '')
|
||||
end
|
||||
|
||||
for _, tc in ipairs(panel_state.test_cases) do
|
||||
tc.status = 'fail'
|
||||
tc.actual = txt
|
||||
tc.actual_highlights = hl
|
||||
tc.error = 'Compilation failed'
|
||||
tc.stderr = ''
|
||||
tc.time_ms = 0
|
||||
tc.code = 1
|
||||
tc.ok = false
|
||||
tc.signal = ''
|
||||
tc.tled = false
|
||||
tc.mled = false
|
||||
tc.rss_mb = 0
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
386
lua/cp/runner/run_render.lua
Normal file
386
lua/cp/runner/run_render.lua
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
---@class StatusInfo
|
||||
---@field text string
|
||||
---@field highlight_group string
|
||||
|
||||
local M = {}
|
||||
|
||||
local function strwidth(s)
|
||||
return vim.api.nvim_strwidth(s)
|
||||
end
|
||||
|
||||
local exit_code_names = {
|
||||
[128] = 'SIGHUP',
|
||||
[129] = 'SIGINT',
|
||||
[130] = 'SIGQUIT',
|
||||
[131] = 'SIGILL',
|
||||
[132] = 'SIGTRAP',
|
||||
[133] = 'SIGABRT',
|
||||
[134] = 'SIGBUS',
|
||||
[135] = 'SIGFPE',
|
||||
[136] = 'SIGKILL',
|
||||
[137] = 'SIGUSR1',
|
||||
[138] = 'SIGSEGV',
|
||||
[139] = 'SIGUSR2',
|
||||
[140] = 'SIGPIPE',
|
||||
[141] = 'SIGALRM',
|
||||
[142] = 'SIGTERM',
|
||||
[143] = 'SIGCHLD',
|
||||
}
|
||||
|
||||
---@param ran_test_case RanTestCase
|
||||
---@return StatusInfo
|
||||
function M.get_status_info(ran_test_case)
|
||||
if ran_test_case.status == 'pending' then
|
||||
return { text = '...', highlight_group = 'CpTestNA' }
|
||||
elseif ran_test_case.status == 'running' then
|
||||
return { text = 'RUN', highlight_group = 'CpTestNA' }
|
||||
end
|
||||
|
||||
if ran_test_case.ok then
|
||||
return { text = 'AC', highlight_group = 'CpTestAC' }
|
||||
end
|
||||
|
||||
if ran_test_case.tled then
|
||||
return { text = 'TLE', highlight_group = 'CpTestTLE' }
|
||||
elseif ran_test_case.mled then
|
||||
return { text = 'MLE', highlight_group = 'CpTestMLE' }
|
||||
elseif ran_test_case.code and ran_test_case.code >= 128 then
|
||||
return { text = 'RTE', highlight_group = 'CpTestRTE' }
|
||||
elseif ran_test_case.code == 0 and not ran_test_case.ok then
|
||||
return { text = 'WA', highlight_group = 'CpTestWA' }
|
||||
end
|
||||
|
||||
return { text = 'N/A', highlight_group = 'CpTestNA' }
|
||||
end
|
||||
|
||||
local function format_exit_code(code)
|
||||
if not code then
|
||||
return '—'
|
||||
end
|
||||
local signal_name = exit_code_names[code]
|
||||
return signal_name and string.format('%d (%s)', code, signal_name) or tostring(code)
|
||||
end
|
||||
|
||||
local function compute_cols(test_state)
|
||||
local w = { num = 5, status = 8, time = 6, timeout = 8, rss = 8, memory = 8, exit = 11 }
|
||||
|
||||
local timeout_str = '—'
|
||||
local memory_str = '—'
|
||||
if test_state.constraints then
|
||||
timeout_str = tostring(test_state.constraints.timeout_ms)
|
||||
memory_str = string.format('%.0f', test_state.constraints.memory_mb)
|
||||
end
|
||||
|
||||
for i, tc in ipairs(test_state.test_cases) do
|
||||
local prefix = (i == test_state.current_index) and '>' or ' '
|
||||
w.num = math.max(w.num, strwidth(' ' .. prefix .. i .. ' '))
|
||||
w.status = math.max(w.status, strwidth(' ' .. M.get_status_info(tc).text .. ' '))
|
||||
local time_str = tc.time_ms and string.format('%.2f', tc.time_ms) or '—'
|
||||
w.time = math.max(w.time, strwidth(' ' .. time_str .. ' '))
|
||||
w.timeout = math.max(w.timeout, strwidth(' ' .. timeout_str .. ' '))
|
||||
local rss_str = (tc.rss_mb and string.format('%.0f', tc.rss_mb)) or '—'
|
||||
w.rss = math.max(w.rss, strwidth(' ' .. rss_str .. ' '))
|
||||
w.memory = math.max(w.memory, strwidth(' ' .. memory_str .. ' '))
|
||||
w.exit = math.max(w.exit, strwidth(' ' .. format_exit_code(tc.code) .. ' '))
|
||||
end
|
||||
|
||||
w.num = math.max(w.num, strwidth(' # '))
|
||||
w.status = math.max(w.status, strwidth(' Status '))
|
||||
w.time = math.max(w.time, strwidth(' Runtime (ms) '))
|
||||
w.timeout = math.max(w.timeout, strwidth(' Time (ms) '))
|
||||
w.rss = math.max(w.rss, strwidth(' RSS (MB) '))
|
||||
w.memory = math.max(w.memory, strwidth(' Mem (MB) '))
|
||||
w.exit = math.max(w.exit, strwidth(' Exit Code '))
|
||||
|
||||
local sum = w.num + w.status + w.time + w.timeout + w.rss + w.memory + w.exit
|
||||
local inner = sum + 6
|
||||
local total = inner + 2
|
||||
return { w = w, sum = sum, inner = inner, total = total }
|
||||
end
|
||||
|
||||
local function center(text, width)
|
||||
local pad = width - strwidth(text)
|
||||
if pad <= 0 then
|
||||
return text
|
||||
end
|
||||
local left = math.ceil(pad / 2)
|
||||
return string.rep(' ', left) .. text .. string.rep(' ', pad - left)
|
||||
end
|
||||
|
||||
local function format_num_column(prefix, idx, width)
|
||||
local num_str = tostring(idx)
|
||||
local content = (#num_str == 1) and (' ' .. prefix .. ' ' .. num_str .. ' ')
|
||||
or (' ' .. prefix .. num_str .. ' ')
|
||||
local total_pad = width - strwidth(content)
|
||||
if total_pad <= 0 then
|
||||
return content
|
||||
end
|
||||
local left_pad = math.ceil(total_pad / 2)
|
||||
local right_pad = total_pad - left_pad
|
||||
return string.rep(' ', left_pad) .. content .. string.rep(' ', right_pad)
|
||||
end
|
||||
|
||||
local function top_border(c)
|
||||
local w = c.w
|
||||
return '┌'
|
||||
.. string.rep('─', w.num)
|
||||
.. '┬'
|
||||
.. string.rep('─', w.status)
|
||||
.. '┬'
|
||||
.. string.rep('─', w.time)
|
||||
.. '┬'
|
||||
.. string.rep('─', w.timeout)
|
||||
.. '┬'
|
||||
.. string.rep('─', w.rss)
|
||||
.. '┬'
|
||||
.. string.rep('─', w.memory)
|
||||
.. '┬'
|
||||
.. string.rep('─', w.exit)
|
||||
.. '┐'
|
||||
end
|
||||
|
||||
local function row_sep(c)
|
||||
local w = c.w
|
||||
return '├'
|
||||
.. string.rep('─', w.num)
|
||||
.. '┼'
|
||||
.. string.rep('─', w.status)
|
||||
.. '┼'
|
||||
.. string.rep('─', w.time)
|
||||
.. '┼'
|
||||
.. string.rep('─', w.timeout)
|
||||
.. '┼'
|
||||
.. string.rep('─', w.rss)
|
||||
.. '┼'
|
||||
.. string.rep('─', w.memory)
|
||||
.. '┼'
|
||||
.. string.rep('─', w.exit)
|
||||
.. '┤'
|
||||
end
|
||||
|
||||
local function bottom_border(c)
|
||||
local w = c.w
|
||||
return '└'
|
||||
.. string.rep('─', w.num)
|
||||
.. '┴'
|
||||
.. string.rep('─', w.status)
|
||||
.. '┴'
|
||||
.. string.rep('─', w.time)
|
||||
.. '┴'
|
||||
.. string.rep('─', w.timeout)
|
||||
.. '┴'
|
||||
.. string.rep('─', w.rss)
|
||||
.. '┴'
|
||||
.. string.rep('─', w.memory)
|
||||
.. '┴'
|
||||
.. string.rep('─', w.exit)
|
||||
.. '┘'
|
||||
end
|
||||
|
||||
local function flat_fence_above(c)
|
||||
local w = c.w
|
||||
return '├'
|
||||
.. string.rep('─', w.num)
|
||||
.. '┴'
|
||||
.. string.rep('─', w.status)
|
||||
.. '┴'
|
||||
.. string.rep('─', w.time)
|
||||
.. '┴'
|
||||
.. string.rep('─', w.timeout)
|
||||
.. '┴'
|
||||
.. string.rep('─', w.rss)
|
||||
.. '┴'
|
||||
.. string.rep('─', w.memory)
|
||||
.. '┴'
|
||||
.. string.rep('─', w.exit)
|
||||
.. '┤'
|
||||
end
|
||||
|
||||
local function flat_fence_below(c)
|
||||
local w = c.w
|
||||
return '├'
|
||||
.. string.rep('─', w.num)
|
||||
.. '┬'
|
||||
.. string.rep('─', w.status)
|
||||
.. '┬'
|
||||
.. string.rep('─', w.time)
|
||||
.. '┬'
|
||||
.. string.rep('─', w.timeout)
|
||||
.. '┬'
|
||||
.. string.rep('─', w.rss)
|
||||
.. '┬'
|
||||
.. string.rep('─', w.memory)
|
||||
.. '┬'
|
||||
.. string.rep('─', w.exit)
|
||||
.. '┤'
|
||||
end
|
||||
|
||||
local function flat_bottom_border(c)
|
||||
return '└' .. string.rep('─', c.inner) .. '┘'
|
||||
end
|
||||
|
||||
local function header_line(c)
|
||||
local w = c.w
|
||||
return '│'
|
||||
.. center('#', w.num)
|
||||
.. '│'
|
||||
.. center('Status', w.status)
|
||||
.. '│'
|
||||
.. center('Runtime (ms)', w.time)
|
||||
.. '│'
|
||||
.. center('Time (ms)', w.timeout)
|
||||
.. '│'
|
||||
.. center('RSS (MB)', w.rss)
|
||||
.. '│'
|
||||
.. center('Mem (MB)', w.memory)
|
||||
.. '│'
|
||||
.. center('Exit Code', w.exit)
|
||||
.. '│'
|
||||
end
|
||||
|
||||
local function data_row(c, idx, tc, is_current, test_state)
|
||||
local w = c.w
|
||||
local prefix = is_current and '>' or ' '
|
||||
local status = M.get_status_info(tc)
|
||||
local time = tc.time_ms and string.format('%.2f', tc.time_ms) or '—'
|
||||
local exit = format_exit_code(tc.code)
|
||||
|
||||
local timeout = '—'
|
||||
local memory = '—'
|
||||
if test_state.constraints then
|
||||
timeout = tostring(test_state.constraints.timeout_ms)
|
||||
memory = string.format('%.0f', test_state.constraints.memory_mb)
|
||||
end
|
||||
|
||||
local rss = (tc.rss_mb and string.format('%.0f', tc.rss_mb)) or '—'
|
||||
|
||||
local line = '│'
|
||||
.. format_num_column(prefix, idx, w.num)
|
||||
.. '│'
|
||||
.. center(status.text, w.status)
|
||||
.. '│'
|
||||
.. center(time, w.time)
|
||||
.. '│'
|
||||
.. center(timeout, w.timeout)
|
||||
.. '│'
|
||||
.. center(rss, w.rss)
|
||||
.. '│'
|
||||
.. center(memory, w.memory)
|
||||
.. '│'
|
||||
.. center(exit, w.exit)
|
||||
.. '│'
|
||||
|
||||
local hi
|
||||
if status.text ~= '' then
|
||||
local status_pos = line:find(status.text, 1, true)
|
||||
if status_pos then
|
||||
hi = {
|
||||
col_start = status_pos - 1,
|
||||
col_end = status_pos - 1 + #status.text,
|
||||
highlight_group = status.highlight_group,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return line, hi
|
||||
end
|
||||
|
||||
---@param test_state PanelState
|
||||
---@return string[] lines
|
||||
---@return Highlight[] highlights
|
||||
---@return integer current_test_line
|
||||
function M.render_test_list(test_state)
|
||||
local lines, highlights = {}, {}
|
||||
local c = compute_cols(test_state)
|
||||
local current_test_line = nil
|
||||
|
||||
table.insert(lines, top_border(c))
|
||||
table.insert(lines, header_line(c))
|
||||
table.insert(lines, row_sep(c))
|
||||
|
||||
for i, tc in ipairs(test_state.test_cases) do
|
||||
local is_current = (i == test_state.current_index)
|
||||
local row, hi = data_row(c, i, tc, is_current, test_state)
|
||||
table.insert(lines, row)
|
||||
|
||||
if is_current then
|
||||
current_test_line = #lines
|
||||
end
|
||||
|
||||
if hi then
|
||||
hi.line = #lines - 1
|
||||
table.insert(highlights, hi)
|
||||
end
|
||||
|
||||
local has_next = (i < #test_state.test_cases)
|
||||
local has_input = is_current and tc.input and tc.input ~= ''
|
||||
|
||||
if has_input then
|
||||
table.insert(lines, flat_fence_above(c))
|
||||
|
||||
local input_header = 'Input:'
|
||||
local header_pad = c.inner - #input_header
|
||||
table.insert(lines, '│' .. input_header .. string.rep(' ', header_pad) .. '│')
|
||||
|
||||
for _, input_line in ipairs(vim.split(tc.input, '\n', { plain = true, trimempty = false })) do
|
||||
local s = input_line or ''
|
||||
if strwidth(s) > c.inner then
|
||||
s = string.sub(s, 1, c.inner)
|
||||
end
|
||||
local pad = c.inner - strwidth(s)
|
||||
table.insert(lines, '│' .. s .. string.rep(' ', pad) .. '│')
|
||||
end
|
||||
|
||||
if has_next then
|
||||
table.insert(lines, flat_fence_below(c))
|
||||
else
|
||||
table.insert(lines, flat_bottom_border(c))
|
||||
end
|
||||
else
|
||||
if has_next then
|
||||
table.insert(lines, row_sep(c))
|
||||
else
|
||||
table.insert(lines, bottom_border(c))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return lines, highlights, current_test_line or 1
|
||||
end
|
||||
|
||||
---@param ran_test_case RanTestCase?
|
||||
---@return string
|
||||
function M.render_status_bar(ran_test_case)
|
||||
if not ran_test_case then
|
||||
return ''
|
||||
end
|
||||
local parts = {}
|
||||
if ran_test_case.time_ms then
|
||||
table.insert(parts, string.format('%.2fms', ran_test_case.time_ms))
|
||||
end
|
||||
if ran_test_case.code then
|
||||
table.insert(parts, string.format('Exit: %d', ran_test_case.code))
|
||||
end
|
||||
return table.concat(parts, ' │ ')
|
||||
end
|
||||
|
||||
---@return table<string, table>
|
||||
function M.get_highlight_groups()
|
||||
return {
|
||||
CpTestAC = { link = 'DiagnosticOk' },
|
||||
CpTestWA = { link = 'DiagnosticError' },
|
||||
CpTestTLE = { link = 'DiagnosticWarn' },
|
||||
CpTestMLE = { link = 'DiagnosticWarn' },
|
||||
CpTestRTE = { link = 'DiagnosticHint' },
|
||||
CpTestNA = { link = 'Comment' },
|
||||
}
|
||||
end
|
||||
|
||||
function M.setup_highlights()
|
||||
local groups = M.get_highlight_groups()
|
||||
for name, opts in pairs(groups) do
|
||||
vim.api.nvim_set_hl(0, name, opts)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -1,291 +0,0 @@
|
|||
---@class ScraperTestCase
|
||||
---@field input string
|
||||
---@field expected string
|
||||
|
||||
---@class ScraperResult
|
||||
---@field success boolean
|
||||
---@field problem_id string
|
||||
---@field url? string
|
||||
---@field tests? ScraperTestCase[]
|
||||
---@field error? string
|
||||
|
||||
local M = {}
|
||||
local cache = require('cp.cache')
|
||||
local logger = require('cp.log')
|
||||
|
||||
local function get_plugin_path()
|
||||
local plugin_path = debug.getinfo(1, 'S').source:sub(2)
|
||||
return vim.fn.fnamemodify(plugin_path, ':h:h:h')
|
||||
end
|
||||
|
||||
local function ensure_io_directory()
|
||||
vim.fn.mkdir('io', 'p')
|
||||
end
|
||||
|
||||
local function check_internet_connectivity()
|
||||
local result = vim.system({ 'ping', '-c', '1', '-W', '3', '8.8.8.8' }, { text = true }):wait()
|
||||
return result.code == 0
|
||||
end
|
||||
|
||||
local function setup_python_env()
|
||||
local plugin_path = get_plugin_path()
|
||||
local venv_dir = plugin_path .. '/.venv'
|
||||
|
||||
if vim.fn.executable('uv') == 0 then
|
||||
logger.log(
|
||||
'uv is not installed. Install it to enable problem scraping: https://docs.astral.sh/uv/',
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
return false
|
||||
end
|
||||
|
||||
if vim.fn.isdirectory(venv_dir) == 0 then
|
||||
logger.log('setting up Python environment for scrapers...')
|
||||
local result = vim.system({ 'uv', 'sync' }, { cwd = plugin_path, text = true }):wait()
|
||||
if result.code ~= 0 then
|
||||
logger.log('failed to setup Python environment: ' .. result.stderr, vim.log.levels.ERROR)
|
||||
return false
|
||||
end
|
||||
logger.log('python environment setup complete')
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@return {success: boolean, problems?: table[], error?: string}
|
||||
function M.scrape_contest_metadata(platform, contest_id)
|
||||
vim.validate({
|
||||
platform = { platform, 'string' },
|
||||
contest_id = { contest_id, 'string' },
|
||||
})
|
||||
|
||||
cache.load()
|
||||
|
||||
local cached_data = cache.get_contest_data(platform, contest_id)
|
||||
if cached_data then
|
||||
return {
|
||||
success = true,
|
||||
problems = cached_data.problems,
|
||||
}
|
||||
end
|
||||
|
||||
if not check_internet_connectivity() then
|
||||
return {
|
||||
success = false,
|
||||
error = 'No internet connection available',
|
||||
}
|
||||
end
|
||||
|
||||
if not setup_python_env() then
|
||||
return {
|
||||
success = false,
|
||||
error = 'Python environment setup failed',
|
||||
}
|
||||
end
|
||||
|
||||
local plugin_path = get_plugin_path()
|
||||
local scraper_path = plugin_path .. '/scrapers/' .. platform .. '.py'
|
||||
|
||||
local args
|
||||
if platform == 'cses' then
|
||||
args = {
|
||||
'uv',
|
||||
'run',
|
||||
'--directory',
|
||||
plugin_path,
|
||||
scraper_path,
|
||||
'metadata',
|
||||
}
|
||||
else
|
||||
args = {
|
||||
'uv',
|
||||
'run',
|
||||
'--directory',
|
||||
plugin_path,
|
||||
scraper_path,
|
||||
'metadata',
|
||||
contest_id,
|
||||
}
|
||||
end
|
||||
|
||||
local result = vim
|
||||
.system(args, {
|
||||
cwd = plugin_path,
|
||||
text = true,
|
||||
timeout = 30000,
|
||||
})
|
||||
:wait()
|
||||
|
||||
if result.code ~= 0 then
|
||||
return {
|
||||
success = false,
|
||||
error = 'Failed to run metadata scraper: ' .. (result.stderr or 'Unknown error'),
|
||||
}
|
||||
end
|
||||
|
||||
local ok, data = pcall(vim.json.decode, result.stdout)
|
||||
if not ok then
|
||||
return {
|
||||
success = false,
|
||||
error = 'Failed to parse metadata scraper output: ' .. tostring(data),
|
||||
}
|
||||
end
|
||||
|
||||
if not data.success then
|
||||
return data
|
||||
end
|
||||
|
||||
local problems_list
|
||||
if platform == 'cses' then
|
||||
problems_list = data.categories and data.categories['CSES Problem Set'] or {}
|
||||
else
|
||||
problems_list = data.problems or {}
|
||||
end
|
||||
|
||||
cache.set_contest_data(platform, contest_id, problems_list)
|
||||
return {
|
||||
success = true,
|
||||
problems = problems_list,
|
||||
}
|
||||
end
|
||||
|
||||
---@param ctx ProblemContext
|
||||
---@return {success: boolean, problem_id: string, test_count?: number, test_cases?: ScraperTestCase[], url?: string, error?: string}
|
||||
function M.scrape_problem(ctx)
|
||||
vim.validate({
|
||||
ctx = { ctx, 'table' },
|
||||
})
|
||||
|
||||
ensure_io_directory()
|
||||
|
||||
if vim.fn.filereadable(ctx.input_file) == 1 and vim.fn.filereadable(ctx.expected_file) == 1 then
|
||||
local base_name = vim.fn.fnamemodify(ctx.input_file, ':r')
|
||||
local test_cases = {}
|
||||
local i = 1
|
||||
|
||||
while true do
|
||||
local input_file = base_name .. '.' .. i .. '.cpin'
|
||||
local expected_file = base_name .. '.' .. i .. '.cpout'
|
||||
|
||||
if vim.fn.filereadable(input_file) == 1 and vim.fn.filereadable(expected_file) == 1 then
|
||||
local input_content = table.concat(vim.fn.readfile(input_file), '\n')
|
||||
local expected_content = table.concat(vim.fn.readfile(expected_file), '\n')
|
||||
|
||||
table.insert(test_cases, {
|
||||
index = i,
|
||||
input = input_content,
|
||||
output = expected_content,
|
||||
})
|
||||
i = i + 1
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
success = true,
|
||||
problem_id = ctx.problem_name,
|
||||
test_count = #test_cases,
|
||||
test_cases = test_cases,
|
||||
}
|
||||
end
|
||||
|
||||
if not check_internet_connectivity() then
|
||||
return {
|
||||
success = false,
|
||||
problem_id = ctx.problem_name,
|
||||
error = 'No internet connection available',
|
||||
}
|
||||
end
|
||||
|
||||
if not setup_python_env() then
|
||||
return {
|
||||
success = false,
|
||||
problem_id = ctx.problem_name,
|
||||
error = 'Python environment setup failed',
|
||||
}
|
||||
end
|
||||
|
||||
local plugin_path = get_plugin_path()
|
||||
local scraper_path = plugin_path .. '/scrapers/' .. ctx.contest .. '.py'
|
||||
|
||||
local args
|
||||
if ctx.contest == 'cses' then
|
||||
args = {
|
||||
'uv',
|
||||
'run',
|
||||
'--directory',
|
||||
plugin_path,
|
||||
scraper_path,
|
||||
'tests',
|
||||
ctx.contest_id,
|
||||
}
|
||||
else
|
||||
args = {
|
||||
'uv',
|
||||
'run',
|
||||
'--directory',
|
||||
plugin_path,
|
||||
scraper_path,
|
||||
'tests',
|
||||
ctx.contest_id,
|
||||
ctx.problem_id,
|
||||
}
|
||||
end
|
||||
|
||||
local result = vim
|
||||
.system(args, {
|
||||
cwd = plugin_path,
|
||||
text = true,
|
||||
timeout = 30000,
|
||||
})
|
||||
:wait()
|
||||
|
||||
if result.code ~= 0 then
|
||||
return {
|
||||
success = false,
|
||||
problem_id = ctx.problem_name,
|
||||
error = 'Failed to run tests scraper: ' .. (result.stderr or 'Unknown error'),
|
||||
}
|
||||
end
|
||||
|
||||
local ok, data = pcall(vim.json.decode, result.stdout)
|
||||
if not ok then
|
||||
return {
|
||||
success = false,
|
||||
problem_id = ctx.problem_name,
|
||||
error = 'Failed to parse tests scraper output: ' .. tostring(data),
|
||||
}
|
||||
end
|
||||
|
||||
if not data.success then
|
||||
return data
|
||||
end
|
||||
|
||||
if data.tests and #data.tests > 0 then
|
||||
local base_name = vim.fn.fnamemodify(ctx.input_file, ':r')
|
||||
|
||||
for i, test_case in ipairs(data.tests) do
|
||||
local input_file = base_name .. '.' .. i .. '.cpin'
|
||||
local expected_file = base_name .. '.' .. i .. '.cpout'
|
||||
|
||||
local input_content = test_case.input:gsub('\r', '')
|
||||
local expected_content = test_case.expected:gsub('\r', '')
|
||||
|
||||
vim.fn.writefile(vim.split(input_content, '\n', true), input_file)
|
||||
vim.fn.writefile(vim.split(expected_content, '\n', true), expected_file)
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
success = true,
|
||||
problem_id = ctx.problem_name,
|
||||
test_count = data.tests and #data.tests or 0,
|
||||
test_cases = data.tests,
|
||||
url = data.url,
|
||||
}
|
||||
end
|
||||
|
||||
return M
|
||||
229
lua/cp/scraper.lua
Normal file
229
lua/cp/scraper.lua
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
local M = {}
|
||||
|
||||
local constants = require('cp.constants')
|
||||
local logger = require('cp.log')
|
||||
local utils = require('cp.utils')
|
||||
|
||||
local function syshandle(result)
|
||||
if result.code ~= 0 then
|
||||
local msg = 'Scraper failed: ' .. (result.stderr or 'Unknown error')
|
||||
return { success = false, error = msg }
|
||||
end
|
||||
|
||||
local ok, data = pcall(vim.json.decode, result.stdout)
|
||||
if not ok then
|
||||
local msg = 'Failed to parse scraper output: ' .. tostring(data)
|
||||
logger.log(msg, vim.log.levels.ERROR)
|
||||
return { success = false, error = msg }
|
||||
end
|
||||
|
||||
return { success = true, data = data }
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@param subcommand string
|
||||
---@param args string[]
|
||||
---@param opts { sync?: boolean, ndjson?: boolean, on_event?: fun(ev: table), on_exit?: fun(result: table) }
|
||||
local function run_scraper(platform, subcommand, args, opts)
|
||||
if not utils.setup_python_env() then
|
||||
local msg = 'no Python environment available (install uv or nix)'
|
||||
logger.log(msg, vim.log.levels.ERROR)
|
||||
if opts and opts.on_exit then
|
||||
opts.on_exit({ success = false, error = msg })
|
||||
end
|
||||
return { success = false, error = msg }
|
||||
end
|
||||
|
||||
local plugin_path = utils.get_plugin_path()
|
||||
local cmd = utils.get_python_cmd(platform, plugin_path)
|
||||
vim.list_extend(cmd, { subcommand })
|
||||
vim.list_extend(cmd, args)
|
||||
|
||||
logger.log('scraper cmd: ' .. table.concat(cmd, ' '))
|
||||
|
||||
local env = vim.fn.environ()
|
||||
env.VIRTUAL_ENV = ''
|
||||
env.PYTHONPATH = ''
|
||||
env.CONDA_PREFIX = ''
|
||||
|
||||
if opts and opts.ndjson then
|
||||
local uv = vim.loop
|
||||
local stdout = uv.new_pipe(false)
|
||||
local stderr = uv.new_pipe(false)
|
||||
local buf = ''
|
||||
|
||||
local handle
|
||||
handle = uv.spawn(cmd[1], {
|
||||
args = vim.list_slice(cmd, 2),
|
||||
stdio = { nil, stdout, stderr },
|
||||
env = env,
|
||||
cwd = plugin_path,
|
||||
}, function(code, signal)
|
||||
if buf ~= '' and opts.on_event then
|
||||
local ok_tail, ev_tail = pcall(vim.json.decode, buf)
|
||||
if ok_tail then
|
||||
opts.on_event(ev_tail)
|
||||
end
|
||||
buf = ''
|
||||
end
|
||||
if opts.on_exit then
|
||||
opts.on_exit({ success = (code == 0), code = code, signal = signal })
|
||||
end
|
||||
if not stdout:is_closing() then
|
||||
stdout:close()
|
||||
end
|
||||
if not stderr:is_closing() then
|
||||
stderr:close()
|
||||
end
|
||||
if handle and not handle:is_closing() then
|
||||
handle:close()
|
||||
end
|
||||
end)
|
||||
|
||||
if not handle then
|
||||
logger.log('Failed to start scraper process', vim.log.levels.ERROR)
|
||||
return { success = false, error = 'spawn failed' }
|
||||
end
|
||||
|
||||
uv.read_start(stdout, function(_, data)
|
||||
if data == nil then
|
||||
if buf ~= '' and opts.on_event then
|
||||
local ok_tail, ev_tail = pcall(vim.json.decode, buf)
|
||||
if ok_tail then
|
||||
opts.on_event(ev_tail)
|
||||
end
|
||||
buf = ''
|
||||
end
|
||||
return
|
||||
end
|
||||
buf = buf .. data
|
||||
while true do
|
||||
local s, e = buf:find('\n', 1, true)
|
||||
if not s then
|
||||
break
|
||||
end
|
||||
local line = buf:sub(1, s - 1)
|
||||
buf = buf:sub(e + 1)
|
||||
local ok, ev = pcall(vim.json.decode, line)
|
||||
if ok and opts.on_event then
|
||||
opts.on_event(ev)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
uv.read_start(stderr, function(_, _) end)
|
||||
return
|
||||
end
|
||||
|
||||
local sysopts = { text = true, timeout = 30000, env = env, cwd = plugin_path }
|
||||
if opts and opts.sync then
|
||||
local result = vim.system(cmd, sysopts):wait()
|
||||
return syshandle(result)
|
||||
else
|
||||
vim.system(cmd, sysopts, function(result)
|
||||
if opts and opts.on_exit then
|
||||
return opts.on_exit(syshandle(result))
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function M.scrape_contest_metadata(platform, contest_id, callback)
|
||||
run_scraper(platform, 'metadata', { contest_id }, {
|
||||
on_exit = function(result)
|
||||
if not result or not result.success then
|
||||
logger.log(
|
||||
("Failed to scrape metadata for %s contest '%s'."):format(
|
||||
constants.PLATFORM_DISPLAY_NAMES[platform],
|
||||
contest_id
|
||||
),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
local data = result.data or {}
|
||||
if not data.problems or #data.problems == 0 then
|
||||
logger.log(
|
||||
("No problems returned for %s contest '%s'."):format(
|
||||
constants.PLATFORM_DISPLAY_NAMES[platform],
|
||||
contest_id
|
||||
),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
if type(callback) == 'function' then
|
||||
callback(data)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
function M.scrape_contest_list(platform)
|
||||
local result = run_scraper(platform, 'contests', {}, { sync = true })
|
||||
if not result or not result.success or not (result.data and result.data.contests) then
|
||||
logger.log(
|
||||
('Could not scrape contests list for platform %s: %s'):format(
|
||||
platform,
|
||||
(result and result.error) or 'unknown'
|
||||
),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return {}
|
||||
end
|
||||
return result.data.contests
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@param callback fun(data: table)|nil
|
||||
function M.scrape_all_tests(platform, contest_id, callback)
|
||||
run_scraper(platform, 'tests', { contest_id }, {
|
||||
ndjson = true,
|
||||
on_event = function(ev)
|
||||
if ev.done then
|
||||
return
|
||||
end
|
||||
if ev.error and ev.problem_id then
|
||||
logger.log(
|
||||
("Failed to load tests for problem '%s' in contest '%s': %s"):format(
|
||||
ev.problem_id,
|
||||
contest_id,
|
||||
ev.error
|
||||
),
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
return
|
||||
end
|
||||
if not ev.problem_id or not ev.tests then
|
||||
return
|
||||
end
|
||||
vim.schedule(function()
|
||||
require('cp.utils').ensure_dirs()
|
||||
local config = require('cp.config')
|
||||
local base_name = config.default_filename(contest_id, ev.problem_id)
|
||||
for i, t in ipairs(ev.tests) do
|
||||
local input_file = 'io/' .. base_name .. '.' .. i .. '.cpin'
|
||||
local expected_file = 'io/' .. base_name .. '.' .. i .. '.cpout'
|
||||
local input_content = t.input:gsub('\r', '')
|
||||
local expected_content = t.expected:gsub('\r', '')
|
||||
vim.fn.writefile(vim.split(input_content, '\n'), input_file)
|
||||
vim.fn.writefile(vim.split(expected_content, '\n'), expected_file)
|
||||
end
|
||||
if type(callback) == 'function' then
|
||||
callback({
|
||||
combined = ev.combined,
|
||||
tests = ev.tests,
|
||||
timeout_ms = ev.timeout_ms or 0,
|
||||
memory_mb = ev.memory_mb or 0,
|
||||
interactive = ev.interactive or false,
|
||||
multi_test = ev.multi_test or false,
|
||||
problem_id = ev.problem_id,
|
||||
})
|
||||
end
|
||||
end)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
394
lua/cp/setup.lua
Normal file
394
lua/cp/setup.lua
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
local M = {}
|
||||
|
||||
local cache = require('cp.cache')
|
||||
local config_module = require('cp.config')
|
||||
local constants = require('cp.constants')
|
||||
local helpers = require('cp.helpers')
|
||||
local logger = require('cp.log')
|
||||
local scraper = require('cp.scraper')
|
||||
local state = require('cp.state')
|
||||
|
||||
---Get the language of the current file from cache
|
||||
---@return string?
|
||||
local function get_current_file_language()
|
||||
local current_file = vim.fn.expand('%:p')
|
||||
if current_file == '' then
|
||||
return nil
|
||||
end
|
||||
cache.load()
|
||||
local file_state = cache.get_file_state(current_file)
|
||||
return file_state and file_state.language or nil
|
||||
end
|
||||
|
||||
---Check if a problem file exists for any enabled language
|
||||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@param problem_id string
|
||||
---@return string?
|
||||
local function get_existing_problem_language(platform, contest_id, problem_id)
|
||||
local config = config_module.get_config()
|
||||
local platform_config = config.platforms[platform]
|
||||
if not platform_config then
|
||||
return nil
|
||||
end
|
||||
|
||||
for _, lang_id in ipairs(platform_config.enabled_languages) do
|
||||
local effective = config.runtime.effective[platform][lang_id]
|
||||
if effective and effective.extension then
|
||||
local basename = config.filename
|
||||
and config.filename(platform, contest_id, problem_id, config, lang_id)
|
||||
or config_module.default_filename(contest_id, problem_id)
|
||||
local filepath = basename .. '.' .. effective.extension
|
||||
if vim.fn.filereadable(filepath) == 1 then
|
||||
return lang_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
---@class TestCaseLite
|
||||
---@field input string
|
||||
---@field expected string
|
||||
|
||||
---@class ScrapeEvent
|
||||
---@field problem_id string
|
||||
---@field tests TestCaseLite[]|nil
|
||||
---@field timeout_ms integer|nil
|
||||
---@field memory_mb integer|nil
|
||||
---@field interactive boolean|nil
|
||||
---@field error string|nil
|
||||
---@field done boolean|nil
|
||||
---@field succeeded integer|nil
|
||||
---@field failed integer|nil
|
||||
|
||||
---@param cd table|nil
|
||||
---@return boolean
|
||||
local function is_metadata_ready(cd)
|
||||
return cd
|
||||
and type(cd.problems) == 'table'
|
||||
and #cd.problems > 0
|
||||
and type(cd.index_map) == 'table'
|
||||
and next(cd.index_map) ~= nil
|
||||
or false
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@param problems table
|
||||
local function start_tests(platform, contest_id, problems)
|
||||
local cached_len = #vim.tbl_filter(function(p)
|
||||
return not vim.tbl_isempty(cache.get_test_cases(platform, contest_id, p.id))
|
||||
end, problems)
|
||||
if cached_len ~= #problems then
|
||||
logger.log(('Fetching %s/%s problem tests...'):format(cached_len, #problems))
|
||||
scraper.scrape_all_tests(platform, contest_id, function(ev)
|
||||
local cached_tests = {}
|
||||
if not ev.interactive and vim.tbl_isempty(ev.tests) then
|
||||
logger.log(("No tests found for problem '%s'."):format(ev.problem_id), vim.log.levels.WARN)
|
||||
end
|
||||
for i, t in ipairs(ev.tests) do
|
||||
cached_tests[i] = { index = i, input = t.input, expected = t.expected }
|
||||
end
|
||||
cache.set_test_cases(
|
||||
platform,
|
||||
contest_id,
|
||||
ev.problem_id,
|
||||
ev.combined,
|
||||
cached_tests,
|
||||
ev.timeout_ms or 0,
|
||||
ev.memory_mb or 0,
|
||||
ev.interactive,
|
||||
ev.multi_test
|
||||
)
|
||||
|
||||
local io_state = state.get_io_view_state()
|
||||
if io_state then
|
||||
local combined_test = cache.get_combined_test(platform, contest_id, state.get_problem_id())
|
||||
if combined_test then
|
||||
local input_lines = vim.split(combined_test.input, '\n')
|
||||
require('cp.utils').update_buffer_content(io_state.input_buf, input_lines, nil, nil)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@param problem_id? string
|
||||
---@param language? string
|
||||
function M.setup_contest(platform, contest_id, problem_id, language)
|
||||
local old_platform, old_contest_id = state.get_platform(), state.get_contest_id()
|
||||
|
||||
state.set_platform(platform)
|
||||
state.set_contest_id(contest_id)
|
||||
|
||||
if language then
|
||||
local lang_result = config_module.get_language_for_platform(platform, language)
|
||||
if not lang_result.valid then
|
||||
logger.log(lang_result.error, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local is_new_contest = old_platform ~= platform and old_contest_id ~= contest_id
|
||||
|
||||
cache.load()
|
||||
|
||||
local function proceed(contest_data)
|
||||
local problems = contest_data.problems
|
||||
local pid = problem_id and problem_id or problems[1].id
|
||||
M.setup_problem(pid, language)
|
||||
start_tests(platform, contest_id, problems)
|
||||
|
||||
if config_module.get_config().open_url and is_new_contest and contest_data.url then
|
||||
vim.ui.open(contest_data.url:format(pid))
|
||||
end
|
||||
end
|
||||
|
||||
local contest_data = cache.get_contest_data(platform, contest_id)
|
||||
if not is_metadata_ready(contest_data) then
|
||||
local cfg = config_module.get_config()
|
||||
local lang = language or (cfg.platforms[platform] and cfg.platforms[platform].default_language)
|
||||
|
||||
vim.cmd.only({ mods = { silent = true } })
|
||||
local bufnr = vim.api.nvim_create_buf(true, false)
|
||||
vim.api.nvim_win_set_buf(0, bufnr)
|
||||
vim.bo[bufnr].filetype = lang or ''
|
||||
vim.bo[bufnr].buftype = ''
|
||||
vim.bo[bufnr].swapfile = false
|
||||
|
||||
state.set_language(lang)
|
||||
|
||||
if cfg.hooks and cfg.hooks.setup_code and not vim.b[bufnr].cp_setup_done then
|
||||
local ok = pcall(cfg.hooks.setup_code, state)
|
||||
if ok then
|
||||
vim.b[bufnr].cp_setup_done = true
|
||||
end
|
||||
end
|
||||
|
||||
state.set_provisional({
|
||||
bufnr = bufnr,
|
||||
platform = platform,
|
||||
contest_id = contest_id,
|
||||
language = lang,
|
||||
requested_problem_id = problem_id,
|
||||
token = vim.loop.hrtime(),
|
||||
})
|
||||
|
||||
logger.log('Fetching contests problems...', vim.log.levels.INFO, true)
|
||||
scraper.scrape_contest_metadata(
|
||||
platform,
|
||||
contest_id,
|
||||
vim.schedule_wrap(function(result)
|
||||
local problems = result.problems or {}
|
||||
cache.set_contest_data(platform, contest_id, problems, result.url)
|
||||
local prov = state.get_provisional()
|
||||
if not prov or prov.platform ~= platform or prov.contest_id ~= contest_id then
|
||||
return
|
||||
end
|
||||
local cd = cache.get_contest_data(platform, contest_id)
|
||||
if not is_metadata_ready(cd) then
|
||||
return
|
||||
end
|
||||
local pid = prov.requested_problem_id
|
||||
if not pid or not cd.index_map or not cd.index_map[pid] then
|
||||
pid = cd.problems[1] and cd.problems[1].id or nil
|
||||
end
|
||||
if not pid then
|
||||
return
|
||||
end
|
||||
proceed(cd)
|
||||
end)
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
proceed(contest_data)
|
||||
end
|
||||
|
||||
---@param problem_id string
|
||||
---@param language? string
|
||||
function M.setup_problem(problem_id, language)
|
||||
local platform = state.get_platform()
|
||||
if not platform then
|
||||
logger.log('No platform/contest/problem configured.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local old_problem_id = state.get_problem_id()
|
||||
state.set_problem_id(problem_id)
|
||||
|
||||
if old_problem_id ~= problem_id then
|
||||
local io_state = state.get_io_view_state()
|
||||
if io_state and io_state.output_buf and vim.api.nvim_buf_is_valid(io_state.output_buf) then
|
||||
local utils = require('cp.utils')
|
||||
utils.update_buffer_content(io_state.output_buf, {}, nil, nil)
|
||||
end
|
||||
end
|
||||
local config = config_module.get_config()
|
||||
local lang = language
|
||||
or (config.platforms[platform] and config.platforms[platform].default_language)
|
||||
|
||||
if language then
|
||||
local lang_result = config_module.get_language_for_platform(platform, language)
|
||||
if not lang_result.valid then
|
||||
logger.log(lang_result.error, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
state.set_language(lang)
|
||||
|
||||
local source_file = state.get_source_file(lang)
|
||||
if not source_file then
|
||||
return
|
||||
end
|
||||
|
||||
vim.fn.mkdir(vim.fn.fnamemodify(source_file, ':h'), 'p')
|
||||
|
||||
local prov = state.get_provisional()
|
||||
if prov and prov.platform == platform and prov.contest_id == (state.get_contest_id() or '') then
|
||||
if vim.api.nvim_buf_is_valid(prov.bufnr) then
|
||||
local existing_bufnr = vim.fn.bufnr(source_file)
|
||||
if existing_bufnr ~= -1 then
|
||||
vim.api.nvim_buf_delete(prov.bufnr, { force = true })
|
||||
state.set_provisional(nil)
|
||||
else
|
||||
vim.api.nvim_buf_set_name(prov.bufnr, source_file)
|
||||
vim.bo[prov.bufnr].swapfile = true
|
||||
-- selene: allow(mixed_table)
|
||||
vim.cmd.write({
|
||||
vim.fn.fnameescape(source_file),
|
||||
bang = true,
|
||||
mods = { silent = true, noautocmd = true, keepalt = true },
|
||||
})
|
||||
state.set_solution_win(vim.api.nvim_get_current_win())
|
||||
if config.hooks and config.hooks.setup_code and not vim.b[prov.bufnr].cp_setup_done then
|
||||
local ok = pcall(config.hooks.setup_code, state)
|
||||
if ok then
|
||||
vim.b[prov.bufnr].cp_setup_done = true
|
||||
end
|
||||
elseif not vim.b[prov.bufnr].cp_setup_done then
|
||||
helpers.clearcol(prov.bufnr)
|
||||
vim.b[prov.bufnr].cp_setup_done = true
|
||||
end
|
||||
cache.set_file_state(
|
||||
vim.fn.fnamemodify(source_file, ':p'),
|
||||
platform,
|
||||
state.get_contest_id() or '',
|
||||
state.get_problem_id() or '',
|
||||
lang
|
||||
)
|
||||
require('cp.ui.views').ensure_io_view()
|
||||
state.set_provisional(nil)
|
||||
return
|
||||
end
|
||||
else
|
||||
state.set_provisional(nil)
|
||||
end
|
||||
end
|
||||
|
||||
vim.cmd.only({ mods = { silent = true } })
|
||||
vim.cmd.e(source_file)
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
state.set_solution_win(vim.api.nvim_get_current_win())
|
||||
require('cp.ui.views').ensure_io_view()
|
||||
if config.hooks and config.hooks.setup_code and not vim.b[bufnr].cp_setup_done then
|
||||
local ok = pcall(config.hooks.setup_code, state)
|
||||
if ok then
|
||||
vim.b[bufnr].cp_setup_done = true
|
||||
end
|
||||
elseif not vim.b[bufnr].cp_setup_done then
|
||||
helpers.clearcol(bufnr)
|
||||
vim.b[bufnr].cp_setup_done = true
|
||||
end
|
||||
cache.set_file_state(
|
||||
vim.fn.expand('%:p'),
|
||||
platform,
|
||||
state.get_contest_id() or '',
|
||||
state.get_problem_id() or '',
|
||||
lang
|
||||
)
|
||||
end
|
||||
|
||||
---@param direction integer
|
||||
---@param language? string
|
||||
function M.navigate_problem(direction, language)
|
||||
if direction == 0 then
|
||||
return
|
||||
end
|
||||
direction = direction > 0 and 1 or -1
|
||||
|
||||
local platform = state.get_platform()
|
||||
local contest_id = state.get_contest_id()
|
||||
local current_problem_id = state.get_problem_id()
|
||||
if not platform or not contest_id or not current_problem_id then
|
||||
logger.log('No platform configured.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
cache.load()
|
||||
local contest_data = cache.get_contest_data(platform, contest_id)
|
||||
if not is_metadata_ready(contest_data) then
|
||||
logger.log(
|
||||
('No data available for %s contest %s.'):format(
|
||||
constants.PLATFORM_DISPLAY_NAMES[platform],
|
||||
contest_id
|
||||
),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
local problems = contest_data.problems
|
||||
local index = contest_data.index_map[current_problem_id]
|
||||
local new_index = index + direction
|
||||
if new_index < 1 or new_index > #problems then
|
||||
return
|
||||
end
|
||||
|
||||
logger.log(('navigate_problem: %s -> %s'):format(current_problem_id, problems[new_index].id))
|
||||
|
||||
local active_panel = state.get_active_panel()
|
||||
if active_panel == 'run' then
|
||||
require('cp.ui.views').disable()
|
||||
end
|
||||
|
||||
local lang = nil
|
||||
|
||||
if language then
|
||||
local lang_result = config_module.get_language_for_platform(platform, language)
|
||||
if not lang_result.valid then
|
||||
logger.log(lang_result.error, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
lang = language
|
||||
else
|
||||
local existing_lang =
|
||||
get_existing_problem_language(platform, contest_id, problems[new_index].id)
|
||||
if existing_lang then
|
||||
lang = existing_lang
|
||||
else
|
||||
lang = get_current_file_language()
|
||||
if lang then
|
||||
local lang_result = config_module.get_language_for_platform(platform, lang)
|
||||
if not lang_result.valid then
|
||||
lang = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local io_state = state.get_io_view_state()
|
||||
if io_state and io_state.output_buf and vim.api.nvim_buf_is_valid(io_state.output_buf) then
|
||||
local utils = require('cp.utils')
|
||||
utils.update_buffer_content(io_state.output_buf, {}, nil, nil)
|
||||
end
|
||||
|
||||
M.setup_contest(platform, contest_id, problems[new_index].id, lang)
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
local M = {}
|
||||
local logger = require('cp.log')
|
||||
|
||||
function M.setup(config)
|
||||
local ok, ls = pcall(require, 'luasnip')
|
||||
if not ok then
|
||||
logger.log('LuaSnip not available - snippets disabled', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
local s, i, fmt = ls.snippet, ls.insert_node, require('luasnip.extras.fmt').fmt
|
||||
|
||||
local constants = require('cp.constants')
|
||||
local filetype_to_language = constants.filetype_to_language
|
||||
|
||||
local language_to_filetype = {}
|
||||
for ext, lang in pairs(filetype_to_language) do
|
||||
if not language_to_filetype[lang] then
|
||||
language_to_filetype[lang] = ext
|
||||
end
|
||||
end
|
||||
|
||||
local template_definitions = {
|
||||
cpp = {
|
||||
codeforces = [[#include <bits/stdc++.h>
|
||||
|
||||
using namespace std;
|
||||
|
||||
void solve() {{
|
||||
{}
|
||||
}}
|
||||
|
||||
int main() {{
|
||||
std::cin.tie(nullptr)->sync_with_stdio(false);
|
||||
|
||||
int tc = 1;
|
||||
std::cin >> tc;
|
||||
|
||||
for (int t = 0; t < tc; ++t) {{
|
||||
solve();
|
||||
}}
|
||||
|
||||
return 0;
|
||||
}}]],
|
||||
|
||||
atcoder = [[#include <bits/stdc++.h>
|
||||
|
||||
using namespace std;
|
||||
|
||||
void solve() {{
|
||||
{}
|
||||
}}
|
||||
|
||||
int main() {{
|
||||
std::cin.tie(nullptr)->sync_with_stdio(false);
|
||||
|
||||
#ifdef LOCAL
|
||||
int tc;
|
||||
std::cin >> tc;
|
||||
|
||||
for (int t = 0; t < tc; ++t) {{
|
||||
solve();
|
||||
}}
|
||||
#else
|
||||
solve();
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}}]],
|
||||
|
||||
cses = [[#include <bits/stdc++.h>
|
||||
|
||||
using namespace std;
|
||||
|
||||
int main() {{
|
||||
std::cin.tie(nullptr)->sync_with_stdio(false);
|
||||
|
||||
{}
|
||||
|
||||
return 0;
|
||||
}}]],
|
||||
},
|
||||
|
||||
python = {
|
||||
codeforces = [[def solve():
|
||||
{}
|
||||
|
||||
if __name__ == "__main__":
|
||||
tc = int(input())
|
||||
for _ in range(tc):
|
||||
solve()]],
|
||||
|
||||
atcoder = [[def solve():
|
||||
{}
|
||||
|
||||
if __name__ == "__main__":
|
||||
solve()]],
|
||||
|
||||
cses = [[{}]],
|
||||
},
|
||||
}
|
||||
|
||||
local user_overrides = {}
|
||||
for _, snippet in ipairs(config.snippets or {}) do
|
||||
user_overrides[snippet.trigger] = snippet
|
||||
end
|
||||
|
||||
for language, template_set in pairs(template_definitions) do
|
||||
local snippets = {}
|
||||
local filetype = constants.canonical_filetypes[language]
|
||||
|
||||
for contest, template in pairs(template_set) do
|
||||
local prefixed_trigger = ('cp.nvim/%s.%s'):format(contest, language)
|
||||
if not user_overrides[prefixed_trigger] then
|
||||
table.insert(snippets, s(prefixed_trigger, fmt(template, { i(1) })))
|
||||
end
|
||||
end
|
||||
|
||||
for trigger, snippet in pairs(user_overrides) do
|
||||
local prefix_match = trigger:match('^cp%.nvim/[^.]+%.(.+)$')
|
||||
if prefix_match == language then
|
||||
table.insert(snippets, snippet)
|
||||
end
|
||||
end
|
||||
|
||||
ls.add_snippets(filetype, snippets)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
222
lua/cp/state.lua
Normal file
222
lua/cp/state.lua
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
---@class cp.ProvisionalState
|
||||
---@field bufnr integer
|
||||
---@field platform string
|
||||
---@field contest_id string
|
||||
---@field language string
|
||||
---@field requested_problem_id string?
|
||||
---@field token integer
|
||||
|
||||
---@class cp.IoViewState
|
||||
---@field output_buf integer
|
||||
---@field input_buf integer
|
||||
---@field current_test_index integer?
|
||||
---@field source_buf integer?
|
||||
|
||||
---@class cp.State
|
||||
---@field get_platform fun(): string?
|
||||
---@field set_platform fun(platform: string)
|
||||
---@field get_contest_id fun(): string?
|
||||
---@field set_contest_id fun(contest_id: string)
|
||||
---@field get_problem_id fun(): string?
|
||||
---@field set_problem_id fun(problem_id: string)
|
||||
---@field get_language fun(): string?
|
||||
---@field set_language fun(language: string)
|
||||
---@field get_active_panel fun(): string?
|
||||
---@field set_active_panel fun(panel: string?)
|
||||
---@field get_base_name fun(): string?
|
||||
---@field get_source_file fun(language?: string): string?
|
||||
---@field get_binary_file fun(): string?
|
||||
---@field get_input_file fun(): string?
|
||||
---@field get_output_file fun(): string?
|
||||
---@field get_expected_file fun(): string?
|
||||
---@field get_provisional fun(): cp.ProvisionalState?
|
||||
---@field set_provisional fun(p: cp.ProvisionalState?)
|
||||
---@field get_saved_session fun(): string?
|
||||
---@field set_saved_session fun(path: string?)
|
||||
---@field get_io_view_state fun(): cp.IoViewState?
|
||||
---@field set_io_view_state fun(s: cp.IoViewState?)
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type table<string, any>
|
||||
local state = {
|
||||
platform = nil,
|
||||
contest_id = nil,
|
||||
problem_id = nil,
|
||||
language = nil,
|
||||
test_cases = nil,
|
||||
saved_session = nil,
|
||||
active_panel = nil,
|
||||
provisional = nil,
|
||||
solution_win = nil,
|
||||
io_view_state = nil,
|
||||
}
|
||||
|
||||
---@return string?
|
||||
function M.get_platform()
|
||||
return state.platform
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
function M.set_platform(platform)
|
||||
state.platform = platform
|
||||
end
|
||||
|
||||
---@return string?
|
||||
function M.get_contest_id()
|
||||
return state.contest_id
|
||||
end
|
||||
|
||||
---@param contest_id string
|
||||
function M.set_contest_id(contest_id)
|
||||
state.contest_id = contest_id
|
||||
end
|
||||
|
||||
---@return string?
|
||||
function M.get_problem_id()
|
||||
return state.problem_id
|
||||
end
|
||||
|
||||
---@param problem_id string
|
||||
function M.set_problem_id(problem_id)
|
||||
state.problem_id = problem_id
|
||||
end
|
||||
|
||||
---@return string?
|
||||
function M.get_language()
|
||||
return state.language
|
||||
end
|
||||
|
||||
---@param language string
|
||||
function M.set_language(language)
|
||||
state.language = language
|
||||
end
|
||||
|
||||
---@return string?
|
||||
function M.get_base_name()
|
||||
local platform, contest_id, problem_id = M.get_platform(), M.get_contest_id(), M.get_problem_id()
|
||||
if not platform or not contest_id or not problem_id then
|
||||
return nil
|
||||
end
|
||||
|
||||
local config_module = require('cp.config')
|
||||
local config = config_module.get_config()
|
||||
|
||||
if config.filename then
|
||||
return config.filename(platform, contest_id, problem_id, config)
|
||||
else
|
||||
return config_module.default_filename(contest_id, problem_id)
|
||||
end
|
||||
end
|
||||
|
||||
---@param language? string
|
||||
---@return string?
|
||||
function M.get_source_file(language)
|
||||
local base_name = M.get_base_name()
|
||||
if not base_name or not M.get_platform() then
|
||||
return nil
|
||||
end
|
||||
|
||||
local config = require('cp.config').get_config()
|
||||
local plat = M.get_platform()
|
||||
local platform_cfg = config.platforms[plat]
|
||||
if not platform_cfg then
|
||||
return nil
|
||||
end
|
||||
|
||||
local target_language = language or state.language or platform_cfg.default_language
|
||||
local eff = config.runtime.effective[plat] and config.runtime.effective[plat][target_language]
|
||||
or nil
|
||||
if not eff or not eff.extension then
|
||||
return nil
|
||||
end
|
||||
|
||||
return base_name .. '.' .. eff.extension
|
||||
end
|
||||
|
||||
---@return string?
|
||||
function M.get_binary_file()
|
||||
local base_name = M.get_base_name()
|
||||
return base_name and ('build/%s.run'):format(base_name) or nil
|
||||
end
|
||||
|
||||
---@return string?
|
||||
function M.get_debug_file()
|
||||
local base_name = M.get_base_name()
|
||||
return base_name and ('build/%s.dbg'):format(base_name) or nil
|
||||
end
|
||||
|
||||
---@return string?
|
||||
function M.get_input_file()
|
||||
local base_name = M.get_base_name()
|
||||
return base_name and ('io/%s.cpin'):format(base_name) or nil
|
||||
end
|
||||
|
||||
---@return string?
|
||||
function M.get_output_file()
|
||||
local base_name = M.get_base_name()
|
||||
return base_name and ('io/%s.cpout'):format(base_name) or nil
|
||||
end
|
||||
|
||||
---@return string?
|
||||
function M.get_expected_file()
|
||||
local base_name = M.get_base_name()
|
||||
return base_name and ('io/%s.expected'):format(base_name) or nil
|
||||
end
|
||||
|
||||
---@return string?
|
||||
function M.get_active_panel()
|
||||
return state.active_panel
|
||||
end
|
||||
|
||||
---@param panel string?
|
||||
function M.set_active_panel(panel)
|
||||
state.active_panel = panel
|
||||
end
|
||||
|
||||
---@return cp.ProvisionalState?
|
||||
function M.get_provisional()
|
||||
return state.provisional
|
||||
end
|
||||
|
||||
---@param p cp.ProvisionalState?
|
||||
function M.set_provisional(p)
|
||||
state.provisional = p
|
||||
end
|
||||
|
||||
---@return integer?
|
||||
function M.get_solution_win()
|
||||
if state.solution_win and vim.api.nvim_win_is_valid(state.solution_win) then
|
||||
return state.solution_win
|
||||
end
|
||||
return vim.api.nvim_get_current_win()
|
||||
end
|
||||
|
||||
---@param win integer?
|
||||
function M.set_solution_win(win)
|
||||
state.solution_win = win
|
||||
end
|
||||
|
||||
---@return cp.IoViewState?
|
||||
function M.get_io_view_state()
|
||||
return state.io_view_state
|
||||
end
|
||||
|
||||
---@param s cp.IoViewState?
|
||||
function M.set_io_view_state(s)
|
||||
state.io_view_state = s
|
||||
end
|
||||
|
||||
---@return string?
|
||||
function M.get_saved_session()
|
||||
return state.saved_session
|
||||
end
|
||||
|
||||
---@param path string?
|
||||
function M.set_saved_session(path)
|
||||
state.saved_session = path
|
||||
end
|
||||
|
||||
M._state = state
|
||||
|
||||
return M
|
||||
281
lua/cp/test.lua
281
lua/cp/test.lua
|
|
@ -1,281 +0,0 @@
|
|||
---@class TestCase
|
||||
---@field index number
|
||||
---@field input string
|
||||
---@field expected string
|
||||
---@field status "pending"|"pass"|"fail"|"running"|"timeout"
|
||||
---@field actual string?
|
||||
---@field time_ms number?
|
||||
---@field error string?
|
||||
---@field selected boolean
|
||||
---@field code number?
|
||||
---@field ok boolean?
|
||||
---@field signal string?
|
||||
---@field timed_out boolean?
|
||||
|
||||
---@class TestPanelState
|
||||
---@field test_cases TestCase[]
|
||||
---@field current_index number
|
||||
---@field buffer number?
|
||||
---@field namespace number?
|
||||
---@field is_active boolean
|
||||
---@field saved_layout table?
|
||||
|
||||
local M = {}
|
||||
local constants = require('cp.constants')
|
||||
local logger = require('cp.log')
|
||||
|
||||
---@type TestPanelState
|
||||
local test_panel_state = {
|
||||
test_cases = {},
|
||||
current_index = 1,
|
||||
buffer = nil,
|
||||
namespace = nil,
|
||||
is_active = false,
|
||||
saved_layout = nil,
|
||||
}
|
||||
|
||||
---@param index number
|
||||
---@param input string
|
||||
---@param expected string
|
||||
---@return TestCase
|
||||
local function create_test_case(index, input, expected)
|
||||
return {
|
||||
index = index,
|
||||
input = input,
|
||||
expected = expected,
|
||||
status = 'pending',
|
||||
actual = nil,
|
||||
time_ms = nil,
|
||||
error = nil,
|
||||
selected = true,
|
||||
}
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@param problem_id string?
|
||||
---@return TestCase[]
|
||||
local function parse_test_cases_from_cache(platform, contest_id, problem_id)
|
||||
local cache = require('cp.cache')
|
||||
cache.load()
|
||||
local cached_test_cases = cache.get_test_cases(platform, contest_id, problem_id)
|
||||
|
||||
if not cached_test_cases or #cached_test_cases == 0 then
|
||||
return {}
|
||||
end
|
||||
|
||||
local test_cases = {}
|
||||
|
||||
for i, test_case in ipairs(cached_test_cases) do
|
||||
local index = test_case.index or i
|
||||
local expected = test_case.expected or test_case.output or ''
|
||||
table.insert(test_cases, create_test_case(index, test_case.input, expected))
|
||||
end
|
||||
|
||||
return test_cases
|
||||
end
|
||||
|
||||
---@param input_file string
|
||||
---@param expected_file string
|
||||
---@return TestCase[]
|
||||
local function parse_test_cases_from_files(input_file, expected_file)
|
||||
if vim.fn.filereadable(input_file) == 0 or vim.fn.filereadable(expected_file) == 0 then
|
||||
return {}
|
||||
end
|
||||
|
||||
local base_name = vim.fn.fnamemodify(input_file, ':r')
|
||||
local test_cases = {}
|
||||
local i = 1
|
||||
|
||||
while true do
|
||||
local individual_input_file = base_name .. '.' .. i .. '.cpin'
|
||||
local individual_expected_file = base_name .. '.' .. i .. '.cpout'
|
||||
|
||||
if
|
||||
vim.fn.filereadable(individual_input_file) == 1
|
||||
and vim.fn.filereadable(individual_expected_file) == 1
|
||||
then
|
||||
local input_content = table.concat(vim.fn.readfile(individual_input_file), '\n')
|
||||
local expected_content = table.concat(vim.fn.readfile(individual_expected_file), '\n')
|
||||
|
||||
table.insert(test_cases, create_test_case(i, input_content, expected_content))
|
||||
i = i + 1
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if #test_cases == 0 then
|
||||
local input_content = table.concat(vim.fn.readfile(input_file), '\n')
|
||||
local expected_content = table.concat(vim.fn.readfile(expected_file), '\n')
|
||||
return { create_test_case(1, input_content, expected_content) }
|
||||
end
|
||||
|
||||
return test_cases
|
||||
end
|
||||
|
||||
---@param ctx ProblemContext
|
||||
---@param contest_config ContestConfig
|
||||
---@param test_case TestCase
|
||||
---@return table
|
||||
local function run_single_test_case(ctx, contest_config, test_case)
|
||||
local language = vim.fn.fnamemodify(ctx.source_file, ':e')
|
||||
local language_name = constants.filetype_to_language[language] or contest_config.default_language
|
||||
local language_config = contest_config[language_name]
|
||||
|
||||
if not language_config then
|
||||
return {
|
||||
status = 'fail',
|
||||
actual = '',
|
||||
error = 'No language configuration',
|
||||
time_ms = 0,
|
||||
}
|
||||
end
|
||||
|
||||
local function substitute_template(cmd_template, substitutions)
|
||||
local result = {}
|
||||
for _, arg in ipairs(cmd_template) do
|
||||
local substituted = arg
|
||||
for key, value in pairs(substitutions) do
|
||||
substituted = substituted:gsub('{' .. key .. '}', value)
|
||||
end
|
||||
table.insert(result, substituted)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local function build_command(cmd_template, executable, substitutions)
|
||||
local cmd = substitute_template(cmd_template, substitutions)
|
||||
if executable then
|
||||
table.insert(cmd, 1, executable)
|
||||
end
|
||||
return cmd
|
||||
end
|
||||
|
||||
local substitutions = {
|
||||
source = ctx.source_file,
|
||||
binary = ctx.binary_file,
|
||||
version = tostring(language_config.version or ''),
|
||||
}
|
||||
|
||||
if language_config.compile and vim.fn.filereadable(ctx.binary_file) == 0 then
|
||||
logger.log('binary not found, compiling first...')
|
||||
local compile_cmd = substitute_template(language_config.compile, substitutions)
|
||||
local compile_result = vim.system(compile_cmd, { text = true }):wait()
|
||||
if compile_result.code ~= 0 then
|
||||
return {
|
||||
status = 'fail',
|
||||
actual = '',
|
||||
error = 'Compilation failed: ' .. (compile_result.stderr or 'Unknown error'),
|
||||
time_ms = 0,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
local run_cmd = build_command(language_config.run, language_config.executable, substitutions)
|
||||
|
||||
local stdin_content = test_case.input .. '\n'
|
||||
|
||||
local start_time = vim.uv.hrtime()
|
||||
local result = vim
|
||||
.system(run_cmd, {
|
||||
stdin = stdin_content,
|
||||
timeout = contest_config.timeout_ms or 2000,
|
||||
text = true,
|
||||
})
|
||||
:wait()
|
||||
local execution_time = (vim.uv.hrtime() - start_time) / 1000000
|
||||
|
||||
local actual_output = (result.stdout or ''):gsub('\n$', '')
|
||||
local expected_output = test_case.expected:gsub('\n$', '')
|
||||
local ok = actual_output == expected_output
|
||||
|
||||
local status
|
||||
local timed_out = result.code == 143 or result.code == 124
|
||||
if timed_out then
|
||||
status = 'timeout'
|
||||
elseif result.code == 0 and ok then
|
||||
status = 'pass'
|
||||
else
|
||||
status = 'fail'
|
||||
end
|
||||
|
||||
local signal = nil
|
||||
if result.code >= 128 then
|
||||
signal = constants.signal_codes[result.code]
|
||||
end
|
||||
|
||||
return {
|
||||
status = status,
|
||||
actual = actual_output,
|
||||
error = result.code ~= 0 and result.stderr or nil,
|
||||
time_ms = execution_time,
|
||||
code = result.code,
|
||||
ok = ok,
|
||||
signal = signal,
|
||||
timed_out = timed_out,
|
||||
}
|
||||
end
|
||||
|
||||
---@param ctx ProblemContext
|
||||
---@param state table
|
||||
---@return boolean
|
||||
function M.load_test_cases(ctx, state)
|
||||
local test_cases = parse_test_cases_from_cache(state.platform, state.contest_id, state.problem_id)
|
||||
|
||||
if #test_cases == 0 then
|
||||
test_cases = parse_test_cases_from_files(ctx.input_file, ctx.expected_file)
|
||||
end
|
||||
|
||||
test_panel_state.test_cases = test_cases
|
||||
test_panel_state.current_index = 1
|
||||
|
||||
logger.log(('loaded %d test case(s)'):format(#test_cases))
|
||||
return #test_cases > 0
|
||||
end
|
||||
|
||||
---@param ctx ProblemContext
|
||||
---@param contest_config ContestConfig
|
||||
---@param index number
|
||||
---@return boolean
|
||||
function M.run_test_case(ctx, contest_config, index)
|
||||
local test_case = test_panel_state.test_cases[index]
|
||||
if not test_case then
|
||||
return false
|
||||
end
|
||||
|
||||
logger.log(('running test case %d'):format(index))
|
||||
test_case.status = 'running'
|
||||
|
||||
local result = run_single_test_case(ctx, contest_config, test_case)
|
||||
|
||||
test_case.status = result.status
|
||||
test_case.actual = result.actual
|
||||
test_case.error = result.error
|
||||
test_case.time_ms = result.time_ms
|
||||
test_case.code = result.code
|
||||
test_case.ok = result.ok
|
||||
test_case.signal = result.signal
|
||||
test_case.timed_out = result.timed_out
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
---@param ctx ProblemContext
|
||||
---@param contest_config ContestConfig
|
||||
---@return TestCase[]
|
||||
function M.run_all_test_cases(ctx, contest_config)
|
||||
local results = {}
|
||||
for i, _ in ipairs(test_panel_state.test_cases) do
|
||||
M.run_test_case(ctx, contest_config, i)
|
||||
table.insert(results, test_panel_state.test_cases[i])
|
||||
end
|
||||
return results
|
||||
end
|
||||
|
||||
---@return TestPanelState
|
||||
function M.get_test_panel_state()
|
||||
return test_panel_state
|
||||
end
|
||||
|
||||
return M
|
||||
371
lua/cp/ui/ansi.lua
Normal file
371
lua/cp/ui/ansi.lua
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
---@class AnsiParseResult
|
||||
---@field lines string[]
|
||||
---@field highlights Highlight[]
|
||||
|
||||
---@class Highlight
|
||||
---@field line number
|
||||
---@field col_start number
|
||||
---@field col_end number
|
||||
---@field highlight_group string
|
||||
|
||||
local M = {}
|
||||
|
||||
local dyn_hl_cache = {}
|
||||
|
||||
local ANSI_TERMINAL_COLOR_CODE_FALLBACK = {
|
||||
[0] = '#000000',
|
||||
[1] = '#800000',
|
||||
[2] = '#008000',
|
||||
[3] = '#808000',
|
||||
[4] = '#000080',
|
||||
[5] = '#800080',
|
||||
[6] = '#008080',
|
||||
[7] = '#c0c0c0',
|
||||
[8] = '#808080',
|
||||
[9] = '#ff0000',
|
||||
[10] = '#00ff00',
|
||||
[11] = '#ffff00',
|
||||
[12] = '#0000ff',
|
||||
[13] = '#ff00ff',
|
||||
[14] = '#00ffff',
|
||||
[15] = '#ffffff',
|
||||
}
|
||||
|
||||
local function xterm_to_hex(n)
|
||||
if n >= 0 and n <= 15 then
|
||||
local key = 'terminal_color_' .. n
|
||||
return vim.g[key] or ANSI_TERMINAL_COLOR_CODE_FALLBACK[n]
|
||||
end
|
||||
if n >= 16 and n <= 231 then
|
||||
local c = n - 16
|
||||
local r = math.floor(c / 36) % 6
|
||||
local g = math.floor(c / 6) % 6
|
||||
local b = c % 6
|
||||
local function level(x)
|
||||
return x == 0 and 0 or 55 + 40 * x
|
||||
end
|
||||
return ('#%02x%02x%02x'):format(level(r), level(g), level(b))
|
||||
end
|
||||
local l = 8 + 10 * (n - 232)
|
||||
return ('#%02x%02x%02x'):format(l, l, l)
|
||||
end
|
||||
|
||||
---@param s string|table
|
||||
---@return string
|
||||
function M.bytes_to_string(s)
|
||||
if type(s) == 'string' then
|
||||
return s
|
||||
end
|
||||
return table.concat(vim.tbl_map(string.char, s))
|
||||
end
|
||||
|
||||
---@param fg table|nil
|
||||
---@param bold boolean
|
||||
---@param italic boolean
|
||||
---@return string|nil
|
||||
local function ensure_hl_for(fg, bold, italic)
|
||||
if not fg and not bold and not italic then
|
||||
return nil
|
||||
end
|
||||
|
||||
local base = 'CpAnsi'
|
||||
local suffix
|
||||
local opts = {}
|
||||
|
||||
if fg and fg.kind == 'named' then
|
||||
suffix = fg.name
|
||||
elseif fg and fg.kind == 'xterm' then
|
||||
suffix = ('X%03d'):format(fg.idx)
|
||||
|
||||
opts.fg = xterm_to_hex(fg.idx) or 'NONE'
|
||||
elseif fg and fg.kind == 'rgb' then
|
||||
suffix = ('Rgb%02x%02x%02x'):format(fg.r, fg.g, fg.b)
|
||||
opts.fg = ('#%02x%02x%02x'):format(fg.r, fg.g, fg.b)
|
||||
end
|
||||
|
||||
local parts = { base }
|
||||
if bold then
|
||||
table.insert(parts, 'Bold')
|
||||
end
|
||||
if italic then
|
||||
table.insert(parts, 'Italic')
|
||||
end
|
||||
if suffix then
|
||||
table.insert(parts, suffix)
|
||||
end
|
||||
local name = table.concat(parts)
|
||||
|
||||
if not dyn_hl_cache[name] then
|
||||
if bold then
|
||||
opts.bold = true
|
||||
end
|
||||
if italic then
|
||||
opts.italic = true
|
||||
end
|
||||
vim.api.nvim_set_hl(0, name, opts)
|
||||
dyn_hl_cache[name] = true
|
||||
end
|
||||
return name
|
||||
end
|
||||
|
||||
---@param text string
|
||||
---@return AnsiParseResult
|
||||
function M.parse_ansi_text(text)
|
||||
local clean_text = text:gsub('\027%[[%d;]*[a-zA-Z]', '')
|
||||
local lines = vim.split(clean_text, '\n', { plain = true })
|
||||
|
||||
local highlights = {}
|
||||
local line_num = 0
|
||||
local col_pos = 0
|
||||
|
||||
local ansi_state = {
|
||||
bold = false,
|
||||
italic = false,
|
||||
foreground = nil,
|
||||
}
|
||||
|
||||
local function get_highlight_group()
|
||||
return ensure_hl_for(ansi_state.foreground, ansi_state.bold, ansi_state.italic)
|
||||
end
|
||||
|
||||
local function apply_highlight(start_line, start_col, end_col)
|
||||
local hl_group = get_highlight_group()
|
||||
if hl_group then
|
||||
table.insert(highlights, {
|
||||
line = start_line,
|
||||
col_start = start_col,
|
||||
col_end = end_col,
|
||||
highlight_group = hl_group,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local i = 1
|
||||
while i <= #text do
|
||||
local ansi_start, ansi_end, code, cmd = text:find('\027%[([%d;]*)([a-zA-Z])', i)
|
||||
|
||||
if ansi_start then
|
||||
if ansi_start > i then
|
||||
local segment = text:sub(i, ansi_start - 1)
|
||||
local start_line = line_num
|
||||
local start_col = col_pos
|
||||
|
||||
for char in segment:gmatch('.') do
|
||||
if char == '\n' then
|
||||
if col_pos > start_col then
|
||||
apply_highlight(start_line, start_col, col_pos)
|
||||
end
|
||||
line_num = line_num + 1
|
||||
start_line = line_num
|
||||
col_pos = 0
|
||||
start_col = 0
|
||||
else
|
||||
col_pos = col_pos + 1
|
||||
end
|
||||
end
|
||||
|
||||
if col_pos > start_col then
|
||||
apply_highlight(start_line, start_col, col_pos)
|
||||
end
|
||||
end
|
||||
|
||||
if cmd == 'm' then
|
||||
M.update_ansi_state(ansi_state, code)
|
||||
end
|
||||
i = ansi_end + 1
|
||||
else
|
||||
local segment = text:sub(i)
|
||||
if segment ~= '' then
|
||||
local start_line = line_num
|
||||
local start_col = col_pos
|
||||
|
||||
for char in segment:gmatch('.') do
|
||||
if char == '\n' then
|
||||
if col_pos > start_col then
|
||||
apply_highlight(start_line, start_col, col_pos)
|
||||
end
|
||||
line_num = line_num + 1
|
||||
start_line = line_num
|
||||
col_pos = 0
|
||||
start_col = 0
|
||||
else
|
||||
col_pos = col_pos + 1
|
||||
end
|
||||
end
|
||||
|
||||
if col_pos > start_col then
|
||||
apply_highlight(start_line, start_col, col_pos)
|
||||
end
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
lines = lines,
|
||||
highlights = highlights,
|
||||
}
|
||||
end
|
||||
|
||||
---@param ansi_state table
|
||||
---@param code_string string
|
||||
---@return nil
|
||||
function M.update_ansi_state(ansi_state, code_string)
|
||||
if code_string == '' or code_string == '0' then
|
||||
ansi_state.bold = false
|
||||
ansi_state.italic = false
|
||||
ansi_state.foreground = nil
|
||||
return
|
||||
end
|
||||
|
||||
local codes = vim.split(code_string, ';', { plain = true })
|
||||
local idx = 1
|
||||
while idx <= #codes do
|
||||
local num = tonumber(codes[idx])
|
||||
|
||||
if num == 1 then
|
||||
ansi_state.bold = true
|
||||
elseif num == 3 then
|
||||
ansi_state.italic = true
|
||||
elseif num == 22 then
|
||||
ansi_state.bold = false
|
||||
elseif num == 23 then
|
||||
ansi_state.italic = false
|
||||
elseif num and num >= 30 and num <= 37 then
|
||||
local colors = { 'Black', 'Red', 'Green', 'Yellow', 'Blue', 'Magenta', 'Cyan', 'White' }
|
||||
ansi_state.foreground = { kind = 'named', name = colors[num - 29] }
|
||||
elseif num and num >= 90 and num <= 97 then
|
||||
local colors = {
|
||||
'BrightBlack',
|
||||
'BrightRed',
|
||||
'BrightGreen',
|
||||
'BrightYellow',
|
||||
'BrightBlue',
|
||||
'BrightMagenta',
|
||||
'BrightCyan',
|
||||
'BrightWhite',
|
||||
}
|
||||
ansi_state.foreground = { kind = 'named', name = colors[num - 89] }
|
||||
elseif num == 39 then
|
||||
ansi_state.foreground = nil
|
||||
elseif num == 38 or num == 48 then
|
||||
local is_fg = (num == 38)
|
||||
local mode = tonumber(codes[idx + 1] or '')
|
||||
if mode == 5 and codes[idx + 2] then
|
||||
local pal = tonumber(codes[idx + 2]) or 0
|
||||
if is_fg then
|
||||
ansi_state.foreground = { kind = 'xterm', idx = pal }
|
||||
end
|
||||
idx = idx + 2
|
||||
elseif mode == 2 and codes[idx + 2] and codes[idx + 3] and codes[idx + 4] then
|
||||
local r = tonumber(codes[idx + 2]) or 0
|
||||
local g = tonumber(codes[idx + 3]) or 0
|
||||
local b = tonumber(codes[idx + 4]) or 0
|
||||
if is_fg then
|
||||
ansi_state.foreground = { kind = 'rgb', r = r, g = g, b = b }
|
||||
end
|
||||
idx = idx + 4
|
||||
end
|
||||
end
|
||||
|
||||
idx = idx + 1
|
||||
end
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.setup_highlight_groups()
|
||||
local color_map = {
|
||||
Black = vim.g.terminal_color_0 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[0],
|
||||
Red = vim.g.terminal_color_1 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[1],
|
||||
Green = vim.g.terminal_color_2 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[2],
|
||||
Yellow = vim.g.terminal_color_3 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[3],
|
||||
Blue = vim.g.terminal_color_4 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[4],
|
||||
Magenta = vim.g.terminal_color_5 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[5],
|
||||
Cyan = vim.g.terminal_color_6 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[6],
|
||||
White = vim.g.terminal_color_7 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[7],
|
||||
BrightBlack = vim.g.terminal_color_8 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[8],
|
||||
BrightRed = vim.g.terminal_color_9 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[9],
|
||||
BrightGreen = vim.g.terminal_color_10 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[10],
|
||||
BrightYellow = vim.g.terminal_color_11 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[11],
|
||||
BrightBlue = vim.g.terminal_color_12 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[12],
|
||||
BrightMagenta = vim.g.terminal_color_13 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[13],
|
||||
BrightCyan = vim.g.terminal_color_14 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[14],
|
||||
BrightWhite = vim.g.terminal_color_15 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[15],
|
||||
}
|
||||
|
||||
local combinations = {
|
||||
{ bold = false, italic = false },
|
||||
{ bold = true, italic = false },
|
||||
{ bold = false, italic = true },
|
||||
{ bold = true, italic = true },
|
||||
}
|
||||
|
||||
for _, combo in ipairs(combinations) do
|
||||
for color_name, terminal_color in pairs(color_map) do
|
||||
local parts = { 'CpAnsi' }
|
||||
local opts = { fg = terminal_color or 'NONE' }
|
||||
if combo.bold then
|
||||
table.insert(parts, 'Bold')
|
||||
opts.bold = true
|
||||
end
|
||||
if combo.italic then
|
||||
table.insert(parts, 'Italic')
|
||||
opts.italic = true
|
||||
end
|
||||
table.insert(parts, color_name)
|
||||
local hl_name = table.concat(parts)
|
||||
vim.api.nvim_set_hl(0, hl_name, opts)
|
||||
end
|
||||
end
|
||||
|
||||
vim.api.nvim_set_hl(0, 'CpAnsiBold', { bold = true })
|
||||
vim.api.nvim_set_hl(0, 'CpAnsiItalic', { italic = true })
|
||||
vim.api.nvim_set_hl(0, 'CpAnsiBoldItalic', { bold = true, italic = true })
|
||||
|
||||
for _, combo in ipairs(combinations) do
|
||||
for color_name, _ in pairs(color_map) do
|
||||
local parts = { 'CpAnsi' }
|
||||
if combo.bold then
|
||||
table.insert(parts, 'Bold')
|
||||
end
|
||||
if combo.italic then
|
||||
table.insert(parts, 'Italic')
|
||||
end
|
||||
table.insert(parts, color_name)
|
||||
local hl_name = table.concat(parts)
|
||||
dyn_hl_cache[hl_name] = true
|
||||
end
|
||||
end
|
||||
|
||||
dyn_hl_cache['CpAnsiBold'] = true
|
||||
dyn_hl_cache['CpAnsiItalic'] = true
|
||||
dyn_hl_cache['CpAnsiBoldItalic'] = true
|
||||
end
|
||||
|
||||
---@param text string
|
||||
---@return string[]
|
||||
function M.debug_ansi_tokens(text)
|
||||
local out = {}
|
||||
local i = 1
|
||||
while true do
|
||||
local s, e, codes, cmd = text:find('\027%[([%d;]*)([a-zA-Z])', i)
|
||||
if not s then
|
||||
break
|
||||
end
|
||||
table.insert(out, ('ESC[%s%s'):format(codes, cmd))
|
||||
i = e + 1
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
---@param s string
|
||||
---@return string
|
||||
function M.hex_dump(s)
|
||||
local t = {}
|
||||
for i = 1, #s do
|
||||
t[#t + 1] = ('%02X'):format(s:byte(i))
|
||||
end
|
||||
return table.concat(t, ' ')
|
||||
end
|
||||
|
||||
return M
|
||||
204
lua/cp/ui/diff.lua
Normal file
204
lua/cp/ui/diff.lua
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
---@class DiffResult
|
||||
---@field content string[]
|
||||
---@field highlights Highlight[]?
|
||||
---@field raw_diff string?
|
||||
|
||||
---@class DiffBackend
|
||||
---@field name string
|
||||
---@field render fun(expected: string, actual: string): DiffResult
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type DiffBackend
|
||||
local vim_backend = {
|
||||
name = 'vim',
|
||||
render = function(_, actual)
|
||||
local actual_lines = vim.split(actual, '\n', { plain = true })
|
||||
|
||||
return {
|
||||
content = actual_lines,
|
||||
highlights = nil,
|
||||
}
|
||||
end,
|
||||
}
|
||||
|
||||
---@type DiffBackend
|
||||
local none_backend = {
|
||||
name = 'none',
|
||||
render = function(expected, actual)
|
||||
local expected_lines = vim.split(expected, '\n', { plain = true, trimempty = true })
|
||||
local actual_lines = vim.split(actual, '\n', { plain = true })
|
||||
|
||||
return {
|
||||
content = { expected = expected_lines, actual = actual_lines },
|
||||
highlights = {},
|
||||
}
|
||||
end,
|
||||
}
|
||||
|
||||
---@type DiffBackend
|
||||
local git_backend = {
|
||||
name = 'git',
|
||||
render = function(expected, actual)
|
||||
local tmp_expected = vim.fn.tempname()
|
||||
local tmp_actual = vim.fn.tempname()
|
||||
|
||||
vim.fn.writefile(vim.split(expected, '\n', { plain = true }), tmp_expected)
|
||||
vim.fn.writefile(vim.split(actual, '\n', { plain = true }), tmp_actual)
|
||||
|
||||
local cmd = {
|
||||
'git',
|
||||
'diff',
|
||||
'--no-index',
|
||||
'--word-diff=plain',
|
||||
'--word-diff-regex=.',
|
||||
'--no-prefix',
|
||||
tmp_expected,
|
||||
tmp_actual,
|
||||
}
|
||||
|
||||
local result = vim.system(cmd, { text = true }):wait()
|
||||
|
||||
vim.fn.delete(tmp_expected)
|
||||
vim.fn.delete(tmp_actual)
|
||||
|
||||
if result.code == 0 then
|
||||
return {
|
||||
content = vim.split(actual, '\n', { plain = true }),
|
||||
highlights = {},
|
||||
}
|
||||
else
|
||||
local diff_content = result.stdout or ''
|
||||
local lines = {}
|
||||
local highlights = {}
|
||||
local line_num = 0
|
||||
|
||||
for line in diff_content:gmatch('[^\n]*') do
|
||||
if
|
||||
line:match('^[%s%+%-]')
|
||||
or (not line:match('^[@%-+]') and not line:match('^index') and not line:match('^diff'))
|
||||
then
|
||||
local clean_line = line
|
||||
if line:match('^[%+%-]') then
|
||||
clean_line = line:sub(2)
|
||||
end
|
||||
|
||||
local col_pos = 0
|
||||
local processed_line = ''
|
||||
local i = 1
|
||||
|
||||
while i <= #clean_line do
|
||||
local removed_start, removed_end = clean_line:find('%[%-[^%-]*%-]', i)
|
||||
local added_start, added_end = clean_line:find('{%+[^%+]*%+}', i)
|
||||
|
||||
local next_marker_start = nil
|
||||
local marker_type = nil
|
||||
|
||||
if removed_start and (not added_start or removed_start < added_start) then
|
||||
next_marker_start = removed_start
|
||||
marker_type = 'removed'
|
||||
elseif added_start then
|
||||
next_marker_start = added_start
|
||||
marker_type = 'added'
|
||||
end
|
||||
|
||||
if next_marker_start then
|
||||
if next_marker_start > i then
|
||||
local before_text = clean_line:sub(i, next_marker_start - 1)
|
||||
processed_line = processed_line .. before_text
|
||||
col_pos = col_pos + #before_text
|
||||
end
|
||||
|
||||
local marker_end = (marker_type == 'removed') and removed_end or added_end
|
||||
local marker_text = clean_line:sub(next_marker_start, marker_end)
|
||||
local content_text
|
||||
|
||||
if marker_type == 'removed' then
|
||||
content_text = marker_text:sub(3, -3)
|
||||
table.insert(highlights, {
|
||||
line = line_num,
|
||||
col_start = col_pos,
|
||||
col_end = col_pos + #content_text,
|
||||
highlight_group = 'DiffDelete',
|
||||
})
|
||||
else
|
||||
content_text = marker_text:sub(3, -3)
|
||||
table.insert(highlights, {
|
||||
line = line_num,
|
||||
col_start = col_pos,
|
||||
col_end = col_pos + #content_text,
|
||||
highlight_group = 'DiffAdd',
|
||||
})
|
||||
end
|
||||
|
||||
processed_line = processed_line .. content_text
|
||||
col_pos = col_pos + #content_text
|
||||
i = marker_end + 1
|
||||
else
|
||||
local rest = clean_line:sub(i)
|
||||
processed_line = processed_line .. rest
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(lines, processed_line)
|
||||
line_num = line_num + 1
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
content = lines,
|
||||
highlights = highlights,
|
||||
raw_diff = diff_content,
|
||||
}
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
---@type table<string, DiffBackend>
|
||||
local backends = {
|
||||
none = none_backend,
|
||||
vim = vim_backend,
|
||||
git = git_backend,
|
||||
}
|
||||
|
||||
---@return string[]
|
||||
function M.get_available_backends()
|
||||
return vim.tbl_keys(backends)
|
||||
end
|
||||
|
||||
---@param name string
|
||||
---@return DiffBackend?
|
||||
function M.get_backend(name)
|
||||
return backends[name]
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function M.is_git_available()
|
||||
local result = vim.system({ 'git', '--version' }, { text = true }):wait()
|
||||
return result.code == 0
|
||||
end
|
||||
|
||||
---@param preferred_backend? string
|
||||
---@return DiffBackend
|
||||
function M.get_best_backend(preferred_backend)
|
||||
if preferred_backend and backends[preferred_backend] then
|
||||
if preferred_backend == 'git' and not M.is_git_available() then
|
||||
return backends.vim
|
||||
end
|
||||
return backends[preferred_backend]
|
||||
end
|
||||
|
||||
return backends.vim
|
||||
end
|
||||
|
||||
---@param expected string
|
||||
---@param actual string
|
||||
---@param backend_name? string
|
||||
---@return DiffResult
|
||||
function M.render_diff(expected, actual, backend_name)
|
||||
local backend = M.get_best_backend(backend_name)
|
||||
return backend.render(expected, actual)
|
||||
end
|
||||
|
||||
return M
|
||||
464
lua/cp/ui/edit.lua
Normal file
464
lua/cp/ui/edit.lua
Normal file
|
|
@ -0,0 +1,464 @@
|
|||
local M = {}
|
||||
|
||||
local cache = require('cp.cache')
|
||||
local config_module = require('cp.config')
|
||||
local helpers = require('cp.helpers')
|
||||
local logger = require('cp.log')
|
||||
local state = require('cp.state')
|
||||
local utils = require('cp.utils')
|
||||
|
||||
---@class TestBufferPair
|
||||
---@field input_buf integer
|
||||
---@field expected_buf integer
|
||||
---@field input_win integer
|
||||
---@field expected_win integer
|
||||
|
||||
---@class EditState
|
||||
---@field test_buffers TestBufferPair[]
|
||||
---@field test_cases TestCase[]
|
||||
---@field constraints ProblemConstraints?
|
||||
|
||||
---@type EditState?
|
||||
local edit_state = nil
|
||||
|
||||
local setup_keybindings
|
||||
|
||||
---@param bufnr integer
|
||||
---@return integer? test_index
|
||||
local function get_current_test_index(bufnr)
|
||||
if not edit_state then
|
||||
return nil
|
||||
end
|
||||
for i, pair in ipairs(edit_state.test_buffers) do
|
||||
if pair.input_buf == bufnr or pair.expected_buf == bufnr then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---@param index integer
|
||||
local function jump_to_test(index)
|
||||
if not edit_state then
|
||||
return
|
||||
end
|
||||
local pair = edit_state.test_buffers[index]
|
||||
if pair and vim.api.nvim_win_is_valid(pair.input_win) then
|
||||
vim.api.nvim_set_current_win(pair.input_win)
|
||||
end
|
||||
end
|
||||
|
||||
---@param delta integer
|
||||
local function navigate_test(delta)
|
||||
local current_buf = vim.api.nvim_get_current_buf()
|
||||
local current_index = get_current_test_index(current_buf)
|
||||
if not current_index or not edit_state then
|
||||
return
|
||||
end
|
||||
local new_index = current_index + delta
|
||||
if new_index < 1 or new_index > #edit_state.test_buffers then
|
||||
return
|
||||
end
|
||||
jump_to_test(new_index)
|
||||
end
|
||||
|
||||
---@param test_index integer
|
||||
local function load_test_into_buffer(test_index)
|
||||
if not edit_state then
|
||||
return
|
||||
end
|
||||
|
||||
local tc = edit_state.test_cases[test_index]
|
||||
local pair = edit_state.test_buffers[test_index]
|
||||
|
||||
if not tc or not pair then
|
||||
return
|
||||
end
|
||||
|
||||
local input_lines = vim.split(tc.input or '', '\n', { plain = true, trimempty = false })
|
||||
vim.api.nvim_buf_set_lines(pair.input_buf, 0, -1, false, input_lines)
|
||||
|
||||
local expected_lines = vim.split(tc.expected or '', '\n', { plain = true, trimempty = false })
|
||||
vim.api.nvim_buf_set_lines(pair.expected_buf, 0, -1, false, expected_lines)
|
||||
|
||||
vim.api.nvim_buf_set_name(pair.input_buf, string.format('cp://test-%d-input', test_index))
|
||||
vim.api.nvim_buf_set_name(pair.expected_buf, string.format('cp://test-%d-expected', test_index))
|
||||
end
|
||||
|
||||
local function delete_current_test()
|
||||
if not edit_state then
|
||||
return
|
||||
end
|
||||
if #edit_state.test_buffers == 1 then
|
||||
logger.log('Problems must have at least one test case.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local current_buf = vim.api.nvim_get_current_buf()
|
||||
local current_index = get_current_test_index(current_buf)
|
||||
if not current_index then
|
||||
return
|
||||
end
|
||||
|
||||
local pair = edit_state.test_buffers[current_index]
|
||||
if vim.api.nvim_win_is_valid(pair.input_win) then
|
||||
vim.api.nvim_win_close(pair.input_win, true)
|
||||
end
|
||||
if vim.api.nvim_win_is_valid(pair.expected_win) then
|
||||
vim.api.nvim_win_close(pair.expected_win, true)
|
||||
end
|
||||
if vim.api.nvim_buf_is_valid(pair.input_buf) then
|
||||
vim.api.nvim_buf_delete(pair.input_buf, { force = true })
|
||||
end
|
||||
if vim.api.nvim_buf_is_valid(pair.expected_buf) then
|
||||
vim.api.nvim_buf_delete(pair.expected_buf, { force = true })
|
||||
end
|
||||
|
||||
table.remove(edit_state.test_buffers, current_index)
|
||||
table.remove(edit_state.test_cases, current_index)
|
||||
|
||||
for i = current_index, #edit_state.test_buffers do
|
||||
load_test_into_buffer(i)
|
||||
end
|
||||
|
||||
local next_index = math.min(current_index, #edit_state.test_buffers)
|
||||
jump_to_test(next_index)
|
||||
|
||||
logger.log(('Deleted test %d'):format(current_index))
|
||||
end
|
||||
|
||||
local function add_new_test()
|
||||
if not edit_state then
|
||||
return
|
||||
end
|
||||
|
||||
local last_pair = edit_state.test_buffers[#edit_state.test_buffers]
|
||||
if not last_pair or not vim.api.nvim_win_is_valid(last_pair.input_win) then
|
||||
return
|
||||
end
|
||||
|
||||
vim.api.nvim_set_current_win(last_pair.input_win)
|
||||
vim.cmd.vsplit()
|
||||
local input_win = vim.api.nvim_get_current_win()
|
||||
local input_buf = utils.create_buffer_with_options()
|
||||
vim.api.nvim_win_set_buf(input_win, input_buf)
|
||||
vim.bo[input_buf].modifiable = true
|
||||
vim.bo[input_buf].readonly = false
|
||||
vim.bo[input_buf].buftype = 'nofile'
|
||||
vim.bo[input_buf].buflisted = false
|
||||
helpers.clearcol(input_buf)
|
||||
|
||||
vim.api.nvim_set_current_win(last_pair.expected_win)
|
||||
vim.cmd.vsplit()
|
||||
local expected_win = vim.api.nvim_get_current_win()
|
||||
local expected_buf = utils.create_buffer_with_options()
|
||||
vim.api.nvim_win_set_buf(expected_win, expected_buf)
|
||||
vim.bo[expected_buf].modifiable = true
|
||||
vim.bo[expected_buf].readonly = false
|
||||
vim.bo[expected_buf].buftype = 'nofile'
|
||||
vim.bo[expected_buf].buflisted = false
|
||||
helpers.clearcol(expected_buf)
|
||||
|
||||
local new_index = #edit_state.test_buffers + 1
|
||||
local new_pair = {
|
||||
input_buf = input_buf,
|
||||
expected_buf = expected_buf,
|
||||
input_win = input_win,
|
||||
expected_win = expected_win,
|
||||
}
|
||||
table.insert(edit_state.test_buffers, new_pair)
|
||||
table.insert(edit_state.test_cases, { index = new_index, input = '', expected = '' })
|
||||
|
||||
setup_keybindings(input_buf)
|
||||
setup_keybindings(expected_buf)
|
||||
load_test_into_buffer(new_index)
|
||||
|
||||
vim.api.nvim_set_current_win(input_win)
|
||||
logger.log(('Added test %d'):format(new_index))
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
setup_keybindings = function(buf)
|
||||
local config = config_module.get_config()
|
||||
local keys = config.ui.edit
|
||||
|
||||
if keys.save_and_exit_key then
|
||||
vim.keymap.set('n', keys.save_and_exit_key, function()
|
||||
M.toggle_edit()
|
||||
end, { buffer = buf, silent = true, desc = 'Save and exit test editor' })
|
||||
end
|
||||
|
||||
if keys.next_test_key then
|
||||
vim.keymap.set('n', keys.next_test_key, function()
|
||||
navigate_test(1)
|
||||
end, { buffer = buf, silent = true, desc = 'Next test' })
|
||||
end
|
||||
|
||||
if keys.prev_test_key then
|
||||
vim.keymap.set('n', keys.prev_test_key, function()
|
||||
navigate_test(-1)
|
||||
end, { buffer = buf, silent = true, desc = 'Previous test' })
|
||||
end
|
||||
|
||||
if keys.delete_test_key then
|
||||
vim.keymap.set(
|
||||
'n',
|
||||
keys.delete_test_key,
|
||||
delete_current_test,
|
||||
{ buffer = buf, silent = true, desc = 'Delete test' }
|
||||
)
|
||||
end
|
||||
|
||||
if keys.add_test_key then
|
||||
vim.keymap.set(
|
||||
'n',
|
||||
keys.add_test_key,
|
||||
add_new_test,
|
||||
{ buffer = buf, silent = true, desc = 'Add test' }
|
||||
)
|
||||
end
|
||||
|
||||
local augroup = vim.api.nvim_create_augroup('cp_edit_guard', { clear = false })
|
||||
vim.api.nvim_create_autocmd({ 'BufDelete', 'BufWipeout' }, {
|
||||
group = augroup,
|
||||
buffer = buf,
|
||||
callback = function()
|
||||
vim.schedule(function()
|
||||
if not edit_state then
|
||||
return
|
||||
end
|
||||
|
||||
local is_tracked = false
|
||||
for _, pair in ipairs(edit_state.test_buffers) do
|
||||
if pair.input_buf == buf or pair.expected_buf == buf then
|
||||
is_tracked = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if is_tracked then
|
||||
logger.log('Test buffer closed unexpectedly. Exiting editor.', vim.log.levels.WARN)
|
||||
M.toggle_edit()
|
||||
end
|
||||
end)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
local function save_all_tests()
|
||||
if not edit_state then
|
||||
return
|
||||
end
|
||||
|
||||
local platform = state.get_platform()
|
||||
local contest_id = state.get_contest_id()
|
||||
local problem_id = state.get_problem_id()
|
||||
|
||||
if not platform or not contest_id or not problem_id then
|
||||
return
|
||||
end
|
||||
|
||||
for i, pair in ipairs(edit_state.test_buffers) do
|
||||
if
|
||||
vim.api.nvim_buf_is_valid(pair.input_buf) and vim.api.nvim_buf_is_valid(pair.expected_buf)
|
||||
then
|
||||
local input_lines = vim.api.nvim_buf_get_lines(pair.input_buf, 0, -1, false)
|
||||
local expected_lines = vim.api.nvim_buf_get_lines(pair.expected_buf, 0, -1, false)
|
||||
|
||||
edit_state.test_cases[i].input = table.concat(input_lines, '\n')
|
||||
edit_state.test_cases[i].expected = table.concat(expected_lines, '\n')
|
||||
end
|
||||
end
|
||||
|
||||
local contest_data = cache.get_contest_data(platform, contest_id)
|
||||
local is_multi_test = contest_data.problems[contest_data.index_map[problem_id]].multi_test
|
||||
or false
|
||||
|
||||
-- Generate combined test from individual test cases
|
||||
local combined_input = table.concat(
|
||||
vim.tbl_map(function(tc)
|
||||
return tc.input
|
||||
end, edit_state.test_cases),
|
||||
'\n'
|
||||
)
|
||||
local combined_expected = table.concat(
|
||||
vim.tbl_map(function(tc)
|
||||
return tc.expected
|
||||
end, edit_state.test_cases),
|
||||
'\n'
|
||||
)
|
||||
|
||||
cache.set_test_cases(
|
||||
platform,
|
||||
contest_id,
|
||||
problem_id,
|
||||
{ input = combined_input, expected = combined_expected },
|
||||
edit_state.test_cases,
|
||||
edit_state.constraints and edit_state.constraints.timeout_ms or 0,
|
||||
edit_state.constraints and edit_state.constraints.memory_mb or 0,
|
||||
false,
|
||||
is_multi_test
|
||||
)
|
||||
|
||||
local config = config_module.get_config()
|
||||
local base_name = config.filename and config.filename(platform, contest_id, problem_id, config)
|
||||
or config_module.default_filename(contest_id, problem_id)
|
||||
|
||||
vim.fn.mkdir('io', 'p')
|
||||
|
||||
for i, tc in ipairs(edit_state.test_cases) do
|
||||
local input_file = string.format('io/%s.%d.cpin', base_name, i)
|
||||
local expected_file = string.format('io/%s.%d.cpout', base_name, i)
|
||||
|
||||
local input_content = (tc.input or ''):gsub('\r', '')
|
||||
local expected_content = (tc.expected or ''):gsub('\r', '')
|
||||
|
||||
vim.fn.writefile(vim.split(input_content, '\n', { trimempty = true }), input_file)
|
||||
vim.fn.writefile(vim.split(expected_content, '\n', { trimempty = true }), expected_file)
|
||||
end
|
||||
|
||||
logger.log('Saved all test cases')
|
||||
end
|
||||
|
||||
function M.toggle_edit(test_index)
|
||||
if edit_state then
|
||||
save_all_tests()
|
||||
edit_state = nil
|
||||
|
||||
pcall(vim.api.nvim_clear_autocmds, { group = 'cp_edit_guard' })
|
||||
|
||||
local saved = state.get_saved_session()
|
||||
if saved then
|
||||
vim.fn.delete(saved)
|
||||
state.set_saved_session(nil)
|
||||
end
|
||||
|
||||
vim.cmd.only({ mods = { silent = true } })
|
||||
local source_file = state.get_source_file()
|
||||
if source_file and vim.fn.filereadable(source_file) == 1 then
|
||||
vim.cmd.edit(source_file)
|
||||
end
|
||||
|
||||
local views = require('cp.ui.views')
|
||||
views.ensure_io_view()
|
||||
|
||||
logger.log('Closed test editor')
|
||||
return
|
||||
end
|
||||
|
||||
local platform, contest_id, problem_id =
|
||||
state.get_platform(), state.get_contest_id(), state.get_problem_id()
|
||||
|
||||
if not platform or not contest_id or not problem_id then
|
||||
logger.log('No problem context. Run :CP <platform> <contest> first.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
cache.load()
|
||||
local test_cases = cache.get_test_cases(platform, contest_id, problem_id)
|
||||
|
||||
if not test_cases or #test_cases == 0 then
|
||||
logger.log('No test cases available for editing.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local timeout_ms, memory_mb = cache.get_constraints(platform, contest_id, problem_id)
|
||||
local constraints = (timeout_ms and memory_mb)
|
||||
and { timeout_ms = timeout_ms, memory_mb = memory_mb }
|
||||
or nil
|
||||
|
||||
local target_index = test_index or 1
|
||||
if target_index < 1 or target_index > #test_cases then
|
||||
logger.log(
|
||||
('Test %d does not exist (only %d tests available)'):format(target_index, #test_cases),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
local io_view_state = state.get_io_view_state()
|
||||
if io_view_state then
|
||||
if io_view_state.output_buf and vim.api.nvim_buf_is_valid(io_view_state.output_buf) then
|
||||
vim.api.nvim_buf_delete(io_view_state.output_buf, { force = true })
|
||||
end
|
||||
if io_view_state.input_buf and vim.api.nvim_buf_is_valid(io_view_state.input_buf) then
|
||||
vim.api.nvim_buf_delete(io_view_state.input_buf, { force = true })
|
||||
end
|
||||
state.set_io_view_state(nil)
|
||||
end
|
||||
|
||||
local session_file = vim.fn.tempname()
|
||||
state.set_saved_session(session_file)
|
||||
-- selene: allow(mixed_table)
|
||||
vim.cmd.mksession({ session_file, bang = true })
|
||||
vim.cmd.only({ mods = { silent = true } })
|
||||
|
||||
local test_buffers = {}
|
||||
local num_tests = #test_cases
|
||||
|
||||
for _ = 1, num_tests - 1 do
|
||||
vim.cmd.vsplit()
|
||||
end
|
||||
|
||||
vim.cmd('1 wincmd w')
|
||||
|
||||
for col = 1, num_tests do
|
||||
vim.cmd.split()
|
||||
|
||||
vim.cmd.wincmd('k')
|
||||
local input_win = vim.api.nvim_get_current_win()
|
||||
local input_buf = utils.create_buffer_with_options()
|
||||
vim.api.nvim_win_set_buf(input_win, input_buf)
|
||||
vim.bo[input_buf].modifiable = true
|
||||
vim.bo[input_buf].readonly = false
|
||||
vim.bo[input_buf].buftype = 'nofile'
|
||||
vim.bo[input_buf].buflisted = false
|
||||
helpers.clearcol(input_buf)
|
||||
|
||||
vim.cmd.wincmd('j')
|
||||
local expected_win = vim.api.nvim_get_current_win()
|
||||
local expected_buf = utils.create_buffer_with_options()
|
||||
vim.api.nvim_win_set_buf(expected_win, expected_buf)
|
||||
vim.bo[expected_buf].modifiable = true
|
||||
vim.bo[expected_buf].readonly = false
|
||||
vim.bo[expected_buf].buftype = 'nofile'
|
||||
vim.bo[expected_buf].buflisted = false
|
||||
helpers.clearcol(expected_buf)
|
||||
|
||||
test_buffers[col] = {
|
||||
input_buf = input_buf,
|
||||
expected_buf = expected_buf,
|
||||
input_win = input_win,
|
||||
expected_win = expected_win,
|
||||
}
|
||||
|
||||
vim.cmd.wincmd('k')
|
||||
vim.cmd.wincmd('l')
|
||||
end
|
||||
|
||||
edit_state = {
|
||||
test_buffers = test_buffers,
|
||||
test_cases = test_cases,
|
||||
constraints = constraints,
|
||||
}
|
||||
|
||||
for i = 1, num_tests do
|
||||
load_test_into_buffer(i)
|
||||
end
|
||||
|
||||
for _, pair in ipairs(test_buffers) do
|
||||
setup_keybindings(pair.input_buf)
|
||||
setup_keybindings(pair.expected_buf)
|
||||
end
|
||||
|
||||
if
|
||||
test_buffers[target_index]
|
||||
and vim.api.nvim_win_is_valid(test_buffers[target_index].input_win)
|
||||
then
|
||||
vim.api.nvim_set_current_win(test_buffers[target_index].input_win)
|
||||
end
|
||||
|
||||
logger.log(('Editing %d test cases'):format(num_tests))
|
||||
end
|
||||
|
||||
return M
|
||||
156
lua/cp/ui/highlight.lua
Normal file
156
lua/cp/ui/highlight.lua
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
---@class DiffHighlight
|
||||
---@field line number
|
||||
---@field col_start number
|
||||
---@field col_end number
|
||||
---@field highlight_group string
|
||||
|
||||
---@class ParsedDiff
|
||||
---@field content string[]
|
||||
---@field highlights DiffHighlight[]
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param text string Raw git diff output line
|
||||
---@return string cleaned_text, DiffHighlight[]
|
||||
local function parse_diff_line(text)
|
||||
local result_text = ''
|
||||
local highlights = {}
|
||||
local pos = 1
|
||||
|
||||
while pos <= #text do
|
||||
local removed_start, removed_end, removed_content = text:find('%[%-(.-)%-%]', pos)
|
||||
if removed_start and removed_start == pos then
|
||||
local highlight_start = #result_text
|
||||
result_text = result_text .. removed_content
|
||||
table.insert(highlights, {
|
||||
line = 0,
|
||||
col_start = highlight_start,
|
||||
col_end = #result_text,
|
||||
highlight_group = 'DiffDelete',
|
||||
})
|
||||
pos = removed_end + 1
|
||||
else
|
||||
local added_start, added_end, added_content = text:find('{%+(.-)%+}', pos)
|
||||
if added_start and added_start == pos then
|
||||
local highlight_start = #result_text
|
||||
result_text = result_text .. added_content
|
||||
table.insert(highlights, {
|
||||
line = 0,
|
||||
col_start = highlight_start,
|
||||
col_end = #result_text,
|
||||
highlight_group = 'DiffAdd',
|
||||
})
|
||||
pos = added_end + 1
|
||||
else
|
||||
result_text = result_text .. text:sub(pos, pos)
|
||||
pos = pos + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return result_text, highlights
|
||||
end
|
||||
|
||||
---@param diff_output string
|
||||
---@return ParsedDiff
|
||||
function M.parse_git_diff(diff_output)
|
||||
if diff_output == '' then
|
||||
return { content = {}, highlights = {} }
|
||||
end
|
||||
|
||||
local lines = vim.split(diff_output, '\n', { plain = true })
|
||||
local content_lines = {}
|
||||
local all_highlights = {}
|
||||
|
||||
local content_started = false
|
||||
for _, line in ipairs(lines) do
|
||||
if
|
||||
content_started
|
||||
or (
|
||||
not line:match('^@@')
|
||||
and not line:match('^%+%+%+')
|
||||
and not line:match('^%-%-%-')
|
||||
and not line:match('^index')
|
||||
and not line:match('^diff %-%-git')
|
||||
)
|
||||
then
|
||||
content_started = true
|
||||
|
||||
if line:match('^%+') then
|
||||
local clean_line = line:sub(2)
|
||||
local parsed_line, line_highlights = parse_diff_line(clean_line)
|
||||
|
||||
table.insert(content_lines, parsed_line)
|
||||
|
||||
local line_num = #content_lines
|
||||
for _, highlight in ipairs(line_highlights) do
|
||||
highlight.line = line_num - 1
|
||||
table.insert(all_highlights, highlight)
|
||||
end
|
||||
elseif not line:match('^%-') and not line:match('^\\') then
|
||||
local clean_line = line:match('^%s') and line:sub(2) or line
|
||||
local parsed_line, line_highlights = parse_diff_line(clean_line)
|
||||
|
||||
if parsed_line ~= '' then
|
||||
table.insert(content_lines, parsed_line)
|
||||
|
||||
local line_num = #content_lines
|
||||
for _, highlight in ipairs(line_highlights) do
|
||||
highlight.line = line_num - 1
|
||||
table.insert(all_highlights, highlight)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
content = content_lines,
|
||||
highlights = all_highlights,
|
||||
}
|
||||
end
|
||||
|
||||
---@param bufnr number
|
||||
---@param highlights DiffHighlight[]
|
||||
---@param namespace number
|
||||
function M.apply_highlights(bufnr, highlights, namespace)
|
||||
vim.api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1)
|
||||
|
||||
for _, highlight in ipairs(highlights) do
|
||||
if highlight.col_start < highlight.col_end then
|
||||
vim.api.nvim_buf_set_extmark(bufnr, namespace, highlight.line, highlight.col_start, {
|
||||
end_col = highlight.col_end,
|
||||
hl_group = highlight.highlight_group,
|
||||
priority = 100,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@return number
|
||||
function M.create_namespace()
|
||||
return vim.api.nvim_create_namespace('cp_diff_highlights')
|
||||
end
|
||||
|
||||
---@param bufnr number
|
||||
---@param diff_output string
|
||||
---@param namespace number
|
||||
---@return string[] content_lines
|
||||
function M.parse_and_apply_diff(bufnr, diff_output, namespace)
|
||||
local parsed = M.parse_git_diff(diff_output)
|
||||
|
||||
local was_modifiable = vim.api.nvim_get_option_value('modifiable', { buf = bufnr })
|
||||
local was_readonly = vim.api.nvim_get_option_value('readonly', { buf = bufnr })
|
||||
|
||||
vim.api.nvim_set_option_value('readonly', false, { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('modifiable', true, { buf = bufnr })
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, parsed.content)
|
||||
vim.api.nvim_set_option_value('modifiable', was_modifiable, { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('readonly', was_readonly, { buf = bufnr })
|
||||
|
||||
M.apply_highlights(bufnr, parsed.highlights, namespace)
|
||||
|
||||
return parsed.content
|
||||
end
|
||||
|
||||
return M
|
||||
320
lua/cp/ui/layouts.lua
Normal file
320
lua/cp/ui/layouts.lua
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
local M = {}
|
||||
|
||||
local helpers = require('cp.helpers')
|
||||
local utils = require('cp.utils')
|
||||
|
||||
M.DIFF_MODES = {
|
||||
['side-by-side'] = 'side-by-side',
|
||||
vim = 'vim',
|
||||
git = 'git',
|
||||
}
|
||||
|
||||
local function create_side_by_side_layout(parent_win, expected_content, actual_content)
|
||||
local expected_buf = utils.create_buffer_with_options()
|
||||
local actual_buf = utils.create_buffer_with_options()
|
||||
helpers.clearcol(expected_buf)
|
||||
helpers.clearcol(actual_buf)
|
||||
|
||||
vim.api.nvim_set_current_win(parent_win)
|
||||
vim.cmd.split()
|
||||
vim.cmd.resize(math.floor(vim.o.lines * 0.35))
|
||||
local actual_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(actual_win, actual_buf)
|
||||
|
||||
vim.cmd.vsplit()
|
||||
local expected_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(expected_win, expected_buf)
|
||||
|
||||
vim.api.nvim_set_option_value('filetype', 'cp', { buf = expected_buf })
|
||||
vim.api.nvim_set_option_value('filetype', 'cp', { buf = actual_buf })
|
||||
local label = M.DIFF_MODES['side-by-side']
|
||||
vim.api.nvim_set_option_value(
|
||||
'winbar',
|
||||
('expected (diff: %s)'):format(label),
|
||||
{ win = expected_win }
|
||||
)
|
||||
vim.api.nvim_set_option_value('winbar', ('actual (diff: %s)'):format(label), { win = actual_win })
|
||||
|
||||
local expected_lines = vim.split(expected_content, '\n', { plain = true, trimempty = true })
|
||||
local actual_lines = vim.split(actual_content, '\n', { plain = true })
|
||||
|
||||
utils.update_buffer_content(expected_buf, expected_lines, {})
|
||||
utils.update_buffer_content(actual_buf, actual_lines, {})
|
||||
|
||||
return {
|
||||
buffers = { expected_buf, actual_buf },
|
||||
windows = { expected_win, actual_win },
|
||||
mode = 'side-by-side',
|
||||
cleanup = function()
|
||||
pcall(vim.api.nvim_win_close, expected_win, true)
|
||||
pcall(vim.api.nvim_win_close, actual_win, true)
|
||||
pcall(vim.api.nvim_buf_delete, expected_buf, { force = true })
|
||||
pcall(vim.api.nvim_buf_delete, actual_buf, { force = true })
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local function create_vim_diff_layout(parent_win, expected_content, actual_content)
|
||||
local expected_buf = utils.create_buffer_with_options()
|
||||
local actual_buf = utils.create_buffer_with_options()
|
||||
helpers.clearcol(expected_buf)
|
||||
helpers.clearcol(actual_buf)
|
||||
|
||||
vim.api.nvim_set_current_win(parent_win)
|
||||
vim.cmd.split()
|
||||
vim.cmd.resize(math.floor(vim.o.lines * 0.35))
|
||||
local actual_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(actual_win, actual_buf)
|
||||
|
||||
vim.cmd.vsplit()
|
||||
local expected_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(expected_win, expected_buf)
|
||||
|
||||
vim.api.nvim_set_option_value('filetype', 'cp', { buf = expected_buf })
|
||||
vim.api.nvim_set_option_value('filetype', 'cp', { buf = actual_buf })
|
||||
local label = M.DIFF_MODES.vim
|
||||
vim.api.nvim_set_option_value(
|
||||
'winbar',
|
||||
('expected (diff: %s)'):format(label),
|
||||
{ win = expected_win }
|
||||
)
|
||||
vim.api.nvim_set_option_value('winbar', ('actual (diff: %s)'):format(label), { win = actual_win })
|
||||
|
||||
local expected_lines = vim.split(expected_content, '\n', { plain = true, trimempty = true })
|
||||
local actual_lines = vim.split(actual_content, '\n', { plain = true })
|
||||
|
||||
utils.update_buffer_content(expected_buf, expected_lines, {})
|
||||
utils.update_buffer_content(actual_buf, actual_lines, {})
|
||||
|
||||
vim.api.nvim_set_option_value('diff', true, { win = expected_win })
|
||||
vim.api.nvim_set_option_value('diff', true, { win = actual_win })
|
||||
vim.api.nvim_win_call(expected_win, function()
|
||||
vim.cmd.diffthis()
|
||||
end)
|
||||
vim.api.nvim_win_call(actual_win, function()
|
||||
vim.cmd.diffthis()
|
||||
end)
|
||||
vim.api.nvim_set_option_value('foldcolumn', '0', { win = expected_win })
|
||||
vim.api.nvim_set_option_value('foldcolumn', '0', { win = actual_win })
|
||||
|
||||
return {
|
||||
buffers = { expected_buf, actual_buf },
|
||||
windows = { expected_win, actual_win },
|
||||
mode = 'vim',
|
||||
cleanup = function()
|
||||
pcall(vim.api.nvim_win_close, expected_win, true)
|
||||
pcall(vim.api.nvim_win_close, actual_win, true)
|
||||
pcall(vim.api.nvim_buf_delete, expected_buf, { force = true })
|
||||
pcall(vim.api.nvim_buf_delete, actual_buf, { force = true })
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local function create_git_diff_layout(parent_win, expected_content, actual_content)
|
||||
local diff_buf = utils.create_buffer_with_options()
|
||||
helpers.clearcol(diff_buf)
|
||||
|
||||
vim.api.nvim_set_current_win(parent_win)
|
||||
vim.cmd.split()
|
||||
vim.cmd.resize(math.floor(vim.o.lines * 0.35))
|
||||
local diff_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(diff_win, diff_buf)
|
||||
|
||||
vim.api.nvim_set_option_value('filetype', 'cp', { buf = diff_buf })
|
||||
local label = M.DIFF_MODES.git
|
||||
vim.api.nvim_set_option_value('winbar', ('diff: %s'):format(label), { win = diff_win })
|
||||
|
||||
local diff_backend = require('cp.ui.diff')
|
||||
local backend = diff_backend.get_best_backend('git')
|
||||
local diff_result = backend.render(expected_content, actual_content)
|
||||
local highlight = require('cp.ui.highlight')
|
||||
local diff_namespace = highlight.create_namespace()
|
||||
|
||||
if diff_result.raw_diff and diff_result.raw_diff ~= '' then
|
||||
highlight.parse_and_apply_diff(diff_buf, diff_result.raw_diff, diff_namespace)
|
||||
else
|
||||
local lines = vim.split(actual_content, '\n', { plain = true })
|
||||
utils.update_buffer_content(diff_buf, lines, {})
|
||||
end
|
||||
|
||||
return {
|
||||
buffers = { diff_buf },
|
||||
windows = { diff_win },
|
||||
mode = 'git',
|
||||
cleanup = function()
|
||||
pcall(vim.api.nvim_win_close, diff_win, true)
|
||||
pcall(vim.api.nvim_buf_delete, diff_buf, { force = true })
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local function create_single_layout(parent_win, content)
|
||||
local buf = utils.create_buffer_with_options()
|
||||
local lines = vim.split(content, '\n', { plain = true })
|
||||
utils.update_buffer_content(buf, lines, {})
|
||||
|
||||
vim.api.nvim_set_current_win(parent_win)
|
||||
vim.cmd.split()
|
||||
vim.cmd.resize(math.floor(vim.o.lines * 0.35))
|
||||
local win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(win, buf)
|
||||
vim.api.nvim_set_option_value('filetype', 'cp', { buf = buf })
|
||||
|
||||
return {
|
||||
buffers = { buf },
|
||||
windows = { win },
|
||||
mode = 'single',
|
||||
cleanup = function()
|
||||
pcall(vim.api.nvim_win_close, win, true)
|
||||
pcall(vim.api.nvim_buf_delete, buf, { force = true })
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
function M.create_diff_layout(mode, parent_win, expected_content, actual_content)
|
||||
if mode == 'single' then
|
||||
return create_single_layout(parent_win, actual_content)
|
||||
elseif mode == 'side-by-side' then
|
||||
return create_side_by_side_layout(parent_win, expected_content, actual_content)
|
||||
elseif mode == 'git' then
|
||||
return create_git_diff_layout(parent_win, expected_content, actual_content)
|
||||
elseif mode == 'vim' then
|
||||
return create_vim_diff_layout(parent_win, expected_content, actual_content)
|
||||
else
|
||||
return create_side_by_side_layout(parent_win, expected_content, actual_content)
|
||||
end
|
||||
end
|
||||
|
||||
function M.update_diff_panes(
|
||||
current_diff_layout,
|
||||
current_mode,
|
||||
main_win,
|
||||
run,
|
||||
config,
|
||||
setup_keybindings_for_buffer
|
||||
)
|
||||
local test_state = run.get_panel_state()
|
||||
local current_test = test_state.test_cases[test_state.current_index]
|
||||
|
||||
if not current_test then
|
||||
return current_diff_layout, current_mode
|
||||
end
|
||||
|
||||
local expected_content = current_test.expected or ''
|
||||
local actual_content = current_test.actual or '(not run yet)'
|
||||
local actual_highlights = current_test.actual_highlights or {}
|
||||
local is_compilation_failure = current_test.error
|
||||
and current_test.error:match('Compilation failed')
|
||||
local should_show_diff = current_test.status == 'fail'
|
||||
and current_test.actual
|
||||
and not is_compilation_failure
|
||||
|
||||
if not should_show_diff then
|
||||
expected_content = expected_content
|
||||
actual_content = actual_content
|
||||
end
|
||||
|
||||
local default_mode = config.ui.panel.diff_modes[1]
|
||||
local desired_mode = is_compilation_failure and 'single' or (current_mode or default_mode)
|
||||
local highlight = require('cp.ui.highlight')
|
||||
local diff_namespace = highlight.create_namespace()
|
||||
local ansi_namespace = vim.api.nvim_create_namespace('cp_ansi_highlights')
|
||||
|
||||
if current_diff_layout and current_diff_layout.mode ~= desired_mode then
|
||||
local saved_pos = vim.api.nvim_win_get_cursor(0)
|
||||
current_diff_layout.cleanup()
|
||||
current_diff_layout = nil
|
||||
current_mode = nil
|
||||
|
||||
current_diff_layout =
|
||||
M.create_diff_layout(desired_mode, main_win, expected_content, actual_content)
|
||||
current_mode = desired_mode
|
||||
|
||||
for _, buf in ipairs(current_diff_layout.buffers) do
|
||||
setup_keybindings_for_buffer(buf)
|
||||
end
|
||||
|
||||
pcall(vim.api.nvim_win_set_cursor, 0, saved_pos)
|
||||
return current_diff_layout, current_mode
|
||||
end
|
||||
|
||||
if not current_diff_layout then
|
||||
current_diff_layout =
|
||||
M.create_diff_layout(desired_mode, main_win, expected_content, actual_content)
|
||||
current_mode = desired_mode
|
||||
|
||||
for _, buf in ipairs(current_diff_layout.buffers) do
|
||||
setup_keybindings_for_buffer(buf)
|
||||
end
|
||||
else
|
||||
if desired_mode == 'single' then
|
||||
local lines = vim.split(actual_content, '\n', { plain = true })
|
||||
utils.update_buffer_content(
|
||||
current_diff_layout.buffers[1],
|
||||
lines,
|
||||
actual_highlights,
|
||||
ansi_namespace
|
||||
)
|
||||
elseif desired_mode == 'git' then
|
||||
local diff_backend = require('cp.ui.diff')
|
||||
local backend = diff_backend.get_best_backend('git')
|
||||
local diff_result = backend.render(expected_content, actual_content)
|
||||
|
||||
if diff_result.raw_diff and diff_result.raw_diff ~= '' then
|
||||
highlight.parse_and_apply_diff(
|
||||
current_diff_layout.buffers[1],
|
||||
diff_result.raw_diff,
|
||||
diff_namespace
|
||||
)
|
||||
else
|
||||
local lines = vim.split(actual_content, '\n', { plain = true })
|
||||
utils.update_buffer_content(
|
||||
current_diff_layout.buffers[1],
|
||||
lines,
|
||||
actual_highlights,
|
||||
ansi_namespace
|
||||
)
|
||||
end
|
||||
elseif desired_mode == 'side-by-side' then
|
||||
local expected_lines = vim.split(expected_content, '\n', { plain = true, trimempty = true })
|
||||
local actual_lines = vim.split(actual_content, '\n', { plain = true })
|
||||
utils.update_buffer_content(current_diff_layout.buffers[1], expected_lines, {})
|
||||
utils.update_buffer_content(
|
||||
current_diff_layout.buffers[2],
|
||||
actual_lines,
|
||||
actual_highlights,
|
||||
ansi_namespace
|
||||
)
|
||||
else
|
||||
local expected_lines = vim.split(expected_content, '\n', { plain = true, trimempty = true })
|
||||
local actual_lines = vim.split(actual_content, '\n', { plain = true })
|
||||
utils.update_buffer_content(current_diff_layout.buffers[1], expected_lines, {})
|
||||
utils.update_buffer_content(
|
||||
current_diff_layout.buffers[2],
|
||||
actual_lines,
|
||||
actual_highlights,
|
||||
ansi_namespace
|
||||
)
|
||||
|
||||
if should_show_diff then
|
||||
vim.api.nvim_set_option_value('diff', true, { win = current_diff_layout.windows[1] })
|
||||
vim.api.nvim_set_option_value('diff', true, { win = current_diff_layout.windows[2] })
|
||||
vim.api.nvim_win_call(current_diff_layout.windows[1], function()
|
||||
vim.cmd.diffthis()
|
||||
end)
|
||||
vim.api.nvim_win_call(current_diff_layout.windows[2], function()
|
||||
vim.cmd.diffthis()
|
||||
end)
|
||||
vim.api.nvim_set_option_value('foldcolumn', '0', { win = current_diff_layout.windows[1] })
|
||||
vim.api.nvim_set_option_value('foldcolumn', '0', { win = current_diff_layout.windows[2] })
|
||||
else
|
||||
vim.api.nvim_set_option_value('diff', false, { win = current_diff_layout.windows[1] })
|
||||
vim.api.nvim_set_option_value('diff', false, { win = current_diff_layout.windows[2] })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return current_diff_layout, current_mode
|
||||
end
|
||||
|
||||
return M
|
||||
1015
lua/cp/ui/views.lua
Normal file
1015
lua/cp/ui/views.lua
Normal file
File diff suppressed because it is too large
Load diff
365
lua/cp/utils.lua
Normal file
365
lua/cp/utils.lua
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
local M = {}
|
||||
|
||||
local logger = require('cp.log')
|
||||
|
||||
local _nix_python = nil
|
||||
local _nix_discovered = false
|
||||
|
||||
local uname = vim.loop.os_uname()
|
||||
|
||||
local _time_cached = false
|
||||
local _time_path = nil
|
||||
local _time_reason = nil
|
||||
local _timeout_cached = false
|
||||
local _timeout_path = nil
|
||||
local _timeout_reason = nil
|
||||
|
||||
local function is_windows()
|
||||
return uname.sysname == 'Windows_NT'
|
||||
end
|
||||
|
||||
local function check_time_is_gnu_time(bin)
|
||||
local ok = vim.fn.executable(bin) == 1
|
||||
if not ok then
|
||||
return false
|
||||
end
|
||||
local r = vim.system({ bin, '--version' }, { text = true }):wait()
|
||||
if r and r.code == 0 and r.stdout and r.stdout:lower():find('gnu time', 1, true) then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function find_gnu_time()
|
||||
if _time_cached then
|
||||
return _time_path, _time_reason
|
||||
end
|
||||
|
||||
if is_windows() then
|
||||
_time_cached = true
|
||||
_time_path = nil
|
||||
_time_reason = 'unsupported on Windows'
|
||||
return _time_path, _time_reason
|
||||
end
|
||||
|
||||
local candidates
|
||||
if uname and uname.sysname == 'Darwin' then
|
||||
candidates = { 'gtime', '/opt/homebrew/bin/gtime', '/usr/local/bin/gtime' }
|
||||
else
|
||||
candidates = { '/usr/bin/time', 'time' }
|
||||
end
|
||||
|
||||
for _, bin in ipairs(candidates) do
|
||||
if check_time_is_gnu_time(bin) then
|
||||
_time_cached = true
|
||||
_time_path = bin
|
||||
_time_reason = nil
|
||||
return _time_path, _time_reason
|
||||
end
|
||||
end
|
||||
|
||||
_time_cached = true
|
||||
_time_path = nil
|
||||
if uname and uname.sysname == 'Darwin' then
|
||||
_time_reason = 'GNU time not found (install via: brew install coreutils)'
|
||||
else
|
||||
_time_reason = 'GNU time not found'
|
||||
end
|
||||
return _time_path, _time_reason
|
||||
end
|
||||
|
||||
---@return string|nil path to GNU time binary
|
||||
function M.time_path()
|
||||
local path = find_gnu_time()
|
||||
return path
|
||||
end
|
||||
|
||||
---@return {ok:boolean, path:string|nil, reason:string|nil}
|
||||
function M.time_capability()
|
||||
local path, reason = find_gnu_time()
|
||||
return { ok = path ~= nil, path = path, reason = reason }
|
||||
end
|
||||
|
||||
---@return string
|
||||
function M.get_plugin_path()
|
||||
local plugin_path = debug.getinfo(1, 'S').source:sub(2)
|
||||
return vim.fn.fnamemodify(plugin_path, ':h:h:h')
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function M.is_nix_build()
|
||||
return _nix_python ~= nil
|
||||
end
|
||||
|
||||
---@return string|nil
|
||||
function M.get_nix_python()
|
||||
return _nix_python
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function M.is_nix_discovered()
|
||||
return _nix_discovered
|
||||
end
|
||||
|
||||
---@param module string
|
||||
---@param plugin_path string
|
||||
---@return string[]
|
||||
function M.get_python_cmd(module, plugin_path)
|
||||
if _nix_python then
|
||||
return { _nix_python, '-m', 'scrapers.' .. module }
|
||||
end
|
||||
return { 'uv', 'run', '--directory', plugin_path, '-m', 'scrapers.' .. module }
|
||||
end
|
||||
|
||||
local python_env_setup = false
|
||||
|
||||
---@return boolean
|
||||
local function discover_nix_python()
|
||||
local cache_dir = vim.fn.stdpath('cache') .. '/cp-nvim'
|
||||
local cache_file = cache_dir .. '/nix-python'
|
||||
|
||||
local f = io.open(cache_file, 'r')
|
||||
if f then
|
||||
local cached = f:read('*l')
|
||||
f:close()
|
||||
if cached and vim.fn.executable(cached) == 1 then
|
||||
_nix_python = cached
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local plugin_path = M.get_plugin_path()
|
||||
vim.notify('[cp.nvim] Building Python environment with nix...', vim.log.levels.INFO)
|
||||
vim.cmd.redraw()
|
||||
local result = vim
|
||||
.system(
|
||||
{ 'nix', 'build', plugin_path .. '#pythonEnv', '--no-link', '--print-out-paths' },
|
||||
{ text = true }
|
||||
)
|
||||
:wait()
|
||||
|
||||
if result.code ~= 0 then
|
||||
logger.log('nix build #pythonEnv failed: ' .. (result.stderr or ''), vim.log.levels.WARN)
|
||||
return false
|
||||
end
|
||||
|
||||
local store_path = result.stdout:gsub('%s+$', '')
|
||||
local python_path = store_path .. '/bin/python3'
|
||||
|
||||
if vim.fn.executable(python_path) ~= 1 then
|
||||
logger.log('nix python not executable at ' .. python_path, vim.log.levels.WARN)
|
||||
return false
|
||||
end
|
||||
|
||||
vim.fn.mkdir(cache_dir, 'p')
|
||||
f = io.open(cache_file, 'w')
|
||||
if f then
|
||||
f:write(python_path)
|
||||
f:close()
|
||||
end
|
||||
|
||||
_nix_python = python_path
|
||||
_nix_discovered = true
|
||||
return true
|
||||
end
|
||||
|
||||
---@return boolean success
|
||||
function M.setup_python_env()
|
||||
if python_env_setup then
|
||||
return true
|
||||
end
|
||||
|
||||
if _nix_python then
|
||||
logger.log('Python env: nix (python=' .. _nix_python .. ')')
|
||||
python_env_setup = true
|
||||
return true
|
||||
end
|
||||
|
||||
if vim.fn.executable('uv') == 1 then
|
||||
local plugin_path = M.get_plugin_path()
|
||||
logger.log('Python env: uv sync (dir=' .. plugin_path .. ')')
|
||||
vim.notify('[cp.nvim] Setting up Python environment...', vim.log.levels.INFO)
|
||||
vim.cmd.redraw()
|
||||
|
||||
local env = vim.fn.environ()
|
||||
env.VIRTUAL_ENV = ''
|
||||
env.PYTHONPATH = ''
|
||||
env.CONDA_PREFIX = ''
|
||||
local result = vim
|
||||
.system({ 'uv', 'sync' }, { cwd = plugin_path, text = true, env = env })
|
||||
:wait()
|
||||
if result.code ~= 0 then
|
||||
logger.log(
|
||||
'Failed to setup Python environment: ' .. (result.stderr or ''),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return false
|
||||
end
|
||||
if result.stderr and result.stderr ~= '' then
|
||||
logger.log('uv sync stderr: ' .. result.stderr:gsub('%s+$', ''))
|
||||
end
|
||||
|
||||
python_env_setup = true
|
||||
return true
|
||||
end
|
||||
|
||||
if vim.fn.executable('nix') == 1 then
|
||||
logger.log('Python env: nix discovery')
|
||||
if discover_nix_python() then
|
||||
python_env_setup = true
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
logger.log(
|
||||
'No Python environment available. Install uv (https://docs.astral.sh/uv/) or use nix.',
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
return false
|
||||
end
|
||||
|
||||
--- Configure the buffer with good defaults
|
||||
---@param filetype? string
|
||||
function M.create_buffer_with_options(filetype)
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_set_option_value('bufhidden', 'hide', { buf = buf })
|
||||
vim.api.nvim_set_option_value('readonly', true, { buf = buf })
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = buf })
|
||||
|
||||
if filetype then
|
||||
vim.api.nvim_set_option_value('filetype', filetype, { buf = buf })
|
||||
end
|
||||
return buf
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param lines string[]
|
||||
---@param highlights? Highlight[]
|
||||
---@param namespace? integer
|
||||
function M.update_buffer_content(bufnr, lines, highlights, namespace)
|
||||
local was_readonly = vim.api.nvim_get_option_value('readonly', { buf = bufnr })
|
||||
|
||||
vim.api.nvim_set_option_value('readonly', false, { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('modifiable', true, { buf = bufnr })
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('readonly', was_readonly, { buf = bufnr })
|
||||
|
||||
if highlights and namespace then
|
||||
local highlight = require('cp.ui.highlight')
|
||||
highlight.apply_highlights(bufnr, highlights, namespace)
|
||||
end
|
||||
end
|
||||
|
||||
function M.check_required_runtime()
|
||||
if is_windows() then
|
||||
return false, 'Windows is not supported'
|
||||
end
|
||||
|
||||
if vim.fn.has('nvim-0.10.0') ~= 1 then
|
||||
return false, 'Neovim 0.10.0+ required'
|
||||
end
|
||||
|
||||
local time = M.time_capability()
|
||||
if not time.ok then
|
||||
return false, time.reason
|
||||
end
|
||||
|
||||
local timeout = M.timeout_capability()
|
||||
if not timeout.ok then
|
||||
return false, timeout.reason
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function check_timeout_is_gnu_timeout(bin)
|
||||
if vim.fn.executable(bin) ~= 1 then
|
||||
return false
|
||||
end
|
||||
local r = vim.system({ bin, '--version' }, { text = true }):wait()
|
||||
if r and r.code == 0 and r.stdout then
|
||||
local s = r.stdout:lower()
|
||||
if s:find('gnu coreutils', 1, true) or s:find('timeout %(gnu coreutils%)', 1, true) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function find_gnu_timeout()
|
||||
if _timeout_cached then
|
||||
return _timeout_path, _timeout_reason
|
||||
end
|
||||
|
||||
if is_windows() then
|
||||
_timeout_cached = true
|
||||
_timeout_path = nil
|
||||
_timeout_reason = 'unsupported on Windows'
|
||||
return _timeout_path, _timeout_reason
|
||||
end
|
||||
|
||||
local candidates
|
||||
if uname and uname.sysname == 'Darwin' then
|
||||
candidates = { 'gtimeout', '/opt/homebrew/bin/gtimeout', '/usr/local/bin/gtimeout' }
|
||||
else
|
||||
candidates = { '/usr/bin/timeout', 'timeout' }
|
||||
end
|
||||
|
||||
for _, bin in ipairs(candidates) do
|
||||
if check_timeout_is_gnu_timeout(bin) then
|
||||
_timeout_cached = true
|
||||
_timeout_path = bin
|
||||
_timeout_reason = nil
|
||||
return _timeout_path, _timeout_reason
|
||||
end
|
||||
end
|
||||
|
||||
_timeout_cached = true
|
||||
_timeout_path = nil
|
||||
if uname and uname.sysname == 'Darwin' then
|
||||
_timeout_reason = 'GNU timeout not found (install via: brew install coreutils)'
|
||||
else
|
||||
_timeout_reason = 'GNU timeout not found'
|
||||
end
|
||||
return _timeout_path, _timeout_reason
|
||||
end
|
||||
|
||||
function M.timeout_path()
|
||||
local path = find_gnu_timeout()
|
||||
return path
|
||||
end
|
||||
|
||||
function M.timeout_capability()
|
||||
local path, reason = find_gnu_timeout()
|
||||
return { ok = path ~= nil, path = path, reason = reason }
|
||||
end
|
||||
|
||||
function M.cwd_executables()
|
||||
local uv = vim.uv or vim.loop
|
||||
local req = uv.fs_scandir('.')
|
||||
if not req then
|
||||
return {}
|
||||
end
|
||||
local out = {}
|
||||
while true do
|
||||
local name, t = uv.fs_scandir_next(req)
|
||||
if not name then
|
||||
break
|
||||
end
|
||||
if t == 'file' or t == 'link' then
|
||||
local path = './' .. name
|
||||
if vim.fn.executable(path) == 1 then
|
||||
out[#out + 1] = name
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(out)
|
||||
return out
|
||||
end
|
||||
|
||||
function M.ensure_dirs()
|
||||
vim.system({ 'mkdir', '-p', 'build', 'io' }):wait()
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
local M = {}
|
||||
|
||||
local utils = require('cp.utils')
|
||||
|
||||
local function get_git_version()
|
||||
local plugin_path = debug.getinfo(1, 'S').source:sub(2)
|
||||
local plugin_root = vim.fn.fnamemodify(plugin_path, ':h:h:h')
|
||||
local plugin_root = utils.get_plugin_path()
|
||||
|
||||
local result = vim
|
||||
.system({ 'git', 'describe', '--tags', '--always', '--dirty' }, {
|
||||
|
|
|
|||
|
|
@ -1,144 +0,0 @@
|
|||
---@class WindowState
|
||||
---@field windows table<integer, WindowData>
|
||||
---@field current_win integer
|
||||
---@field layout string
|
||||
|
||||
---@class WindowData
|
||||
---@field bufnr integer
|
||||
---@field view table
|
||||
---@field width integer
|
||||
---@field height integer
|
||||
|
||||
local M = {}
|
||||
local constants = require('cp.constants')
|
||||
|
||||
---@return WindowState
|
||||
function M.save_layout()
|
||||
local windows = {}
|
||||
for _, win in ipairs(vim.api.nvim_list_wins()) do
|
||||
if vim.api.nvim_win_is_valid(win) then
|
||||
local bufnr = vim.api.nvim_win_get_buf(win)
|
||||
windows[win] = {
|
||||
bufnr = bufnr,
|
||||
view = vim.fn.winsaveview(),
|
||||
width = vim.api.nvim_win_get_width(win),
|
||||
height = vim.api.nvim_win_get_height(win),
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
windows = windows,
|
||||
current_win = vim.api.nvim_get_current_win(),
|
||||
layout = vim.fn.winrestcmd(),
|
||||
}
|
||||
end
|
||||
|
||||
---@param state? WindowState
|
||||
---@param tile_fn? fun(source_buf: integer, input_buf: integer, output_buf: integer)
|
||||
function M.restore_layout(state, tile_fn)
|
||||
vim.validate({
|
||||
state = { state, { 'table', 'nil' }, true },
|
||||
tile_fn = { tile_fn, { 'function', 'nil' }, true },
|
||||
})
|
||||
|
||||
if not state then
|
||||
return
|
||||
end
|
||||
|
||||
vim.cmd.diffoff()
|
||||
|
||||
local problem_id = vim.fn.expand('%:t:r')
|
||||
if problem_id == '' then
|
||||
for win, win_state in pairs(state.windows) do
|
||||
if vim.api.nvim_win_is_valid(win) and vim.api.nvim_buf_is_valid(win_state.bufnr) then
|
||||
local bufname = vim.api.nvim_buf_get_name(win_state.bufnr)
|
||||
if
|
||||
not bufname:match('%.in$')
|
||||
and not bufname:match('%.out$')
|
||||
and not bufname:match('%.expected$')
|
||||
then
|
||||
problem_id = vim.fn.fnamemodify(bufname, ':t:r')
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if problem_id ~= '' then
|
||||
vim.cmd('silent only')
|
||||
|
||||
local base_fp = vim.fn.getcwd()
|
||||
local input_file = ('%s/io/%s.in'):format(base_fp, problem_id)
|
||||
local output_file = ('%s/io/%s.out'):format(base_fp, problem_id)
|
||||
local source_files = vim.fn.glob(problem_id .. '.*')
|
||||
local source_file
|
||||
if source_files ~= '' then
|
||||
local files = vim.split(source_files, '\n')
|
||||
local valid_extensions = vim.tbl_keys(constants.filetype_to_language)
|
||||
for _, file in ipairs(files) do
|
||||
local ext = vim.fn.fnamemodify(file, ':e')
|
||||
if vim.tbl_contains(valid_extensions, ext) then
|
||||
source_file = file
|
||||
break
|
||||
end
|
||||
end
|
||||
source_file = source_file or files[1]
|
||||
end
|
||||
|
||||
if not source_file or vim.fn.filereadable(source_file) == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
vim.cmd.edit(source_file)
|
||||
local source_buf = vim.api.nvim_get_current_buf()
|
||||
local input_buf = vim.fn.bufnr(input_file, true)
|
||||
local output_buf = vim.fn.bufnr(output_file, true)
|
||||
|
||||
if tile_fn then
|
||||
tile_fn(source_buf, input_buf, output_buf)
|
||||
else
|
||||
M.default_tile(source_buf, input_buf, output_buf)
|
||||
end
|
||||
else
|
||||
vim.cmd(state.layout)
|
||||
|
||||
for win, win_state in pairs(state.windows) do
|
||||
if vim.api.nvim_win_is_valid(win) then
|
||||
vim.api.nvim_set_current_win(win)
|
||||
if vim.api.nvim_get_current_buf() == win_state.bufnr then
|
||||
vim.fn.winrestview(win_state.view)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if vim.api.nvim_win_is_valid(state.current_win) then
|
||||
vim.api.nvim_set_current_win(state.current_win)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param source_buf integer
|
||||
---@param input_buf integer
|
||||
---@param output_buf integer
|
||||
local function default_tile(source_buf, input_buf, output_buf)
|
||||
vim.validate({
|
||||
source_buf = { source_buf, 'number' },
|
||||
input_buf = { input_buf, 'number' },
|
||||
output_buf = { output_buf, 'number' },
|
||||
})
|
||||
|
||||
vim.api.nvim_set_current_buf(source_buf)
|
||||
vim.cmd.vsplit()
|
||||
vim.api.nvim_set_current_buf(output_buf)
|
||||
vim.bo.filetype = 'cp'
|
||||
vim.cmd(('vertical resize %d'):format(math.floor(vim.o.columns * 0.3)))
|
||||
vim.cmd.split()
|
||||
vim.api.nvim_set_current_buf(input_buf)
|
||||
vim.bo.filetype = 'cp'
|
||||
vim.cmd.wincmd('h')
|
||||
end
|
||||
|
||||
M.default_tile = default_tile
|
||||
|
||||
return M
|
||||
161
plugin/cp.lua
161
plugin/cp.lua
|
|
@ -3,10 +3,6 @@ if vim.g.loaded_cp then
|
|||
end
|
||||
vim.g.loaded_cp = 1
|
||||
|
||||
local constants = require('cp.constants')
|
||||
local platforms = constants.PLATFORMS
|
||||
local actions = constants.ACTIONS
|
||||
|
||||
vim.api.nvim_create_user_command('CP', function(opts)
|
||||
local cp = require('cp')
|
||||
cp.handle_command(opts)
|
||||
|
|
@ -14,48 +10,161 @@ end, {
|
|||
nargs = '*',
|
||||
desc = 'Competitive programming helper',
|
||||
complete = function(ArgLead, CmdLine, _)
|
||||
local constants = require('cp.constants')
|
||||
local platforms = constants.PLATFORMS
|
||||
local actions = constants.ACTIONS
|
||||
|
||||
local args = vim.split(vim.trim(CmdLine), '%s+')
|
||||
local num_args = #args
|
||||
if CmdLine:sub(-1) == ' ' then
|
||||
num_args = num_args + 1
|
||||
end
|
||||
|
||||
if num_args == 2 then
|
||||
local candidates = {}
|
||||
local cp = require('cp')
|
||||
local context = cp.get_current_context()
|
||||
if context.platform and context.contest_id then
|
||||
vim.list_extend(candidates, actions)
|
||||
local cache = require('cp.cache')
|
||||
cache.load()
|
||||
local contest_data = cache.get_contest_data(context.platform, context.contest_id)
|
||||
if contest_data and contest_data.problems then
|
||||
for _, problem in ipairs(contest_data.problems) do
|
||||
table.insert(candidates, problem.id)
|
||||
end
|
||||
end
|
||||
else
|
||||
vim.list_extend(candidates, platforms)
|
||||
end
|
||||
local function filter_candidates(candidates)
|
||||
return vim.tbl_filter(function(cmd)
|
||||
return cmd:find(ArgLead, 1, true) == 1
|
||||
end, candidates)
|
||||
elseif num_args == 4 then
|
||||
end
|
||||
|
||||
local function get_enabled_languages(platform)
|
||||
local config = require('cp.config').get_config()
|
||||
if platform and config.platforms[platform] then
|
||||
return config.platforms[platform].enabled_languages
|
||||
end
|
||||
return vim.tbl_keys(config.languages)
|
||||
end
|
||||
|
||||
if num_args == 2 then
|
||||
local candidates = {}
|
||||
local state = require('cp.state')
|
||||
local platform = state.get_platform()
|
||||
local contest_id = state.get_contest_id()
|
||||
|
||||
vim.list_extend(candidates, platforms)
|
||||
table.insert(candidates, 'cache')
|
||||
table.insert(candidates, 'pick')
|
||||
|
||||
if platform and contest_id then
|
||||
vim.list_extend(candidates, actions)
|
||||
local cache = require('cp.cache')
|
||||
cache.load()
|
||||
local contest_data = cache.get_contest_data(platform, contest_id)
|
||||
|
||||
if contest_data and contest_data.index_map then
|
||||
local ids = vim.tbl_keys(contest_data.index_map)
|
||||
table.sort(ids)
|
||||
vim.list_extend(candidates, ids)
|
||||
end
|
||||
end
|
||||
|
||||
return filter_candidates(candidates)
|
||||
elseif num_args == 3 then
|
||||
if vim.tbl_contains(platforms, args[2]) then
|
||||
local cache = require('cp.cache')
|
||||
cache.load()
|
||||
local contests = cache.get_cached_contest_ids(args[2])
|
||||
return filter_candidates(contests)
|
||||
elseif args[2] == 'cache' then
|
||||
return filter_candidates({ 'clear', 'read' })
|
||||
elseif args[2] == 'interact' then
|
||||
local utils = require('cp.utils')
|
||||
return filter_candidates(utils.cwd_executables())
|
||||
elseif args[2] == 'edit' then
|
||||
local state = require('cp.state')
|
||||
local platform = state.get_platform()
|
||||
local contest_id = state.get_contest_id()
|
||||
local problem_id = state.get_problem_id()
|
||||
local candidates = {}
|
||||
if platform and contest_id and problem_id then
|
||||
local cache = require('cp.cache')
|
||||
cache.load()
|
||||
local test_cases = cache.get_test_cases(platform, contest_id, problem_id)
|
||||
if test_cases then
|
||||
for i = 1, #test_cases do
|
||||
table.insert(candidates, tostring(i))
|
||||
end
|
||||
end
|
||||
end
|
||||
return filter_candidates(candidates)
|
||||
elseif args[2] == 'run' or args[2] == 'panel' then
|
||||
local state = require('cp.state')
|
||||
local platform = state.get_platform()
|
||||
local contest_id = state.get_contest_id()
|
||||
local problem_id = state.get_problem_id()
|
||||
local candidates = { '--debug' }
|
||||
if platform and contest_id and problem_id then
|
||||
local cache = require('cp.cache')
|
||||
cache.load()
|
||||
local test_cases = cache.get_test_cases(platform, contest_id, problem_id)
|
||||
if test_cases then
|
||||
for i = 1, #test_cases do
|
||||
table.insert(candidates, tostring(i))
|
||||
end
|
||||
end
|
||||
end
|
||||
return filter_candidates(candidates)
|
||||
elseif args[2] == 'next' or args[2] == 'prev' or args[2] == 'pick' then
|
||||
return filter_candidates({ '--lang' })
|
||||
else
|
||||
local state = require('cp.state')
|
||||
if state.get_platform() and state.get_contest_id() then
|
||||
return filter_candidates({ '--lang' })
|
||||
end
|
||||
end
|
||||
elseif num_args == 4 then
|
||||
if args[2] == 'cache' and args[3] == 'clear' then
|
||||
local candidates = vim.list_extend({}, platforms)
|
||||
table.insert(candidates, '')
|
||||
return filter_candidates(candidates)
|
||||
elseif args[3] == '--lang' then
|
||||
local platform = require('cp.state').get_platform()
|
||||
return filter_candidates(get_enabled_languages(platform))
|
||||
elseif (args[2] == 'run' or args[2] == 'panel') and tonumber(args[3]) then
|
||||
return filter_candidates({ '--debug' })
|
||||
elseif vim.tbl_contains(platforms, args[2]) then
|
||||
local cache = require('cp.cache')
|
||||
cache.load()
|
||||
local contest_data = cache.get_contest_data(args[2], args[3])
|
||||
local candidates = { '--lang' }
|
||||
if contest_data and contest_data.problems then
|
||||
local candidates = {}
|
||||
for _, problem in ipairs(contest_data.problems) do
|
||||
table.insert(candidates, problem.id)
|
||||
end
|
||||
return vim.tbl_filter(function(cmd)
|
||||
return cmd:find(ArgLead, 1, true) == 1
|
||||
end, candidates)
|
||||
end
|
||||
return filter_candidates(candidates)
|
||||
end
|
||||
elseif num_args == 5 then
|
||||
if args[2] == 'cache' and args[3] == 'clear' and vim.tbl_contains(platforms, args[4]) then
|
||||
local cache = require('cp.cache')
|
||||
cache.load()
|
||||
local contests = cache.get_cached_contest_ids(args[4])
|
||||
return filter_candidates(contests)
|
||||
elseif vim.tbl_contains(platforms, args[2]) then
|
||||
if args[4] == '--lang' then
|
||||
return filter_candidates(get_enabled_languages(args[2]))
|
||||
else
|
||||
return filter_candidates({ '--lang' })
|
||||
end
|
||||
end
|
||||
elseif num_args == 6 then
|
||||
if vim.tbl_contains(platforms, args[2]) and args[5] == '--lang' then
|
||||
return filter_candidates(get_enabled_languages(args[2]))
|
||||
end
|
||||
end
|
||||
return {}
|
||||
end,
|
||||
})
|
||||
|
||||
local function cp_action(action)
|
||||
return function()
|
||||
require('cp').handle_command({ fargs = { action } })
|
||||
end
|
||||
end
|
||||
|
||||
vim.keymap.set('n', '<Plug>(cp-run)', cp_action('run'), { desc = 'CP run tests' })
|
||||
vim.keymap.set('n', '<Plug>(cp-panel)', cp_action('panel'), { desc = 'CP open panel' })
|
||||
vim.keymap.set('n', '<Plug>(cp-edit)', cp_action('edit'), { desc = 'CP edit test cases' })
|
||||
vim.keymap.set('n', '<Plug>(cp-next)', cp_action('next'), { desc = 'CP next problem' })
|
||||
vim.keymap.set('n', '<Plug>(cp-prev)', cp_action('prev'), { desc = 'CP previous problem' })
|
||||
vim.keymap.set('n', '<Plug>(cp-pick)', cp_action('pick'), { desc = 'CP pick contest' })
|
||||
vim.keymap.set('n', '<Plug>(cp-interact)', cp_action('interact'), { desc = 'CP interactive mode' })
|
||||
|
|
|
|||
|
|
@ -1,11 +1,33 @@
|
|||
[project]
|
||||
name = "scrapers"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
description = "Competitive programming scrapers for a variety of web platforms."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"backoff>=2.2.1",
|
||||
"beautifulsoup4>=4.13.5",
|
||||
"cloudscraper>=1.2.71",
|
||||
"curl-cffi>=0.13.0",
|
||||
"httpx>=0.28.1",
|
||||
"ndjson>=0.3.1",
|
||||
"pydantic>=2.11.10",
|
||||
"requests>=2.32.5",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"types-beautifulsoup4>=4.12.0.20250516",
|
||||
"types-requests>=2.32.4.20250913",
|
||||
"pytest>=8.0.0",
|
||||
"pytest-mock>=3.12.0",
|
||||
"pre-commit>=4.3.0",
|
||||
"basedpyright>=1.31.6",
|
||||
"ruff>=0.14.2",
|
||||
"ty>=0.0.1a32",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = ["."]
|
||||
|
||||
[tool.mypy]
|
||||
ignore_missing_imports = true
|
||||
|
|
|
|||
72
readme.md
72
readme.md
|
|
@ -1,72 +0,0 @@
|
|||
# cp.nvim
|
||||
|
||||
neovim plugin for competitive programming.
|
||||
|
||||
https://github.com/user-attachments/assets/cb142535-fba0-4280-8f11-66ad1ca50ca9
|
||||
|
||||
[video config](https://github.com/barrett-ruth/dots/blob/main/nvim/lua/plugins/cp.lua)
|
||||
|
||||
> Sample test data from [codeforces](https://codeforces.com) is scraped via [cloudscraper](https://github.com/VeNoMouS/cloudscraper). Use at your own risk.
|
||||
|
||||
## Features
|
||||
|
||||
- Support for multiple online judges ([AtCoder](https://atcoder.jp/), [Codeforces](https://codeforces.com/), [CSES](https://cses.fi))
|
||||
- Multi-language support (C++, Python)
|
||||
- Automatic problem scraping and test case management
|
||||
- Integrated build, run, and debug commands
|
||||
- Enhanced test viewer with individual test case management
|
||||
- LuaSnip integration for contest-specific snippets
|
||||
|
||||
## Requirements
|
||||
|
||||
- Neovim 0.10.0+
|
||||
- [uv](https://docs.astral.sh/uv/): problem scraping (optional)
|
||||
- [LuaSnip](https://github.com/L3MON4D3/LuaSnip): contest-specific snippets (optional)
|
||||
|
||||
## Documentation
|
||||
|
||||
```vim
|
||||
:help cp.nvim
|
||||
```
|
||||
|
||||
## Philosophy
|
||||
|
||||
This plugin is highly tuned to my workflow and may not fit for you. Personally,
|
||||
I believe there are two aspects of a cp workflow:
|
||||
|
||||
- local work (i.e. coding, running test cases)
|
||||
- site work (i.e. problem reading, submitting)
|
||||
|
||||
Namely, I do not like the idea of submitting problems locally - the experience
|
||||
will never quite offer what the remote does. Therefore, cp.nvim works as
|
||||
follows:
|
||||
|
||||
1. Find a problem
|
||||
|
||||
- Browse the remote and find it
|
||||
- Read it on the remote
|
||||
|
||||
2. Set up your local environment with `:CP ...`
|
||||
|
||||
- test cases and expected output automatically scraped
|
||||
- templates automatically configured
|
||||
|
||||
3. Solve the problem locally
|
||||
|
||||
- easy to run/debug
|
||||
- easy to diff actual vs. expected output
|
||||
|
||||
4. Submit the problem (on the remote!)
|
||||
|
||||
|
||||
## Similar Projects
|
||||
|
||||
- [competitest.nvim](https://github.com/xeluxee/competitest.nvim)
|
||||
- [assistant.nvim](https://github.com/A7Lavinraj/assistant.nvim)
|
||||
|
||||
## TODO
|
||||
|
||||
- fzf/telescope integration (whichever available)
|
||||
- autocomplete with --lang and --debug
|
||||
- finer-tuned problem limits (i.e. per-problem codeforces time, memory)
|
||||
- notify discord members
|
||||
0
scrapers/__init__.py
Normal file
0
scrapers/__init__.py
Normal file
|
|
@ -1,222 +1,398 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import backoff
|
||||
import httpx
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from bs4 import BeautifulSoup, Tag
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util.retry import Retry
|
||||
|
||||
from .base import BaseScraper
|
||||
from .models import (
|
||||
CombinedTest,
|
||||
ContestListResult,
|
||||
ContestSummary,
|
||||
MetadataResult,
|
||||
ProblemSummary,
|
||||
TestCase,
|
||||
TestsResult,
|
||||
)
|
||||
|
||||
MIB_TO_MB = 1.048576
|
||||
BASE_URL = "https://atcoder.jp"
|
||||
ARCHIVE_URL = f"{BASE_URL}/contests/archive"
|
||||
TIMEOUT_SECONDS = 30
|
||||
HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
||||
}
|
||||
RETRY_STATUS = {429, 502, 503, 504}
|
||||
FATAL_STATUS = {400, 401, 403, 404, 410}
|
||||
|
||||
_session = requests.Session()
|
||||
_adapter = HTTPAdapter(
|
||||
pool_connections=100,
|
||||
pool_maxsize=100,
|
||||
max_retries=Retry(total=0),
|
||||
)
|
||||
_session.mount("https://", _adapter)
|
||||
_session.mount("http://", _adapter)
|
||||
|
||||
|
||||
def parse_problem_url(contest_id: str, problem_letter: str) -> str:
|
||||
task_id: str = f"{contest_id}_{problem_letter}"
|
||||
return f"https://atcoder.jp/contests/{contest_id}/tasks/{task_id}"
|
||||
def _give_up_requests(exc: Exception) -> bool:
|
||||
if isinstance(exc, requests.HTTPError) and exc.response is not None:
|
||||
return exc.response.status_code in FATAL_STATUS
|
||||
return False
|
||||
|
||||
|
||||
def extract_problem_from_row(row, contest_id: str) -> dict[str, str] | None:
|
||||
cells = row.find_all("td")
|
||||
if len(cells) < 2:
|
||||
return None
|
||||
|
||||
task_link = cells[1].find("a")
|
||||
if not task_link:
|
||||
return None
|
||||
|
||||
task_name = task_link.get_text(strip=True)
|
||||
task_href = task_link.get("href", "")
|
||||
|
||||
if not task_href:
|
||||
return None
|
||||
|
||||
task_id = task_href.split("/")[-1]
|
||||
if not task_id.startswith(contest_id + "_"):
|
||||
return None
|
||||
|
||||
problem_letter = task_id[len(contest_id) + 1 :]
|
||||
if not problem_letter or not task_name:
|
||||
return None
|
||||
|
||||
return {"id": problem_letter.lower(), "name": task_name}
|
||||
def _retry_after_requests(details):
|
||||
exc = details.get("exception")
|
||||
if isinstance(exc, requests.HTTPError) and exc.response is not None:
|
||||
ra = exc.response.headers.get("Retry-After")
|
||||
if ra:
|
||||
try:
|
||||
time.sleep(max(0.0, float(ra)))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
def scrape_contest_problems(contest_id: str) -> list[dict[str, str]]:
|
||||
try:
|
||||
contest_url = f"https://atcoder.jp/contests/{contest_id}/tasks"
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
}
|
||||
@backoff.on_exception(
|
||||
backoff.expo,
|
||||
(requests.ConnectionError, requests.Timeout, requests.HTTPError),
|
||||
max_tries=5,
|
||||
jitter=backoff.full_jitter,
|
||||
giveup=_give_up_requests,
|
||||
on_backoff=_retry_after_requests,
|
||||
)
|
||||
def _fetch(url: str) -> str:
|
||||
r = _session.get(url, headers=HEADERS, timeout=TIMEOUT_SECONDS)
|
||||
if r.status_code in RETRY_STATUS:
|
||||
raise requests.HTTPError(response=r)
|
||||
r.raise_for_status()
|
||||
return r.text
|
||||
|
||||
response = requests.get(contest_url, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
soup = BeautifulSoup(response.text, "html.parser")
|
||||
task_table = soup.find("table", class_="table")
|
||||
def _giveup_httpx(exc: Exception) -> bool:
|
||||
return (
|
||||
isinstance(exc, httpx.HTTPStatusError)
|
||||
and exc.response is not None
|
||||
and (exc.response.status_code in FATAL_STATUS)
|
||||
)
|
||||
|
||||
if not task_table:
|
||||
return []
|
||||
|
||||
rows = task_table.find_all("tr")[1:]
|
||||
problems = []
|
||||
@backoff.on_exception(
|
||||
backoff.expo,
|
||||
(httpx.ConnectError, httpx.ReadTimeout, httpx.HTTPStatusError),
|
||||
max_tries=5,
|
||||
jitter=backoff.full_jitter,
|
||||
giveup=_giveup_httpx,
|
||||
)
|
||||
async def _get_async(client: httpx.AsyncClient, url: str) -> str:
|
||||
r = await client.get(url, headers=HEADERS, timeout=TIMEOUT_SECONDS)
|
||||
r.raise_for_status()
|
||||
return r.text
|
||||
|
||||
for row in rows:
|
||||
problem = extract_problem_from_row(row, contest_id)
|
||||
if problem:
|
||||
problems.append(problem)
|
||||
|
||||
problems.sort(key=lambda x: x["id"])
|
||||
return problems
|
||||
def _text_from_pre(pre: Tag) -> str:
|
||||
return (
|
||||
pre.get_text(separator="\n", strip=False)
|
||||
.replace("\r", "")
|
||||
.replace("\xa0", " ")
|
||||
.rstrip("\n")
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to scrape AtCoder contest problems: {e}", file=sys.stderr)
|
||||
|
||||
def _parse_last_page(html: str) -> int:
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
nav = soup.select_one("ul.pagination")
|
||||
if not nav:
|
||||
return 1
|
||||
nums = []
|
||||
for a in nav.select("a"):
|
||||
s = a.get_text(strip=True)
|
||||
if s.isdigit():
|
||||
nums.append(int(s))
|
||||
return max(nums) if nums else 1
|
||||
|
||||
|
||||
def _parse_archive_contests(html: str) -> list[ContestSummary]:
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
tbody = soup.select_one("table.table-default tbody") or soup.select_one("tbody")
|
||||
if not tbody:
|
||||
return []
|
||||
out: list[ContestSummary] = []
|
||||
for tr in tbody.select("tr"):
|
||||
a = tr.select_one("a[href^='/contests/']")
|
||||
if not a:
|
||||
continue
|
||||
href_attr = a.get("href")
|
||||
if not isinstance(href_attr, str):
|
||||
continue
|
||||
m = re.search(r"/contests/([^/?#]+)", href_attr)
|
||||
if not m:
|
||||
continue
|
||||
cid = m.group(1)
|
||||
name = a.get_text(strip=True)
|
||||
out.append(ContestSummary(id=cid, name=name, display_name=name))
|
||||
return out
|
||||
|
||||
|
||||
def extract_test_case_from_headers(sample_headers, i: int) -> tuple[str, str] | None:
|
||||
if i >= len(sample_headers):
|
||||
return None
|
||||
|
||||
header = sample_headers[i]
|
||||
if "input" not in header.get_text().lower():
|
||||
return None
|
||||
|
||||
input_pre = header.find_next("pre")
|
||||
if not input_pre or i + 1 >= len(sample_headers):
|
||||
return None
|
||||
|
||||
next_header = sample_headers[i + 1]
|
||||
if "output" not in next_header.get_text().lower():
|
||||
return None
|
||||
|
||||
output_pre = next_header.find_next("pre")
|
||||
if not output_pre:
|
||||
return None
|
||||
|
||||
input_text = input_pre.get_text().strip().replace("\r", "")
|
||||
output_text = output_pre.get_text().strip().replace("\r", "")
|
||||
|
||||
if not input_text or not output_text:
|
||||
return None
|
||||
|
||||
return (input_text, output_text)
|
||||
def _parse_tasks_list(html: str) -> list[dict[str, str]]:
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
tbody = soup.select_one("table tbody")
|
||||
if not tbody:
|
||||
return []
|
||||
rows: list[dict[str, str]] = []
|
||||
for tr in tbody.select("tr"):
|
||||
tds = tr.select("td")
|
||||
if len(tds) < 2:
|
||||
continue
|
||||
letter = tds[0].get_text(strip=True)
|
||||
a = tds[1].select_one("a[href*='/tasks/']")
|
||||
if not a:
|
||||
continue
|
||||
href_attr = a.get("href")
|
||||
if not isinstance(href_attr, str):
|
||||
continue
|
||||
m = re.search(r"/contests/[^/]+/tasks/([^/?#]+)", href_attr)
|
||||
if not m:
|
||||
continue
|
||||
slug = m.group(1)
|
||||
title = a.get_text(strip=True)
|
||||
rows.append({"letter": letter, "title": title, "slug": slug})
|
||||
return rows
|
||||
|
||||
|
||||
def scrape(url: str) -> list[tuple[str, str]]:
|
||||
def _extract_problem_info(html: str) -> tuple[int, float, bool]:
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
txt = soup.get_text(" ", strip=True)
|
||||
timeout_ms = 0
|
||||
memory_mb = 0.0
|
||||
ts = re.search(r"Time\s*Limit:\s*([\d.]+)\s*sec", txt, flags=re.I)
|
||||
if ts:
|
||||
timeout_ms = int(float(ts.group(1)) * 1000)
|
||||
ms = re.search(r"Memory\s*Limit:\s*(\d+)\s*MiB", txt, flags=re.I)
|
||||
if ms:
|
||||
memory_mb = float(ms.group(1)) * MIB_TO_MB
|
||||
div = soup.select_one("#problem-statement")
|
||||
txt = div.get_text(" ", strip=True) if div else soup.get_text(" ", strip=True)
|
||||
interactive = "This is an interactive" in txt
|
||||
return timeout_ms, memory_mb, interactive
|
||||
|
||||
|
||||
def _extract_samples(html: str) -> list[TestCase]:
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
root = soup.select_one("#task-statement") or soup
|
||||
inputs: dict[str, str] = {}
|
||||
outputs: dict[str, str] = {}
|
||||
for h in root.find_all(re.compile(r"h[2-4]")):
|
||||
title = h.get_text(" ", strip=True)
|
||||
pre = h.find_next("pre")
|
||||
if not pre:
|
||||
continue
|
||||
t = _text_from_pre(pre)
|
||||
mi = re.search(r"Sample\s*Input\s*(\d+)", title, flags=re.I)
|
||||
mo = re.search(r"Sample\s*Output\s*(\d+)", title, flags=re.I)
|
||||
if mi:
|
||||
inputs[mi.group(1)] = t.strip()
|
||||
elif mo:
|
||||
outputs[mo.group(1)] = t.strip()
|
||||
cases: list[TestCase] = []
|
||||
for k in sorted(set(inputs) & set(outputs), key=lambda s: int(s)):
|
||||
cases.append(TestCase(input=inputs[k], expected=outputs[k]))
|
||||
return cases
|
||||
|
||||
|
||||
def _scrape_tasks_sync(contest_id: str) -> list[dict[str, str]]:
|
||||
html = _fetch(f"{BASE_URL}/contests/{contest_id}/tasks")
|
||||
return _parse_tasks_list(html)
|
||||
|
||||
|
||||
def _scrape_problem_page_sync(contest_id: str, slug: str) -> dict[str, Any]:
|
||||
html = _fetch(f"{BASE_URL}/contests/{contest_id}/tasks/{slug}")
|
||||
try:
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
}
|
||||
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
soup = BeautifulSoup(response.text, "html.parser")
|
||||
|
||||
sample_headers = soup.find_all(
|
||||
"h3", string=lambda x: x and "sample" in x.lower() if x else False
|
||||
)
|
||||
|
||||
tests = _extract_samples(html)
|
||||
except Exception:
|
||||
tests = []
|
||||
i = 0
|
||||
|
||||
while i < len(sample_headers):
|
||||
test_case = extract_test_case_from_headers(sample_headers, i)
|
||||
if test_case:
|
||||
tests.append(test_case)
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
|
||||
return tests
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error scraping AtCoder: {e}", file=sys.stderr)
|
||||
return []
|
||||
timeout_ms, memory_mb, interactive = _extract_problem_info(html)
|
||||
return {
|
||||
"tests": tests,
|
||||
"timeout_ms": timeout_ms,
|
||||
"memory_mb": memory_mb,
|
||||
"interactive": interactive,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
def _to_problem_summaries(rows: list[dict[str, str]]) -> list[ProblemSummary]:
|
||||
out: list[ProblemSummary] = []
|
||||
for r in rows:
|
||||
letter = (r.get("letter") or "").strip().upper()
|
||||
title = r.get("title") or ""
|
||||
if not letter:
|
||||
continue
|
||||
pid = letter.lower()
|
||||
out.append(ProblemSummary(id=pid, name=title))
|
||||
return out
|
||||
|
||||
|
||||
async def _fetch_all_contests_async() -> list[ContestSummary]:
|
||||
async with httpx.AsyncClient(
|
||||
limits=httpx.Limits(max_connections=100, max_keepalive_connections=100),
|
||||
) as client:
|
||||
first_html = await _get_async(client, ARCHIVE_URL)
|
||||
last = _parse_last_page(first_html)
|
||||
out = _parse_archive_contests(first_html)
|
||||
if last <= 1:
|
||||
return out
|
||||
tasks = [
|
||||
asyncio.create_task(_get_async(client, f"{ARCHIVE_URL}?page={p}"))
|
||||
for p in range(2, last + 1)
|
||||
]
|
||||
for coro in asyncio.as_completed(tasks):
|
||||
html = await coro
|
||||
out.extend(_parse_archive_contests(html))
|
||||
return out
|
||||
|
||||
|
||||
class AtcoderScraper(BaseScraper):
|
||||
@property
|
||||
def platform_name(self) -> str:
|
||||
return "atcoder"
|
||||
|
||||
async def scrape_contest_metadata(self, contest_id: str) -> MetadataResult:
|
||||
try:
|
||||
rows = await asyncio.to_thread(_scrape_tasks_sync, contest_id)
|
||||
problems = _to_problem_summaries(rows)
|
||||
if not problems:
|
||||
return self._metadata_error(
|
||||
f"No problems found for contest {contest_id}"
|
||||
)
|
||||
return MetadataResult(
|
||||
success=True,
|
||||
error="",
|
||||
contest_id=contest_id,
|
||||
problems=problems,
|
||||
url=f"https://atcoder.jp/contests/{contest_id}/tasks/{contest_id}_%s",
|
||||
)
|
||||
except Exception as e:
|
||||
return self._metadata_error(str(e))
|
||||
|
||||
async def scrape_contest_list(self) -> ContestListResult:
|
||||
try:
|
||||
contests = await _fetch_all_contests_async()
|
||||
if not contests:
|
||||
return self._contests_error("No contests found")
|
||||
return ContestListResult(success=True, error="", contests=contests)
|
||||
except Exception as e:
|
||||
return self._contests_error(str(e))
|
||||
|
||||
async def stream_tests_for_category_async(self, category_id: str) -> None:
|
||||
rows = await asyncio.to_thread(_scrape_tasks_sync, category_id)
|
||||
|
||||
async def emit(row: dict[str, str]) -> None:
|
||||
letter = (row.get("letter") or "").strip().lower()
|
||||
slug = row.get("slug") or ""
|
||||
if not letter or not slug:
|
||||
return
|
||||
data = await asyncio.to_thread(_scrape_problem_page_sync, category_id, slug)
|
||||
tests: list[TestCase] = data.get("tests", [])
|
||||
combined_input = "\n".join(t.input for t in tests) if tests else ""
|
||||
combined_expected = "\n".join(t.expected for t in tests) if tests else ""
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"problem_id": letter,
|
||||
"combined": {
|
||||
"input": combined_input,
|
||||
"expected": combined_expected,
|
||||
},
|
||||
"tests": [
|
||||
{"input": t.input, "expected": t.expected} for t in tests
|
||||
],
|
||||
"timeout_ms": data.get("timeout_ms", 0),
|
||||
"memory_mb": data.get("memory_mb", 0),
|
||||
"interactive": bool(data.get("interactive")),
|
||||
"multi_test": False,
|
||||
}
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
|
||||
await asyncio.gather(*(emit(r) for r in rows))
|
||||
|
||||
|
||||
async def main_async() -> int:
|
||||
if len(sys.argv) < 2:
|
||||
result: dict[str, str | bool] = {
|
||||
"success": False,
|
||||
"error": "Usage: atcoder.py metadata <contest_id> OR atcoder.py tests <contest_id> <problem_letter>",
|
||||
}
|
||||
print(json.dumps(result))
|
||||
sys.exit(1)
|
||||
result = MetadataResult(
|
||||
success=False,
|
||||
error="Usage: atcoder.py metadata <contest_id> OR atcoder.py tests <contest_id> OR atcoder.py contests",
|
||||
url="",
|
||||
)
|
||||
print(result.model_dump_json())
|
||||
return 1
|
||||
|
||||
mode: str = sys.argv[1]
|
||||
scraper = AtcoderScraper()
|
||||
|
||||
if mode == "metadata":
|
||||
if len(sys.argv) != 3:
|
||||
result = {
|
||||
"success": False,
|
||||
"error": "Usage: atcoder.py metadata <contest_id>",
|
||||
}
|
||||
print(json.dumps(result))
|
||||
sys.exit(1)
|
||||
result = MetadataResult(
|
||||
success=False,
|
||||
error="Usage: atcoder.py metadata <contest_id>",
|
||||
url="",
|
||||
)
|
||||
print(result.model_dump_json())
|
||||
return 1
|
||||
contest_id = sys.argv[2]
|
||||
result = await scraper.scrape_contest_metadata(contest_id)
|
||||
print(result.model_dump_json())
|
||||
return 0 if result.success else 1
|
||||
|
||||
contest_id: str = sys.argv[2]
|
||||
problems: list[dict[str, str]] = scrape_contest_problems(contest_id)
|
||||
if mode == "tests":
|
||||
if len(sys.argv) != 3:
|
||||
tests_result = TestsResult(
|
||||
success=False,
|
||||
error="Usage: atcoder.py tests <contest_id>",
|
||||
problem_id="",
|
||||
combined=CombinedTest(input="", expected=""),
|
||||
tests=[],
|
||||
timeout_ms=0,
|
||||
memory_mb=0,
|
||||
)
|
||||
print(tests_result.model_dump_json())
|
||||
return 1
|
||||
contest_id = sys.argv[2]
|
||||
await scraper.stream_tests_for_category_async(contest_id)
|
||||
return 0
|
||||
|
||||
if not problems:
|
||||
result = {
|
||||
"success": False,
|
||||
"error": f"No problems found for contest {contest_id}",
|
||||
}
|
||||
print(json.dumps(result))
|
||||
sys.exit(1)
|
||||
if mode == "contests":
|
||||
if len(sys.argv) != 2:
|
||||
contest_result = ContestListResult(
|
||||
success=False, error="Usage: atcoder.py contests"
|
||||
)
|
||||
print(contest_result.model_dump_json())
|
||||
return 1
|
||||
contest_result = await scraper.scrape_contest_list()
|
||||
print(contest_result.model_dump_json())
|
||||
return 0 if contest_result.success else 1
|
||||
|
||||
result = {
|
||||
"success": True,
|
||||
"contest_id": contest_id,
|
||||
"problems": problems,
|
||||
}
|
||||
print(json.dumps(result))
|
||||
result = MetadataResult(
|
||||
success=False,
|
||||
error="Unknown mode. Use 'metadata <contest_id>', 'tests <contest_id>', or 'contests'",
|
||||
url="",
|
||||
)
|
||||
print(result.model_dump_json())
|
||||
return 1
|
||||
|
||||
elif mode == "tests":
|
||||
if len(sys.argv) != 4:
|
||||
result = {
|
||||
"success": False,
|
||||
"error": "Usage: atcoder.py tests <contest_id> <problem_letter>",
|
||||
}
|
||||
print(json.dumps(result))
|
||||
sys.exit(1)
|
||||
|
||||
contest_id: str = sys.argv[2]
|
||||
problem_letter: str = sys.argv[3]
|
||||
problem_id: str = contest_id + problem_letter.lower()
|
||||
|
||||
url: str = parse_problem_url(contest_id, problem_letter)
|
||||
print(f"Scraping: {url}", file=sys.stderr)
|
||||
|
||||
tests: list[tuple[str, str]] = scrape(url)
|
||||
|
||||
if not tests:
|
||||
result = {
|
||||
"success": False,
|
||||
"error": f"No tests found for {contest_id} {problem_letter}",
|
||||
"problem_id": problem_id,
|
||||
"url": url,
|
||||
}
|
||||
print(json.dumps(result))
|
||||
sys.exit(1)
|
||||
|
||||
test_list: list[dict[str, str]] = []
|
||||
for input_data, output_data in tests:
|
||||
test_list.append({"input": input_data, "expected": output_data})
|
||||
|
||||
result = {
|
||||
"success": True,
|
||||
"problem_id": problem_id,
|
||||
"url": url,
|
||||
"tests": test_list,
|
||||
}
|
||||
print(json.dumps(result))
|
||||
|
||||
else:
|
||||
result = {
|
||||
"success": False,
|
||||
"error": f"Unknown mode: {mode}. Use 'metadata' or 'tests'",
|
||||
}
|
||||
print(json.dumps(result))
|
||||
sys.exit(1)
|
||||
def main() -> None:
|
||||
sys.exit(asyncio.run(main_async()))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
83
scrapers/base.py
Normal file
83
scrapers/base.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import asyncio
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from .models import CombinedTest, ContestListResult, MetadataResult, TestsResult
|
||||
|
||||
|
||||
class BaseScraper(ABC):
|
||||
@property
|
||||
@abstractmethod
|
||||
def platform_name(self) -> str: ...
|
||||
|
||||
@abstractmethod
|
||||
async def scrape_contest_metadata(self, contest_id: str) -> MetadataResult: ...
|
||||
|
||||
@abstractmethod
|
||||
async def scrape_contest_list(self) -> ContestListResult: ...
|
||||
|
||||
@abstractmethod
|
||||
async def stream_tests_for_category_async(self, category_id: str) -> None: ...
|
||||
|
||||
def _usage(self) -> str:
|
||||
name = self.platform_name
|
||||
return f"Usage: {name}.py metadata <id> | tests <id> | contests"
|
||||
|
||||
def _metadata_error(self, msg: str) -> MetadataResult:
|
||||
return MetadataResult(success=False, error=msg, url="")
|
||||
|
||||
def _tests_error(self, msg: str) -> TestsResult:
|
||||
return TestsResult(
|
||||
success=False,
|
||||
error=msg,
|
||||
problem_id="",
|
||||
combined=CombinedTest(input="", expected=""),
|
||||
tests=[],
|
||||
timeout_ms=0,
|
||||
memory_mb=0,
|
||||
)
|
||||
|
||||
def _contests_error(self, msg: str) -> ContestListResult:
|
||||
return ContestListResult(success=False, error=msg)
|
||||
|
||||
async def _run_cli_async(self, args: list[str]) -> int:
|
||||
if len(args) < 2:
|
||||
print(self._metadata_error(self._usage()).model_dump_json())
|
||||
return 1
|
||||
|
||||
mode = args[1]
|
||||
|
||||
match mode:
|
||||
case "metadata":
|
||||
if len(args) != 3:
|
||||
print(self._metadata_error(self._usage()).model_dump_json())
|
||||
return 1
|
||||
result = await self.scrape_contest_metadata(args[2])
|
||||
print(result.model_dump_json())
|
||||
return 0 if result.success else 1
|
||||
|
||||
case "tests":
|
||||
if len(args) != 3:
|
||||
print(self._tests_error(self._usage()).model_dump_json())
|
||||
return 1
|
||||
await self.stream_tests_for_category_async(args[2])
|
||||
return 0
|
||||
|
||||
case "contests":
|
||||
if len(args) != 2:
|
||||
print(self._contests_error(self._usage()).model_dump_json())
|
||||
return 1
|
||||
result = await self.scrape_contest_list()
|
||||
print(result.model_dump_json())
|
||||
return 0 if result.success else 1
|
||||
|
||||
case _:
|
||||
print(
|
||||
self._metadata_error(
|
||||
f"Unknown mode: {mode}. {self._usage()}"
|
||||
).model_dump_json()
|
||||
)
|
||||
return 1
|
||||
|
||||
def run_cli(self) -> None:
|
||||
sys.exit(asyncio.run(self._run_cli_async(sys.argv)))
|
||||
253
scrapers/codechef.py
Normal file
253
scrapers/codechef.py
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from curl_cffi import requests as curl_requests
|
||||
|
||||
from .base import BaseScraper
|
||||
from .models import (
|
||||
ContestListResult,
|
||||
ContestSummary,
|
||||
MetadataResult,
|
||||
ProblemSummary,
|
||||
TestCase,
|
||||
)
|
||||
|
||||
BASE_URL = "https://www.codechef.com"
|
||||
API_CONTESTS_ALL = "/api/list/contests/all"
|
||||
API_CONTEST = "/api/contests/{contest_id}"
|
||||
API_PROBLEM = "/api/contests/{contest_id}/problems/{problem_id}"
|
||||
PROBLEM_URL = "https://www.codechef.com/problems/{problem_id}"
|
||||
HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
}
|
||||
TIMEOUT_S = 15.0
|
||||
CONNECTIONS = 8
|
||||
MEMORY_LIMIT_RE = re.compile(
|
||||
r"Memory\s+[Ll]imit.*?([0-9.]+)\s*(MB|GB)", re.IGNORECASE | re.DOTALL
|
||||
)
|
||||
|
||||
|
||||
async def fetch_json(client: httpx.AsyncClient, path: str) -> dict:
|
||||
r = await client.get(BASE_URL + path, headers=HEADERS, timeout=TIMEOUT_S)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def _extract_memory_limit(html: str) -> float:
|
||||
m = MEMORY_LIMIT_RE.search(html)
|
||||
if not m:
|
||||
return 256.0
|
||||
value = float(m.group(1))
|
||||
unit = m.group(2).upper()
|
||||
if unit == "GB":
|
||||
return value * 1024.0
|
||||
return value
|
||||
|
||||
|
||||
def _fetch_html_sync(url: str) -> str:
|
||||
response = curl_requests.get(url, impersonate="chrome", timeout=TIMEOUT_S)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
|
||||
|
||||
class CodeChefScraper(BaseScraper):
|
||||
@property
|
||||
def platform_name(self) -> str:
|
||||
return "codechef"
|
||||
|
||||
async def scrape_contest_metadata(self, contest_id: str) -> MetadataResult:
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
data = await fetch_json(
|
||||
client, API_CONTEST.format(contest_id=contest_id)
|
||||
)
|
||||
if not data.get("problems"):
|
||||
return self._metadata_error(
|
||||
f"No problems found for contest {contest_id}"
|
||||
)
|
||||
problems = []
|
||||
for problem_code, problem_data in data["problems"].items():
|
||||
if problem_data.get("category_name") == "main":
|
||||
problems.append(
|
||||
ProblemSummary(
|
||||
id=problem_code,
|
||||
name=problem_data.get("name", problem_code),
|
||||
)
|
||||
)
|
||||
return MetadataResult(
|
||||
success=True,
|
||||
error="",
|
||||
contest_id=contest_id,
|
||||
problems=problems,
|
||||
url=f"{BASE_URL}/{contest_id}",
|
||||
)
|
||||
except Exception as e:
|
||||
return self._metadata_error(f"Failed to fetch contest {contest_id}: {e}")
|
||||
|
||||
async def scrape_contest_list(self) -> ContestListResult:
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
data = await fetch_json(client, API_CONTESTS_ALL)
|
||||
except httpx.HTTPStatusError as e:
|
||||
return self._contests_error(f"Failed to fetch contests: {e}")
|
||||
all_contests = data.get("future_contests", []) + data.get(
|
||||
"past_contests", []
|
||||
)
|
||||
max_num = 0
|
||||
for contest in all_contests:
|
||||
contest_code = contest.get("contest_code", "")
|
||||
if contest_code.startswith("START"):
|
||||
match = re.match(r"START(\d+)", contest_code)
|
||||
if match:
|
||||
num = int(match.group(1))
|
||||
max_num = max(max_num, num)
|
||||
if max_num == 0:
|
||||
return self._contests_error("No Starters contests found")
|
||||
contests = []
|
||||
sem = asyncio.Semaphore(CONNECTIONS)
|
||||
|
||||
async def fetch_divisions(i: int) -> list[ContestSummary]:
|
||||
parent_id = f"START{i}"
|
||||
async with sem:
|
||||
try:
|
||||
parent_data = await fetch_json(
|
||||
client, API_CONTEST.format(contest_id=parent_id)
|
||||
)
|
||||
except Exception as e:
|
||||
import sys
|
||||
|
||||
print(f"Error fetching {parent_id}: {e}", file=sys.stderr)
|
||||
return []
|
||||
child_contests = parent_data.get("child_contests", {})
|
||||
if not child_contests:
|
||||
return []
|
||||
base_name = f"Starters {i}"
|
||||
divisions = []
|
||||
for div_key, div_data in child_contests.items():
|
||||
div_code = div_data.get("contest_code", "")
|
||||
div_num = div_data.get("div", {}).get("div_number", "")
|
||||
if div_code and div_num:
|
||||
divisions.append(
|
||||
ContestSummary(
|
||||
id=div_code,
|
||||
name=base_name,
|
||||
display_name=f"{base_name} (Div. {div_num})",
|
||||
)
|
||||
)
|
||||
return divisions
|
||||
|
||||
tasks = [fetch_divisions(i) for i in range(1, max_num + 1)]
|
||||
for coro in asyncio.as_completed(tasks):
|
||||
divisions = await coro
|
||||
contests.extend(divisions)
|
||||
return ContestListResult(success=True, error="", contests=contests)
|
||||
|
||||
async def stream_tests_for_category_async(self, category_id: str) -> None:
|
||||
async with httpx.AsyncClient(
|
||||
limits=httpx.Limits(max_connections=CONNECTIONS)
|
||||
) as client:
|
||||
try:
|
||||
contest_data = await fetch_json(
|
||||
client, API_CONTEST.format(contest_id=category_id)
|
||||
)
|
||||
except Exception as e:
|
||||
print(
|
||||
json.dumps(
|
||||
{"error": f"Failed to fetch contest {category_id}: {str(e)}"}
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
return
|
||||
all_problems = contest_data.get("problems", {})
|
||||
if not all_problems:
|
||||
print(
|
||||
json.dumps(
|
||||
{"error": f"No problems found for contest {category_id}"}
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
return
|
||||
problems = {
|
||||
code: data
|
||||
for code, data in all_problems.items()
|
||||
if data.get("category_name") == "main"
|
||||
}
|
||||
if not problems:
|
||||
print(
|
||||
json.dumps(
|
||||
{"error": f"No main problems found for contest {category_id}"}
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
return
|
||||
sem = asyncio.Semaphore(CONNECTIONS)
|
||||
|
||||
async def run_one(problem_code: str) -> dict[str, Any]:
|
||||
async with sem:
|
||||
try:
|
||||
problem_data = await fetch_json(
|
||||
client,
|
||||
API_PROBLEM.format(
|
||||
contest_id=category_id, problem_id=problem_code
|
||||
),
|
||||
)
|
||||
sample_tests = (
|
||||
problem_data.get("problemComponents", {}).get(
|
||||
"sampleTestCases", []
|
||||
)
|
||||
or []
|
||||
)
|
||||
tests = [
|
||||
TestCase(
|
||||
input=t.get("input", "").strip(),
|
||||
expected=t.get("output", "").strip(),
|
||||
)
|
||||
for t in sample_tests
|
||||
if not t.get("isDeleted", False)
|
||||
]
|
||||
time_limit_str = problem_data.get("max_timelimit", "1")
|
||||
timeout_ms = int(float(time_limit_str) * 1000)
|
||||
problem_url = PROBLEM_URL.format(problem_id=problem_code)
|
||||
loop = asyncio.get_event_loop()
|
||||
html = await loop.run_in_executor(
|
||||
None, _fetch_html_sync, problem_url
|
||||
)
|
||||
memory_mb = _extract_memory_limit(html)
|
||||
interactive = False
|
||||
except Exception:
|
||||
tests = []
|
||||
timeout_ms = 1000
|
||||
memory_mb = 256.0
|
||||
interactive = False
|
||||
combined_input = "\n".join(t.input for t in tests) if tests else ""
|
||||
combined_expected = (
|
||||
"\n".join(t.expected for t in tests) if tests else ""
|
||||
)
|
||||
return {
|
||||
"problem_id": problem_code,
|
||||
"combined": {
|
||||
"input": combined_input,
|
||||
"expected": combined_expected,
|
||||
},
|
||||
"tests": [
|
||||
{"input": t.input, "expected": t.expected} for t in tests
|
||||
],
|
||||
"timeout_ms": timeout_ms,
|
||||
"memory_mb": memory_mb,
|
||||
"interactive": interactive,
|
||||
"multi_test": False,
|
||||
}
|
||||
|
||||
tasks = [run_one(problem_code) for problem_code in problems.keys()]
|
||||
for coro in asyncio.as_completed(tasks):
|
||||
payload = await coro
|
||||
print(json.dumps(payload), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
CodeChefScraper().run_cli()
|
||||
|
|
@ -1,270 +1,273 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import cloudscraper
|
||||
from bs4 import BeautifulSoup
|
||||
import requests
|
||||
from bs4 import BeautifulSoup, Tag
|
||||
from curl_cffi import requests as curl_requests
|
||||
|
||||
from .base import BaseScraper
|
||||
from .models import (
|
||||
ContestListResult,
|
||||
ContestSummary,
|
||||
MetadataResult,
|
||||
ProblemSummary,
|
||||
TestCase,
|
||||
)
|
||||
|
||||
BASE_URL = "https://codeforces.com"
|
||||
API_CONTEST_LIST_URL = f"{BASE_URL}/api/contest.list"
|
||||
TIMEOUT_SECONDS = 30
|
||||
HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
||||
}
|
||||
|
||||
|
||||
def scrape(url: str) -> list[tuple[str, str]]:
|
||||
try:
|
||||
scraper = cloudscraper.create_scraper()
|
||||
response = scraper.get(url, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
soup = BeautifulSoup(response.text, "html.parser")
|
||||
input_sections = soup.find_all("div", class_="input")
|
||||
output_sections = soup.find_all("div", class_="output")
|
||||
|
||||
individual_inputs = {}
|
||||
individual_outputs = {}
|
||||
|
||||
for inp_section in input_sections:
|
||||
inp_pre = inp_section.find("pre")
|
||||
if not inp_pre:
|
||||
continue
|
||||
|
||||
test_line_divs = inp_pre.find_all(
|
||||
"div", class_=lambda x: x and "test-example-line-" in x
|
||||
)
|
||||
if not test_line_divs:
|
||||
continue
|
||||
|
||||
for div in test_line_divs:
|
||||
classes = div.get("class", [])
|
||||
class_name = next(
|
||||
(
|
||||
cls
|
||||
for cls in classes
|
||||
if "test-example-line-" in cls and cls.split("-")[-1].isdigit()
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not class_name:
|
||||
continue
|
||||
|
||||
test_num = class_name.replace("test-example-line-", "")
|
||||
if test_num not in individual_inputs:
|
||||
individual_inputs[test_num] = []
|
||||
individual_inputs[test_num].append(div.get_text().strip())
|
||||
|
||||
for out_section in output_sections:
|
||||
out_pre = out_section.find("pre")
|
||||
if not out_pre:
|
||||
continue
|
||||
|
||||
test_line_divs = out_pre.find_all(
|
||||
"div", class_=lambda x: x and "test-example-line-" in x
|
||||
)
|
||||
if not test_line_divs:
|
||||
continue
|
||||
|
||||
for div in test_line_divs:
|
||||
classes = div.get("class", [])
|
||||
class_name = next(
|
||||
(
|
||||
cls
|
||||
for cls in classes
|
||||
if "test-example-line-" in cls and cls.split("-")[-1].isdigit()
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not class_name:
|
||||
continue
|
||||
|
||||
test_num = class_name.replace("test-example-line-", "")
|
||||
if test_num not in individual_outputs:
|
||||
individual_outputs[test_num] = []
|
||||
individual_outputs[test_num].append(div.get_text().strip())
|
||||
|
||||
if individual_inputs and individual_outputs:
|
||||
common_tests = set(individual_inputs.keys()) & set(
|
||||
individual_outputs.keys()
|
||||
)
|
||||
if common_tests:
|
||||
tests = []
|
||||
for test_num in sorted(common_tests):
|
||||
input_text = "\n".join(individual_inputs[test_num])
|
||||
output_text = "\n".join(individual_outputs[test_num])
|
||||
prefixed_input = "1\n" + input_text
|
||||
tests.append((prefixed_input, output_text))
|
||||
return tests
|
||||
all_inputs = []
|
||||
all_outputs = []
|
||||
|
||||
for inp_section in input_sections:
|
||||
inp_pre = inp_section.find("pre")
|
||||
if not inp_pre:
|
||||
continue
|
||||
|
||||
divs = inp_pre.find_all("div")
|
||||
if divs:
|
||||
lines = [div.get_text().strip() for div in divs]
|
||||
text = "\n".join(lines)
|
||||
else:
|
||||
text = inp_pre.get_text().replace("\r", "").strip()
|
||||
all_inputs.append(text)
|
||||
|
||||
for out_section in output_sections:
|
||||
out_pre = out_section.find("pre")
|
||||
if not out_pre:
|
||||
continue
|
||||
|
||||
divs = out_pre.find_all("div")
|
||||
if divs:
|
||||
lines = [div.get_text().strip() for div in divs]
|
||||
text = "\n".join(lines)
|
||||
else:
|
||||
text = out_pre.get_text().replace("\r", "").strip()
|
||||
all_outputs.append(text)
|
||||
|
||||
if not all_inputs or not all_outputs:
|
||||
return []
|
||||
|
||||
combined_input = "\n".join(all_inputs)
|
||||
combined_output = "\n".join(all_outputs)
|
||||
return [(combined_input, combined_output)]
|
||||
|
||||
except Exception as e:
|
||||
print(f"CloudScraper failed: {e}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
|
||||
def parse_problem_url(contest_id: str, problem_letter: str) -> str:
|
||||
def _text_from_pre(pre: Tag) -> str:
|
||||
return (
|
||||
f"https://codeforces.com/contest/{contest_id}/problem/{problem_letter.upper()}"
|
||||
pre.get_text(separator="\n", strip=False)
|
||||
.replace("\r", "")
|
||||
.replace("\xa0", " ")
|
||||
.strip()
|
||||
)
|
||||
|
||||
|
||||
def scrape_contest_problems(contest_id: str) -> list[dict[str, str]]:
|
||||
try:
|
||||
contest_url: str = f"https://codeforces.com/contest/{contest_id}"
|
||||
scraper = cloudscraper.create_scraper()
|
||||
response = scraper.get(contest_url, timeout=10)
|
||||
response.raise_for_status()
|
||||
def _extract_limits(block: Tag) -> tuple[int, float]:
|
||||
tdiv = block.find("div", class_="time-limit")
|
||||
mdiv = block.find("div", class_="memory-limit")
|
||||
timeout_ms = 0
|
||||
memory_mb = 0.0
|
||||
if tdiv:
|
||||
ttxt = tdiv.get_text(" ", strip=True)
|
||||
ts = re.search(r"(\d+)\s*seconds?", ttxt)
|
||||
if ts:
|
||||
timeout_ms = int(ts.group(1)) * 1000
|
||||
if mdiv:
|
||||
mtxt = mdiv.get_text(" ", strip=True)
|
||||
ms = re.search(r"(\d+)\s*megabytes?", mtxt)
|
||||
if ms:
|
||||
memory_mb = float(ms.group(1))
|
||||
return timeout_ms, memory_mb
|
||||
|
||||
soup = BeautifulSoup(response.text, "html.parser")
|
||||
problems: list[dict[str, str]] = []
|
||||
|
||||
problem_links = soup.find_all(
|
||||
"a", href=lambda x: x and f"/contest/{contest_id}/problem/" in x
|
||||
def _group_lines_by_id(pre: Tag) -> dict[int, list[str]]:
|
||||
groups: dict[int, list[str]] = {}
|
||||
for div in pre.find_all("div", class_="test-example-line"):
|
||||
cls = " ".join(div.get("class", []))
|
||||
m = re.search(r"\btest-example-line-(\d+)\b", cls)
|
||||
if not m:
|
||||
continue
|
||||
gid = int(m.group(1))
|
||||
groups.setdefault(gid, []).append(div.get_text("", strip=False))
|
||||
return groups
|
||||
|
||||
|
||||
def _extract_title(block: Tag) -> tuple[str, str]:
|
||||
t = block.find("div", class_="title")
|
||||
if not t:
|
||||
return "", ""
|
||||
s = t.get_text(" ", strip=True)
|
||||
parts = s.split(".", 1)
|
||||
if len(parts) != 2:
|
||||
return "", s.strip()
|
||||
return parts[0].strip().upper(), parts[1].strip()
|
||||
|
||||
|
||||
def _extract_samples(block: Tag) -> tuple[list[TestCase], bool]:
|
||||
st = block.find("div", class_="sample-test")
|
||||
if not isinstance(st, Tag):
|
||||
return [], False
|
||||
|
||||
input_pres: list[Tag] = [
|
||||
inp.find("pre")
|
||||
for inp in st.find_all("div", class_="input")
|
||||
if isinstance(inp, Tag) and inp.find("pre")
|
||||
]
|
||||
output_pres: list[Tag] = [
|
||||
out.find("pre")
|
||||
for out in st.find_all("div", class_="output")
|
||||
if isinstance(out, Tag) and out.find("pre")
|
||||
]
|
||||
input_pres = [p for p in input_pres if isinstance(p, Tag)]
|
||||
output_pres = [p for p in output_pres if isinstance(p, Tag)]
|
||||
|
||||
has_grouped = any(
|
||||
p.find("div", class_="test-example-line") for p in input_pres + output_pres
|
||||
)
|
||||
if has_grouped:
|
||||
inputs_by_gid: dict[int, list[str]] = {}
|
||||
outputs_by_gid: dict[int, list[str]] = {}
|
||||
for p in input_pres:
|
||||
g = _group_lines_by_id(p)
|
||||
for k, v in g.items():
|
||||
inputs_by_gid.setdefault(k, []).extend(v)
|
||||
for p in output_pres:
|
||||
g = _group_lines_by_id(p)
|
||||
for k, v in g.items():
|
||||
outputs_by_gid.setdefault(k, []).extend(v)
|
||||
inputs_by_gid.pop(0, None)
|
||||
outputs_by_gid.pop(0, None)
|
||||
keys = sorted(set(inputs_by_gid.keys()) & set(outputs_by_gid.keys()))
|
||||
if keys:
|
||||
samples = [
|
||||
TestCase(
|
||||
input="\n".join(inputs_by_gid[k]).strip(),
|
||||
expected="\n".join(outputs_by_gid[k]).strip(),
|
||||
)
|
||||
for k in keys
|
||||
]
|
||||
return samples, True
|
||||
|
||||
inputs = [_text_from_pre(p) for p in input_pres]
|
||||
outputs = [_text_from_pre(p) for p in output_pres]
|
||||
n = min(len(inputs), len(outputs))
|
||||
return [TestCase(input=inputs[i], expected=outputs[i]) for i in range(n)], False
|
||||
|
||||
|
||||
def _is_interactive(block: Tag) -> bool:
|
||||
ps = block.find("div", class_="problem-statement")
|
||||
txt = ps.get_text(" ", strip=True) if ps else block.get_text(" ", strip=True)
|
||||
return "This is an interactive problem" in txt
|
||||
|
||||
|
||||
def _fetch_problems_html(contest_id: str) -> str:
|
||||
url = f"{BASE_URL}/contest/{contest_id}/problems"
|
||||
response = curl_requests.get(url, impersonate="chrome", timeout=TIMEOUT_SECONDS)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
|
||||
|
||||
def _parse_all_blocks(html: str) -> list[dict[str, Any]]:
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
blocks = soup.find_all("div", class_="problem-statement")
|
||||
out: list[dict[str, Any]] = []
|
||||
for b in blocks:
|
||||
holder = b.find_parent("div", class_="problemindexholder")
|
||||
letter = (holder.get("problemindex") if holder else "").strip().upper()
|
||||
name = _extract_title(b)[1]
|
||||
if not letter:
|
||||
continue
|
||||
raw_samples, is_grouped = _extract_samples(b)
|
||||
timeout_ms, memory_mb = _extract_limits(b)
|
||||
interactive = _is_interactive(b)
|
||||
|
||||
if is_grouped and raw_samples:
|
||||
combined_input = f"{len(raw_samples)}\n" + "\n".join(
|
||||
tc.input for tc in raw_samples
|
||||
)
|
||||
combined_expected = "\n".join(tc.expected for tc in raw_samples)
|
||||
individual_tests = [
|
||||
TestCase(input=f"1\n{tc.input}", expected=tc.expected)
|
||||
for tc in raw_samples
|
||||
]
|
||||
else:
|
||||
combined_input = "\n".join(tc.input for tc in raw_samples)
|
||||
combined_expected = "\n".join(tc.expected for tc in raw_samples)
|
||||
individual_tests = raw_samples
|
||||
|
||||
out.append(
|
||||
{
|
||||
"letter": letter,
|
||||
"name": name,
|
||||
"combined_input": combined_input,
|
||||
"combined_expected": combined_expected,
|
||||
"tests": individual_tests,
|
||||
"timeout_ms": timeout_ms,
|
||||
"memory_mb": memory_mb,
|
||||
"interactive": interactive,
|
||||
"multi_test": is_grouped,
|
||||
}
|
||||
)
|
||||
|
||||
for link in problem_links:
|
||||
href: str = link.get("href", "")
|
||||
if f"/contest/{contest_id}/problem/" in href:
|
||||
problem_letter: str = href.split("/")[-1].lower()
|
||||
problem_name: str = link.get_text(strip=True)
|
||||
|
||||
if problem_letter and problem_name:
|
||||
problems.append({"id": problem_letter, "name": problem_name})
|
||||
|
||||
problems.sort(key=lambda x: x["id"])
|
||||
|
||||
seen: set[str] = set()
|
||||
unique_problems: list[dict[str, str]] = []
|
||||
for p in problems:
|
||||
if p["id"] not in seen:
|
||||
seen.add(p["id"])
|
||||
unique_problems.append(p)
|
||||
|
||||
return unique_problems
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to scrape contest problems: {e}", file=sys.stderr)
|
||||
return []
|
||||
return out
|
||||
|
||||
|
||||
def scrape_sample_tests(url: str) -> list[tuple[str, str]]:
|
||||
print(f"Scraping: {url}", file=sys.stderr)
|
||||
return scrape(url)
|
||||
def _scrape_contest_problems_sync(contest_id: str) -> list[ProblemSummary]:
|
||||
html = _fetch_problems_html(contest_id)
|
||||
blocks = _parse_all_blocks(html)
|
||||
problems: list[ProblemSummary] = []
|
||||
for b in blocks:
|
||||
pid = b["letter"].upper()
|
||||
problems.append(ProblemSummary(id=pid.lower(), name=b["name"]))
|
||||
return problems
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) < 2:
|
||||
result: dict[str, str | bool] = {
|
||||
"success": False,
|
||||
"error": "Usage: codeforces.py metadata <contest_id> OR codeforces.py tests <contest_id> <problem_letter>",
|
||||
}
|
||||
print(json.dumps(result))
|
||||
sys.exit(1)
|
||||
class CodeforcesScraper(BaseScraper):
|
||||
@property
|
||||
def platform_name(self) -> str:
|
||||
return "codeforces"
|
||||
|
||||
mode: str = sys.argv[1]
|
||||
async def scrape_contest_metadata(self, contest_id: str) -> MetadataResult:
|
||||
try:
|
||||
problems = await asyncio.to_thread(
|
||||
_scrape_contest_problems_sync, contest_id
|
||||
)
|
||||
if not problems:
|
||||
return self._metadata_error(
|
||||
f"No problems found for contest {contest_id}"
|
||||
)
|
||||
return MetadataResult(
|
||||
success=True,
|
||||
error="",
|
||||
contest_id=contest_id,
|
||||
problems=problems,
|
||||
url=f"https://codeforces.com/contest/{contest_id}/problem/%s",
|
||||
)
|
||||
except Exception as e:
|
||||
return self._metadata_error(str(e))
|
||||
|
||||
if mode == "metadata":
|
||||
if len(sys.argv) != 3:
|
||||
result: dict[str, str | bool] = {
|
||||
"success": False,
|
||||
"error": "Usage: codeforces.py metadata <contest_id>",
|
||||
}
|
||||
print(json.dumps(result))
|
||||
sys.exit(1)
|
||||
async def scrape_contest_list(self) -> ContestListResult:
|
||||
try:
|
||||
r = requests.get(API_CONTEST_LIST_URL, timeout=TIMEOUT_SECONDS)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
if data.get("status") != "OK":
|
||||
return self._contests_error("Invalid API response")
|
||||
|
||||
contest_id: str = sys.argv[2]
|
||||
problems: list[dict[str, str]] = scrape_contest_problems(contest_id)
|
||||
contests: list[ContestSummary] = []
|
||||
for c in data["result"]:
|
||||
if c.get("phase") != "FINISHED":
|
||||
continue
|
||||
cid = str(c["id"])
|
||||
name = c["name"]
|
||||
contests.append(ContestSummary(id=cid, name=name, display_name=name))
|
||||
|
||||
if not problems:
|
||||
result: dict[str, str | bool] = {
|
||||
"success": False,
|
||||
"error": f"No problems found for contest {contest_id}",
|
||||
}
|
||||
print(json.dumps(result))
|
||||
sys.exit(1)
|
||||
if not contests:
|
||||
return self._contests_error("No contests found")
|
||||
|
||||
result: dict[str, str | bool | list] = {
|
||||
"success": True,
|
||||
"contest_id": contest_id,
|
||||
"problems": problems,
|
||||
}
|
||||
print(json.dumps(result))
|
||||
return ContestListResult(success=True, error="", contests=contests)
|
||||
except Exception as e:
|
||||
return self._contests_error(str(e))
|
||||
|
||||
elif mode == "tests":
|
||||
if len(sys.argv) != 4:
|
||||
result: dict[str, str | bool] = {
|
||||
"success": False,
|
||||
"error": "Usage: codeforces.py tests <contest_id> <problem_letter>",
|
||||
}
|
||||
print(json.dumps(result))
|
||||
sys.exit(1)
|
||||
async def stream_tests_for_category_async(self, category_id: str) -> None:
|
||||
html = await asyncio.to_thread(_fetch_problems_html, category_id)
|
||||
blocks = await asyncio.to_thread(_parse_all_blocks, html)
|
||||
|
||||
contest_id: str = sys.argv[2]
|
||||
problem_letter: str = sys.argv[3]
|
||||
problem_id: str = contest_id + problem_letter.lower()
|
||||
|
||||
url: str = parse_problem_url(contest_id, problem_letter)
|
||||
tests: list[tuple[str, str]] = scrape_sample_tests(url)
|
||||
|
||||
if not tests:
|
||||
result: dict[str, str | bool] = {
|
||||
"success": False,
|
||||
"error": f"No tests found for {contest_id} {problem_letter}",
|
||||
"problem_id": problem_id,
|
||||
"url": url,
|
||||
}
|
||||
print(json.dumps(result))
|
||||
sys.exit(1)
|
||||
|
||||
test_list: list[dict[str, str]] = []
|
||||
for input_data, output_data in tests:
|
||||
test_list.append({"input": input_data, "expected": output_data})
|
||||
|
||||
result: dict[str, str | bool | list] = {
|
||||
"success": True,
|
||||
"problem_id": problem_id,
|
||||
"url": url,
|
||||
"tests": test_list,
|
||||
}
|
||||
print(json.dumps(result))
|
||||
|
||||
else:
|
||||
result: dict[str, str | bool] = {
|
||||
"success": False,
|
||||
"error": f"Unknown mode: {mode}. Use 'metadata' or 'tests'",
|
||||
}
|
||||
print(json.dumps(result))
|
||||
sys.exit(1)
|
||||
for b in blocks:
|
||||
pid = b["letter"].lower()
|
||||
tests: list[TestCase] = b.get("tests", [])
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"problem_id": pid,
|
||||
"combined": {
|
||||
"input": b.get("combined_input", ""),
|
||||
"expected": b.get("combined_expected", ""),
|
||||
},
|
||||
"tests": [
|
||||
{"input": t.input, "expected": t.expected} for t in tests
|
||||
],
|
||||
"timeout_ms": b.get("timeout_ms", 0),
|
||||
"memory_mb": b.get("memory_mb", 0),
|
||||
"interactive": bool(b.get("interactive")),
|
||||
"multi_test": bool(b.get("multi_test", False)),
|
||||
}
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
CodeforcesScraper().run_cli()
|
||||
|
|
|
|||
405
scrapers/cses.py
Executable file → Normal file
405
scrapers/cses.py
Executable file → Normal file
|
|
@ -1,223 +1,262 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
import httpx
|
||||
|
||||
from .base import BaseScraper
|
||||
from .models import (
|
||||
ContestListResult,
|
||||
ContestSummary,
|
||||
MetadataResult,
|
||||
ProblemSummary,
|
||||
TestCase,
|
||||
)
|
||||
|
||||
BASE_URL = "https://cses.fi"
|
||||
INDEX_PATH = "/problemset"
|
||||
TASK_PATH = "/problemset/task/{id}"
|
||||
HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
}
|
||||
TIMEOUT_S = 15.0
|
||||
CONNECTIONS = 8
|
||||
|
||||
|
||||
def parse_problem_url(problem_input: str) -> str | None:
|
||||
if problem_input.startswith("https://cses.fi/problemset/task/"):
|
||||
return problem_input
|
||||
elif problem_input.isdigit():
|
||||
return f"https://cses.fi/problemset/task/{problem_input}"
|
||||
return None
|
||||
def normalize_category_name(category_name: str) -> str:
|
||||
return category_name.lower().replace(" ", "_").replace("&", "and")
|
||||
|
||||
|
||||
def process_problem_element(
|
||||
element, current_category: str, all_categories: dict
|
||||
) -> str | None:
|
||||
if element.name == "h1":
|
||||
category_name = element.get_text().strip()
|
||||
if category_name not in all_categories:
|
||||
all_categories[category_name] = []
|
||||
return category_name
|
||||
def snake_to_title(name: str) -> str:
|
||||
small_words = {
|
||||
"a",
|
||||
"an",
|
||||
"the",
|
||||
"and",
|
||||
"but",
|
||||
"or",
|
||||
"nor",
|
||||
"for",
|
||||
"so",
|
||||
"yet",
|
||||
"at",
|
||||
"by",
|
||||
"in",
|
||||
"of",
|
||||
"on",
|
||||
"per",
|
||||
"to",
|
||||
"vs",
|
||||
"via",
|
||||
}
|
||||
words: list[str] = name.split("_")
|
||||
n = len(words)
|
||||
|
||||
if element.name != "a" or "/problemset/task/" not in element.get("href", ""):
|
||||
return current_category
|
||||
def fix_word(i_word):
|
||||
i, word = i_word
|
||||
lw = word.lower()
|
||||
return lw.capitalize() if i == 0 or i == n - 1 or lw not in small_words else lw
|
||||
|
||||
href = element.get("href", "")
|
||||
if not href:
|
||||
return current_category
|
||||
|
||||
problem_id = href.split("/")[-1]
|
||||
problem_name = element.get_text(strip=True)
|
||||
|
||||
if not (problem_id.isdigit() and problem_name and current_category):
|
||||
return current_category
|
||||
|
||||
all_categories[current_category].append({"id": problem_id, "name": problem_name})
|
||||
return current_category
|
||||
return " ".join(map(fix_word, enumerate(words)))
|
||||
|
||||
|
||||
def scrape_all_problems() -> dict[str, list[dict[str, str]]]:
|
||||
try:
|
||||
problemset_url = "https://cses.fi/problemset/"
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
}
|
||||
async def fetch_text(client: httpx.AsyncClient, path: str) -> str:
|
||||
r = await client.get(BASE_URL + path, headers=HEADERS, timeout=TIMEOUT_S)
|
||||
r.raise_for_status()
|
||||
return r.text
|
||||
|
||||
response = requests.get(problemset_url, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
soup = BeautifulSoup(response.text, "html.parser")
|
||||
all_categories = {}
|
||||
CATEGORY_BLOCK_RE = re.compile(
|
||||
r'<h2>(?P<cat>[^<]+)</h2>\s*<ul\s+class="task-list">(?P<body>.*?)</ul>',
|
||||
re.DOTALL,
|
||||
)
|
||||
TASK_LINK_RE = re.compile(
|
||||
r'<li\s+class="task">\s*<a\s+href="/problemset/task/(?P<id>\d+)/?">(?P<title>[^<]+)</a\s*>',
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
problem_links = soup.find_all(
|
||||
"a", href=lambda x: x and "/problemset/task/" in x
|
||||
)
|
||||
print(f"Found {len(problem_links)} problem links", file=sys.stderr)
|
||||
TITLE_RE = re.compile(
|
||||
r'<div\s+class="title-block">.*?<h1>(?P<title>[^<]+)</h1>', re.DOTALL
|
||||
)
|
||||
TIME_RE = re.compile(r"<li>\s*<b>Time limit:</b>\s*([0-9.]+)\s*s\s*</li>")
|
||||
MEM_RE = re.compile(r"<li>\s*<b>Memory limit:</b>\s*(\d+)\s*MB\s*</li>")
|
||||
SIDEBAR_CAT_RE = re.compile(
|
||||
r'<div\s+class="nav sidebar">.*?<h4>(?P<cat>[^<]+)</h4>', re.DOTALL
|
||||
)
|
||||
|
||||
current_category = None
|
||||
for element in soup.find_all(["h1", "a"]):
|
||||
current_category = process_problem_element(
|
||||
element, current_category, all_categories
|
||||
MD_BLOCK_RE = re.compile(r'<div\s+class="md">(.*?)</div>', re.DOTALL | re.IGNORECASE)
|
||||
EXAMPLE_SECTION_RE = re.compile(
|
||||
r"<h[1-6][^>]*>\s*example[s]?:?\s*</h[1-6]>\s*(?P<section>.*?)(?=<h[1-6][^>]*>|$)",
|
||||
re.DOTALL | re.IGNORECASE,
|
||||
)
|
||||
LABELED_IO_RE = re.compile(
|
||||
r"input\s*:\s*</p>\s*<pre>(?P<input>.*?)</pre>.*?output\s*:\s*</p>\s*<pre>(?P<output>.*?)</pre>",
|
||||
re.DOTALL | re.IGNORECASE,
|
||||
)
|
||||
PRE_RE = re.compile(r"<pre>(.*?)</pre>", re.DOTALL | re.IGNORECASE)
|
||||
|
||||
|
||||
def parse_categories(html: str) -> list[ContestSummary]:
|
||||
out: list[ContestSummary] = []
|
||||
for m in CATEGORY_BLOCK_RE.finditer(html):
|
||||
cat = m.group("cat").strip()
|
||||
if cat == "General":
|
||||
continue
|
||||
out.append(
|
||||
ContestSummary(
|
||||
id=normalize_category_name(cat),
|
||||
name=cat,
|
||||
display_name=cat,
|
||||
)
|
||||
|
||||
for category in all_categories:
|
||||
all_categories[category].sort(key=lambda x: int(x["id"]))
|
||||
|
||||
print(f"Found {len(all_categories)} categories", file=sys.stderr)
|
||||
return all_categories
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to scrape CSES problems: {e}", file=sys.stderr)
|
||||
return {}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def extract_example_test_case(soup) -> tuple[str, str] | None:
|
||||
example_header = soup.find("h1", string="Example")
|
||||
if not example_header:
|
||||
return None
|
||||
|
||||
current = example_header.find_next_sibling()
|
||||
input_text = None
|
||||
output_text = None
|
||||
|
||||
while current:
|
||||
if current.name == "p" and "Input:" in current.get_text():
|
||||
input_pre = current.find_next_sibling("pre")
|
||||
if input_pre:
|
||||
input_text = input_pre.get_text().strip()
|
||||
elif current.name == "p" and "Output:" in current.get_text():
|
||||
output_pre = current.find_next_sibling("pre")
|
||||
if output_pre:
|
||||
output_text = output_pre.get_text().strip()
|
||||
break
|
||||
current = current.find_next_sibling()
|
||||
|
||||
if not input_text or not output_text:
|
||||
return None
|
||||
|
||||
return (input_text, output_text)
|
||||
def parse_category_problems(category_id: str, html: str) -> list[ProblemSummary]:
|
||||
want = snake_to_title(category_id)
|
||||
for m in CATEGORY_BLOCK_RE.finditer(html):
|
||||
cat = m.group("cat").strip()
|
||||
if cat != want:
|
||||
continue
|
||||
body = m.group("body")
|
||||
return [
|
||||
ProblemSummary(id=mm.group("id"), name=mm.group("title"))
|
||||
for mm in TASK_LINK_RE.finditer(body)
|
||||
]
|
||||
return []
|
||||
|
||||
|
||||
def scrape(url: str) -> list[tuple[str, str]]:
|
||||
try:
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
}
|
||||
def _extract_problem_info(html: str) -> tuple[int, int, bool]:
|
||||
tm = TIME_RE.search(html)
|
||||
mm = MEM_RE.search(html)
|
||||
t = int(round(float(tm.group(1)) * 1000)) if tm else 0
|
||||
m = int(mm.group(1)) if mm else 0
|
||||
md = MD_BLOCK_RE.search(html)
|
||||
interactive = False
|
||||
if md:
|
||||
body = md.group(1)
|
||||
interactive = "This is an interactive problem." in body
|
||||
return t, m, interactive
|
||||
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
soup = BeautifulSoup(response.text, "html.parser")
|
||||
def parse_title(html: str) -> str:
|
||||
mt = TITLE_RE.search(html)
|
||||
return mt.group("title").strip() if mt else ""
|
||||
|
||||
test_case = extract_example_test_case(soup)
|
||||
if not test_case:
|
||||
return []
|
||||
|
||||
return [test_case]
|
||||
def parse_category_from_sidebar(html: str) -> str | None:
|
||||
m = SIDEBAR_CAT_RE.search(html)
|
||||
return m.group("cat").strip() if m else None
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error scraping CSES: {e}", file=sys.stderr)
|
||||
|
||||
def parse_tests(html: str) -> list[TestCase]:
|
||||
md = MD_BLOCK_RE.search(html)
|
||||
if not md:
|
||||
return []
|
||||
block = md.group(1)
|
||||
|
||||
msec = EXAMPLE_SECTION_RE.search(block)
|
||||
section = msec.group("section") if msec else block
|
||||
|
||||
mlabel = LABELED_IO_RE.search(section)
|
||||
if mlabel:
|
||||
a = mlabel.group("input").strip()
|
||||
b = mlabel.group("output").strip()
|
||||
return [TestCase(input=a, expected=b)]
|
||||
|
||||
pres = PRE_RE.findall(section)
|
||||
if len(pres) >= 2:
|
||||
return [TestCase(input=pres[0].strip(), expected=pres[1].strip())]
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) < 2:
|
||||
result: dict[str, str | bool] = {
|
||||
"success": False,
|
||||
"error": "Usage: cses.py metadata OR cses.py tests <problem_id_or_url>",
|
||||
}
|
||||
print(json.dumps(result))
|
||||
sys.exit(1)
|
||||
def task_path(problem_id: str | int) -> str:
|
||||
return TASK_PATH.format(id=str(problem_id))
|
||||
|
||||
mode: str = sys.argv[1]
|
||||
|
||||
if mode == "metadata":
|
||||
if len(sys.argv) != 2:
|
||||
result = {
|
||||
"success": False,
|
||||
"error": "Usage: cses.py metadata",
|
||||
}
|
||||
print(json.dumps(result))
|
||||
sys.exit(1)
|
||||
class CSESScraper(BaseScraper):
|
||||
@property
|
||||
def platform_name(self) -> str:
|
||||
return "cses"
|
||||
|
||||
all_categories: dict[str, list[dict[str, str]]] = scrape_all_problems()
|
||||
|
||||
if not all_categories:
|
||||
result = {
|
||||
"success": False,
|
||||
"error": "Failed to scrape CSES problem categories",
|
||||
}
|
||||
print(json.dumps(result))
|
||||
sys.exit(1)
|
||||
|
||||
result = {
|
||||
"success": True,
|
||||
"categories": all_categories,
|
||||
}
|
||||
print(json.dumps(result))
|
||||
|
||||
elif mode == "tests":
|
||||
if len(sys.argv) != 3:
|
||||
result = {
|
||||
"success": False,
|
||||
"error": "Usage: cses.py tests <problem_id_or_url>",
|
||||
}
|
||||
print(json.dumps(result))
|
||||
sys.exit(1)
|
||||
|
||||
problem_input: str = sys.argv[2]
|
||||
url: str | None = parse_problem_url(problem_input)
|
||||
|
||||
if not url:
|
||||
result = {
|
||||
"success": False,
|
||||
"error": f"Invalid problem input: {problem_input}. Use either problem ID (e.g., 1068) or full URL",
|
||||
"problem_id": problem_input if problem_input.isdigit() else None,
|
||||
}
|
||||
print(json.dumps(result))
|
||||
sys.exit(1)
|
||||
|
||||
tests: list[tuple[str, str]] = scrape(url)
|
||||
|
||||
problem_id: str = (
|
||||
problem_input if problem_input.isdigit() else problem_input.split("/")[-1]
|
||||
async def scrape_contest_metadata(self, contest_id: str) -> MetadataResult:
|
||||
async with httpx.AsyncClient() as client:
|
||||
html = await fetch_text(client, INDEX_PATH)
|
||||
problems = parse_category_problems(contest_id, html)
|
||||
if not problems:
|
||||
return MetadataResult(
|
||||
success=False,
|
||||
error=f"{self.platform_name}: No problems found for category: {contest_id}",
|
||||
url="",
|
||||
)
|
||||
return MetadataResult(
|
||||
success=True,
|
||||
error="",
|
||||
contest_id=contest_id,
|
||||
problems=problems,
|
||||
url="https://cses.fi/problemset/task/%s",
|
||||
)
|
||||
|
||||
if not tests:
|
||||
result = {
|
||||
"success": False,
|
||||
"error": f"No tests found for {problem_input}",
|
||||
"problem_id": problem_id,
|
||||
"url": url,
|
||||
}
|
||||
print(json.dumps(result))
|
||||
sys.exit(1)
|
||||
async def scrape_contest_list(self) -> ContestListResult:
|
||||
async with httpx.AsyncClient() as client:
|
||||
html = await fetch_text(client, INDEX_PATH)
|
||||
cats = parse_categories(html)
|
||||
if not cats:
|
||||
return ContestListResult(
|
||||
success=False, error=f"{self.platform_name}: No contests found"
|
||||
)
|
||||
return ContestListResult(success=True, error="", contests=cats)
|
||||
|
||||
test_list: list[dict[str, str]] = []
|
||||
for input_data, output_data in tests:
|
||||
test_list.append({"input": input_data, "expected": output_data})
|
||||
async def stream_tests_for_category_async(self, category_id: str) -> None:
|
||||
async with httpx.AsyncClient(
|
||||
limits=httpx.Limits(max_connections=CONNECTIONS)
|
||||
) as client:
|
||||
index_html = await fetch_text(client, INDEX_PATH)
|
||||
problems = parse_category_problems(category_id, index_html)
|
||||
if not problems:
|
||||
return
|
||||
|
||||
result = {
|
||||
"success": True,
|
||||
"problem_id": problem_id,
|
||||
"url": url,
|
||||
"tests": test_list,
|
||||
}
|
||||
print(json.dumps(result))
|
||||
sem = asyncio.Semaphore(CONNECTIONS)
|
||||
|
||||
else:
|
||||
result = {
|
||||
"success": False,
|
||||
"error": f"Unknown mode: {mode}. Use 'metadata' or 'tests'",
|
||||
}
|
||||
print(json.dumps(result))
|
||||
sys.exit(1)
|
||||
async def run_one(pid: str) -> dict[str, Any]:
|
||||
async with sem:
|
||||
try:
|
||||
html = await fetch_text(client, task_path(pid))
|
||||
tests = parse_tests(html)
|
||||
timeout_ms, memory_mb, interactive = _extract_problem_info(html)
|
||||
except Exception:
|
||||
tests = []
|
||||
timeout_ms, memory_mb, interactive = 0, 0, False
|
||||
|
||||
combined_input = "\n".join(t.input for t in tests) if tests else ""
|
||||
combined_expected = (
|
||||
"\n".join(t.expected for t in tests) if tests else ""
|
||||
)
|
||||
|
||||
return {
|
||||
"problem_id": pid,
|
||||
"combined": {
|
||||
"input": combined_input,
|
||||
"expected": combined_expected,
|
||||
},
|
||||
"tests": [
|
||||
{"input": t.input, "expected": t.expected} for t in tests
|
||||
],
|
||||
"timeout_ms": timeout_ms,
|
||||
"memory_mb": memory_mb,
|
||||
"interactive": interactive,
|
||||
"multi_test": False,
|
||||
}
|
||||
|
||||
tasks = [run_one(p.id) for p in problems]
|
||||
for coro in asyncio.as_completed(tasks):
|
||||
payload = await coro
|
||||
print(json.dumps(payload), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
CSESScraper().run_cli()
|
||||
|
|
|
|||
72
scrapers/models.py
Normal file
72
scrapers/models.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class TestCase(BaseModel):
|
||||
input: str
|
||||
expected: str
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class CombinedTest(BaseModel):
|
||||
input: str
|
||||
expected: str
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class ProblemSummary(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class ContestSummary(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
display_name: str | None = None
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class ScrapingResult(BaseModel):
|
||||
success: bool
|
||||
error: str
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class MetadataResult(ScrapingResult):
|
||||
contest_id: str = ""
|
||||
problems: list[ProblemSummary] = Field(default_factory=list)
|
||||
url: str
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class ContestListResult(ScrapingResult):
|
||||
contests: list[ContestSummary] = Field(default_factory=list)
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class TestsResult(ScrapingResult):
|
||||
problem_id: str
|
||||
combined: CombinedTest
|
||||
tests: list[TestCase] = Field(default_factory=list)
|
||||
timeout_ms: int
|
||||
memory_mb: float
|
||||
interactive: bool = False
|
||||
multi_test: bool = False
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class ScraperConfig(BaseModel):
|
||||
timeout_seconds: int = 30
|
||||
max_retries: int = 3
|
||||
backoff_base: float = 2.0
|
||||
rate_limit_delay: float = 1.0
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
58
scripts/interact.py
Normal file
58
scripts/interact.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
#!/usr/bin/env python3
|
||||
import asyncio
|
||||
import shlex
|
||||
import sys
|
||||
from collections.abc import Sequence
|
||||
|
||||
|
||||
async def pump(
|
||||
reader: asyncio.StreamReader, writer: asyncio.StreamWriter | None
|
||||
) -> None:
|
||||
while True:
|
||||
data = await reader.readline()
|
||||
if not data:
|
||||
break
|
||||
_ = sys.stdout.buffer.write(data)
|
||||
_ = sys.stdout.flush()
|
||||
if writer:
|
||||
writer.write(data)
|
||||
await writer.drain()
|
||||
|
||||
|
||||
async def main(interactor_cmd: Sequence[str], interactee_cmd: Sequence[str]) -> None:
|
||||
interactor = await asyncio.create_subprocess_exec(
|
||||
*interactor_cmd,
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
)
|
||||
interactee = await asyncio.create_subprocess_exec(
|
||||
*interactee_cmd,
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
)
|
||||
|
||||
assert (
|
||||
interactor.stdout
|
||||
and interactor.stdin
|
||||
and interactee.stdout
|
||||
and interactee.stdin
|
||||
)
|
||||
|
||||
tasks = [
|
||||
asyncio.create_task(pump(interactor.stdout, interactee.stdin)),
|
||||
asyncio.create_task(pump(interactee.stdout, interactor.stdin)),
|
||||
]
|
||||
_ = await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED)
|
||||
_ = await interactor.wait()
|
||||
_ = await interactee.wait()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print("Usage: interact.py <interactor> <interactee>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
interactor_cmd = shlex.split(sys.argv[1])
|
||||
interactee_cmd = shlex.split(sys.argv[2])
|
||||
|
||||
_ = asyncio.run(main(interactor_cmd, interactee_cmd))
|
||||
|
|
@ -1 +1 @@
|
|||
std = "vim"
|
||||
std = 'vim'
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
local cp = require('cp')
|
||||
|
||||
describe('neovim plugin', function()
|
||||
it('work as expect', function()
|
||||
cp.setup()
|
||||
end)
|
||||
end)
|
||||
275
tests/conftest.py
Normal file
275
tests/conftest.py
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
import asyncio
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import requests
|
||||
from curl_cffi import requests as curl_requests
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
FIX = Path(__file__).resolve().parent / "fixtures"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fixture_text():
|
||||
def _load(name: str) -> str:
|
||||
p = FIX / name
|
||||
return p.read_text(encoding="utf-8")
|
||||
|
||||
return _load
|
||||
|
||||
|
||||
def _load_scraper_module(module_path: Path, module_name: str):
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
f"scrapers.{module_name}", module_path
|
||||
)
|
||||
if spec is None or spec.loader is None:
|
||||
raise ImportError(f"Cannot load module {module_name}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[f"scrapers.{module_name}"] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _capture_stdout(coro):
|
||||
buf = io.StringIO()
|
||||
old = sys.stdout
|
||||
sys.stdout = buf
|
||||
try:
|
||||
rc = asyncio.run(coro)
|
||||
out = buf.getvalue()
|
||||
finally:
|
||||
sys.stdout = old
|
||||
return rc, out
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def run_scraper_offline(fixture_text):
|
||||
def _router_cses(*, path: str | None = None, url: str | None = None) -> str:
|
||||
if not path and not url:
|
||||
raise AssertionError("CSES expects path or url")
|
||||
|
||||
target = path or url
|
||||
if target is None:
|
||||
raise AssertionError(f"No target for CSES (path={path!r}, url={url!r})")
|
||||
|
||||
if target.startswith("https://cses.fi"):
|
||||
target = target.removeprefix("https://cses.fi")
|
||||
|
||||
if target.strip("/") == "problemset":
|
||||
return fixture_text("cses/contests.html")
|
||||
|
||||
if target.startswith("/problemset/task/") or target.startswith(
|
||||
"problemset/task/"
|
||||
):
|
||||
pid = target.rstrip("/").split("/")[-1]
|
||||
return fixture_text(f"cses/task_{pid}.html")
|
||||
|
||||
raise AssertionError(f"No fixture for CSES path={path!r} url={url!r}")
|
||||
|
||||
def _router_atcoder(*, path: str | None = None, url: str | None = None) -> str:
|
||||
if not url:
|
||||
raise AssertionError("AtCoder expects url routing")
|
||||
if "/contests/archive" in url:
|
||||
return fixture_text("atcoder/contests.html")
|
||||
if url.endswith("/tasks"):
|
||||
return fixture_text("atcoder/abc100_tasks.html")
|
||||
if "/tasks/" in url:
|
||||
slug = url.rsplit("/", 1)[-1]
|
||||
return fixture_text(f"atcoder/task_{slug}.html")
|
||||
raise AssertionError(f"No fixture for AtCoder url={url!r}")
|
||||
|
||||
def _router_codeforces(*, path: str | None = None, url: str | None = None) -> str:
|
||||
if not url:
|
||||
raise AssertionError("Codeforces expects url routing")
|
||||
if "/contest/" in url and url.endswith("/problems"):
|
||||
contest_id = url.rstrip("/").split("/")[-2]
|
||||
return fixture_text(f"codeforces/{contest_id}_problems.html")
|
||||
if "/contests" in url and "/problem/" not in url:
|
||||
return fixture_text("codeforces/contests.html")
|
||||
if "/problem/" in url:
|
||||
parts = url.rstrip("/").split("/")
|
||||
contest_id, index = parts[-3], parts[-1]
|
||||
return fixture_text(f"codeforces/{contest_id}_{index}.html")
|
||||
if "/problemset/problem/" in url:
|
||||
parts = url.rstrip("/").split("/")
|
||||
contest_id, index = parts[-2], parts[-1]
|
||||
return fixture_text(f"codeforces/{contest_id}_{index}.html")
|
||||
|
||||
raise AssertionError(f"No fixture for Codeforces url={url!r}")
|
||||
|
||||
def _make_offline_fetches(scraper_name: str):
|
||||
match scraper_name:
|
||||
case "cses":
|
||||
|
||||
async def __offline_fetch_text(client, path: str, **kwargs):
|
||||
html = _router_cses(path=path)
|
||||
return SimpleNamespace(
|
||||
text=html,
|
||||
status_code=200,
|
||||
raise_for_status=lambda: None,
|
||||
)
|
||||
|
||||
return {
|
||||
"__offline_fetch_text": __offline_fetch_text,
|
||||
}
|
||||
|
||||
case "atcoder":
|
||||
|
||||
def __offline_fetch(url: str, *args, **kwargs):
|
||||
html = _router_atcoder(url=url)
|
||||
return html
|
||||
|
||||
async def __offline_get_async(client, url: str, **kwargs):
|
||||
return _router_atcoder(url=url)
|
||||
|
||||
return {
|
||||
"_fetch": __offline_fetch,
|
||||
"_get_async": __offline_get_async,
|
||||
}
|
||||
|
||||
case "codeforces":
|
||||
|
||||
class MockCurlResponse:
|
||||
def __init__(self, html: str):
|
||||
self.text = html
|
||||
|
||||
def raise_for_status(self):
|
||||
pass
|
||||
|
||||
def _mock_curl_get(url: str, **kwargs):
|
||||
return MockCurlResponse(_router_codeforces(url=url))
|
||||
|
||||
def _mock_requests_get(url: str, **kwargs):
|
||||
if "api/contest.list" in url:
|
||||
data = {
|
||||
"status": "OK",
|
||||
"result": [
|
||||
{
|
||||
"id": 1550,
|
||||
"name": "Educational Codeforces Round 155 (Rated for Div. 2)",
|
||||
"phase": "FINISHED",
|
||||
},
|
||||
{
|
||||
"id": 1000,
|
||||
"name": "Codeforces Round #1000",
|
||||
"phase": "FINISHED",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
class R:
|
||||
def json(self_inner):
|
||||
return data
|
||||
|
||||
def raise_for_status(self_inner):
|
||||
return None
|
||||
|
||||
return R()
|
||||
raise AssertionError(f"Unexpected requests.get call: {url}")
|
||||
|
||||
return {
|
||||
"curl_requests.get": _mock_curl_get,
|
||||
"requests.get": _mock_requests_get,
|
||||
}
|
||||
|
||||
case "codechef":
|
||||
|
||||
class MockResponse:
|
||||
def __init__(self, json_data):
|
||||
self._json_data = json_data
|
||||
self.status_code = 200
|
||||
|
||||
def json(self):
|
||||
return self._json_data
|
||||
|
||||
def raise_for_status(self):
|
||||
pass
|
||||
|
||||
async def __offline_get_async(client, url: str, **kwargs):
|
||||
if "/api/list/contests/all" in url:
|
||||
data = json.loads(fixture_text("codechef/contests.json"))
|
||||
return MockResponse(data)
|
||||
if "/api/contests/START" in url and "/problems/" not in url:
|
||||
contest_id = url.rstrip("/").split("/")[-1]
|
||||
try:
|
||||
data = json.loads(
|
||||
fixture_text(f"codechef/{contest_id}.json")
|
||||
)
|
||||
return MockResponse(data)
|
||||
except FileNotFoundError:
|
||||
raise AssertionError(f"No fixture for CodeChef url={url!r}")
|
||||
if "/api/contests/START" in url and "/problems/" in url:
|
||||
parts = url.rstrip("/").split("/")
|
||||
contest_id = parts[-3]
|
||||
problem_id = parts[-1]
|
||||
data = json.loads(
|
||||
fixture_text(f"codechef/{contest_id}_{problem_id}.json")
|
||||
)
|
||||
return MockResponse(data)
|
||||
raise AssertionError(f"No fixture for CodeChef url={url!r}")
|
||||
|
||||
class MockCodeChefCurlResponse:
|
||||
def __init__(self, html: str):
|
||||
self.text = html
|
||||
|
||||
def raise_for_status(self):
|
||||
pass
|
||||
|
||||
def _mock_curl_get(url: str, **kwargs):
|
||||
if "/problems/" in url:
|
||||
problem_id = url.rstrip("/").split("/")[-1]
|
||||
html = fixture_text(f"codechef/{problem_id}.html")
|
||||
return MockCodeChefCurlResponse(html)
|
||||
raise AssertionError(f"No fixture for CodeChef url={url!r}")
|
||||
|
||||
return {
|
||||
"__offline_get_async": __offline_get_async,
|
||||
"curl_requests.get": _mock_curl_get,
|
||||
}
|
||||
|
||||
case _:
|
||||
raise AssertionError(f"Unknown scraper: {scraper_name}")
|
||||
|
||||
scraper_classes = {
|
||||
"cses": "CSESScraper",
|
||||
"atcoder": "AtcoderScraper",
|
||||
"codeforces": "CodeforcesScraper",
|
||||
"codechef": "CodeChefScraper",
|
||||
}
|
||||
|
||||
def _run(scraper_name: str, mode: str, *args: str):
|
||||
mod_path = ROOT / "scrapers" / f"{scraper_name}.py"
|
||||
ns = _load_scraper_module(mod_path, scraper_name)
|
||||
offline_fetches = _make_offline_fetches(scraper_name)
|
||||
|
||||
if scraper_name == "codeforces":
|
||||
curl_requests.get = offline_fetches["curl_requests.get"]
|
||||
requests.get = offline_fetches["requests.get"]
|
||||
elif scraper_name == "atcoder":
|
||||
ns._fetch = offline_fetches["_fetch"]
|
||||
ns._get_async = offline_fetches["_get_async"]
|
||||
elif scraper_name == "cses":
|
||||
httpx.AsyncClient.get = offline_fetches["__offline_fetch_text"]
|
||||
elif scraper_name == "codechef":
|
||||
httpx.AsyncClient.get = offline_fetches["__offline_get_async"]
|
||||
curl_requests.get = offline_fetches["curl_requests.get"]
|
||||
|
||||
scraper_class = getattr(ns, scraper_classes[scraper_name])
|
||||
scraper = scraper_class()
|
||||
|
||||
argv = [str(mod_path), mode, *args]
|
||||
rc, out = _capture_stdout(scraper._run_cli_async(argv))
|
||||
|
||||
json_lines: list[Any] = []
|
||||
for line in (_line for _line in out.splitlines() if _line.strip()):
|
||||
json_lines.append(json.loads(line))
|
||||
return rc, json_lines
|
||||
|
||||
return _run
|
||||
519
tests/fixtures/atcoder/abc100_tasks.html
vendored
Normal file
519
tests/fixtures/atcoder/abc100_tasks.html
vendored
Normal file
|
|
@ -0,0 +1,519 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Tasks - AtCoder Beginner Contest 100</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="Content-Language" content="en" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta
|
||||
name="google-site-verification"
|
||||
content="nXGC_JxO0yoP1qBzMnYD_xgufO6leSLw1kyNo2HZltM"
|
||||
/>
|
||||
|
||||
<script
|
||||
async
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-RC512FD18N"
|
||||
></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || []
|
||||
function gtag() {
|
||||
dataLayer.push(arguments)
|
||||
}
|
||||
gtag('js', new Date())
|
||||
gtag('set', 'user_properties', {
|
||||
login_status: 'logged_out'
|
||||
})
|
||||
gtag('config', 'G-RC512FD18N')
|
||||
</script>
|
||||
|
||||
<meta
|
||||
name="description"
|
||||
content="AtCoder is a programming contest site for anyone from beginners to experts. We hold weekly programming contests online."
|
||||
/>
|
||||
<meta name="author" content="AtCoder Inc." />
|
||||
|
||||
<meta property="og:site_name" content="AtCoder" />
|
||||
|
||||
<meta property="og:title" content="Tasks - AtCoder Beginner Contest 100" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="AtCoder is a programming contest site for anyone from beginners to experts. We hold weekly programming contests online."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:url"
|
||||
content="https://atcoder.jp/contests/abc100/tasks"
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://img.atcoder.jp/assets/atcoder.png"
|
||||
/>
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="@atcoder" />
|
||||
|
||||
<meta
|
||||
property="twitter:title"
|
||||
content="Tasks - AtCoder Beginner Contest 100"
|
||||
/>
|
||||
|
||||
<link
|
||||
href="//fonts.googleapis.com/css?family=Lato:400,700"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="//img.atcoder.jp/public/6372bb3/css/bootstrap.min.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="//img.atcoder.jp/public/6372bb3/css/base.css"
|
||||
/>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/png"
|
||||
href="//img.atcoder.jp/assets/favicon.png"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="//img.atcoder.jp/assets/atcoder.png" />
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/lib/jquery-1.9.1.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/lib/bootstrap.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/js.cookie.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/moment.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/moment_js-ja.js"></script>
|
||||
<script>
|
||||
var LANG = 'en'
|
||||
var userScreenName = ''
|
||||
var csrfToken = 'q+4tZ4tLQh/4nobcpVAuiml6OVEZOdZDZURhPenxPbc='
|
||||
</script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/utils.js"></script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/contest.js"></script>
|
||||
<link
|
||||
href="//img.atcoder.jp/public/6372bb3/css/contest.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script>
|
||||
var contestScreenName = 'abc100'
|
||||
var remainingText = 'Remaining Time'
|
||||
var countDownText = 'Contest begins in'
|
||||
var startTime = moment('2018-06-16T21:00:00+09:00')
|
||||
var endTime = moment('2018-06-16T22:40:00+09:00')
|
||||
</script>
|
||||
<style></style>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/base.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
var __pParams = __pParams || []
|
||||
__pParams.push({
|
||||
client_id: '468',
|
||||
c_1: 'atcodercontest',
|
||||
c_2: 'ClientSite'
|
||||
})
|
||||
</script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://cdn.d2-apps.net/js/tr.js"
|
||||
async
|
||||
></script>
|
||||
|
||||
<div
|
||||
id="modal-contest-start"
|
||||
class="modal fade"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
>
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Contest started</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>AtCoder Beginner Contest 100 has begun.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-contest-end" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Contest is over</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>AtCoder Beginner Contest 100 has ended.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="main-div" class="float-container">
|
||||
<nav class="navbar navbar-inverse navbar-fixed-top">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button
|
||||
type="button"
|
||||
class="navbar-toggle collapsed"
|
||||
data-toggle="collapse"
|
||||
data-target="#navbar-collapse"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="icon-bar"></span><span class="icon-bar"></span
|
||||
><span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="/home"></a>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse" id="navbar-collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li>
|
||||
<a class="contest-title" href="/contests/abc100"
|
||||
>AtCoder Beginner Contest 100</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<a
|
||||
class="dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
href="#"
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<img src="//img.atcoder.jp/assets/top/img/flag-lang/en.png" />
|
||||
English <span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="/contests/abc100/tasks?lang=ja"
|
||||
><img
|
||||
src="//img.atcoder.jp/assets/top/img/flag-lang/ja.png"
|
||||
/>
|
||||
日本語</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contests/abc100/tasks?lang=en"
|
||||
><img
|
||||
src="//img.atcoder.jp/assets/top/img/flag-lang/en.png"
|
||||
/>
|
||||
English</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
href="/register?continue=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc100%2Ftasks"
|
||||
>Sign Up</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/login?continue=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc100%2Ftasks"
|
||||
>Sign In</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
name="form_logout"
|
||||
action="/logout?continue=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc100%2Ftasks"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="csrf_token"
|
||||
value="q+4tZ4tLQh/4nobcpVAuiml6OVEZOdZDZURhPenxPbc="
|
||||
/>
|
||||
</form>
|
||||
<div id="main-container" class="container" style="padding-top: 50px">
|
||||
<div class="row">
|
||||
<div id="contest-nav-tabs" class="col-sm-12 mb-2 cnvtb-fixed">
|
||||
<div>
|
||||
<small class="contest-duration">
|
||||
Contest Duration:
|
||||
<a
|
||||
href="http://www.timeanddate.com/worldclock/fixedtime.html?iso=20180616T2100&p1=248"
|
||||
target="blank"
|
||||
><time class="fixtime fixtime-full"
|
||||
>2018-06-16 21:00:00+0900</time
|
||||
></a
|
||||
>
|
||||
-
|
||||
<a
|
||||
href="http://www.timeanddate.com/worldclock/fixedtime.html?iso=20180616T2240&p1=248"
|
||||
target="blank"
|
||||
><time class="fixtime fixtime-full"
|
||||
>2018-06-16 22:40:00+0900</time
|
||||
></a
|
||||
>
|
||||
(local time) (100 minutes)
|
||||
</small>
|
||||
<small class="back-to-home pull-right"
|
||||
><a href="/home">Back to Home</a></small
|
||||
>
|
||||
</div>
|
||||
<ul class="nav nav-tabs">
|
||||
<li>
|
||||
<a href="/contests/abc100"
|
||||
><span
|
||||
class="glyphicon glyphicon-home"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Top</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li class="active">
|
||||
<a href="/contests/abc100/tasks"
|
||||
><span
|
||||
class="glyphicon glyphicon-tasks"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Tasks</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/clarifications"
|
||||
><span
|
||||
class="glyphicon glyphicon-question-sign"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Clarifications <span id="clar-badge" class="badge"></span
|
||||
></a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
href="#"
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
><span
|
||||
class="glyphicon glyphicon-list"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Results<span class="caret"></span
|
||||
></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="/contests/abc100/submissions"
|
||||
><span
|
||||
class="glyphicon glyphicon-globe"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
All Submissions</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/standings"
|
||||
><span
|
||||
class="glyphicon glyphicon-sort-by-attributes-alt"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Standings</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/standings/virtual"
|
||||
><span
|
||||
class="glyphicon glyphicon-sort-by-attributes-alt"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Virtual Standings</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/editorial"
|
||||
><span
|
||||
class="glyphicon glyphicon-book"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Editorial</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li class="pull-right">
|
||||
<a id="fix-cnvtb" href="javascript:void(0)"
|
||||
><span
|
||||
class="glyphicon glyphicon-pushpin"
|
||||
aria-hidden="true"
|
||||
></span
|
||||
></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<h2>Tasks</h2>
|
||||
<hr />
|
||||
|
||||
<div class="panel panel-default table-responsive">
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="3%" class="text-center"></th>
|
||||
<th>Task Name</th>
|
||||
<th width="10%" class="text-right no-break">Time Limit</th>
|
||||
<th width="10%" class="text-right no-break">
|
||||
Memory Limit
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center no-break">
|
||||
<a href="/contests/abc100/tasks/abc100_a">A</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/contests/abc100/tasks/abc100_a"
|
||||
>Happy Birthday!</a
|
||||
>
|
||||
</td>
|
||||
<td class="text-right">2 sec</td>
|
||||
<td class="text-right">976 MiB</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="text-center no-break">
|
||||
<a href="/contests/abc100/tasks/abc100_b">B</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/contests/abc100/tasks/abc100_b"
|
||||
>Ringo's Favorite Numbers</a
|
||||
>
|
||||
</td>
|
||||
<td class="text-right">2 sec</td>
|
||||
<td class="text-right">976 MiB</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="text-center no-break">
|
||||
<a href="/contests/abc100/tasks/abc100_c">C</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/contests/abc100/tasks/abc100_c">*3 or /2</a>
|
||||
</td>
|
||||
<td class="text-right">2 sec</td>
|
||||
<td class="text-right">976 MiB</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="text-center no-break">
|
||||
<a href="/contests/abc100/tasks/abc100_d">D</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/contests/abc100/tasks/abc100_d"
|
||||
>Patisserie ABC</a
|
||||
>
|
||||
</td>
|
||||
<td class="text-right">2 sec</td>
|
||||
<td class="text-right">976 MiB</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p class="btn-text-group">
|
||||
<a class="btn-text" href="/contests/abc100/tasks_print"
|
||||
>Tasks for printing</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div
|
||||
class="a2a_kit a2a_kit_size_20 a2a_default_style pull-right"
|
||||
data-a2a-url="https://atcoder.jp/contests/abc100/tasks?lang=en"
|
||||
data-a2a-title="Tasks - AtCoder Beginner Contest 100"
|
||||
>
|
||||
<a class="a2a_button_facebook"></a>
|
||||
<a class="a2a_button_twitter"></a>
|
||||
|
||||
<a class="a2a_button_telegram"></a>
|
||||
|
||||
<a class="a2a_dd" href="https://www.addtoany.com/share"></a>
|
||||
</div>
|
||||
|
||||
<script async src="//static.addtoany.com/menu/page.js"></script>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<div class="container" style="margin-bottom: 80px">
|
||||
<footer class="footer">
|
||||
<ul>
|
||||
<li><a href="/contests/abc100/rules">Rule</a></li>
|
||||
<li><a href="/contests/abc100/glossary">Glossary</a></li>
|
||||
</ul>
|
||||
|
||||
<ul>
|
||||
<li><a href="/tos">Terms of service</a></li>
|
||||
<li><a href="/privacy">Privacy Policy</a></li>
|
||||
<li><a href="/personal">Information Protection Policy</a></li>
|
||||
<li><a href="/company">Company</a></li>
|
||||
<li><a href="/faq">FAQ</a></li>
|
||||
<li><a href="/contact">Contact</a></li>
|
||||
</ul>
|
||||
<div class="text-center">
|
||||
<small id="copyright"
|
||||
>Copyright Since 2012 ©<a href="http://atcoder.co.jp"
|
||||
>AtCoder Inc.</a
|
||||
>
|
||||
All rights reserved.</small
|
||||
>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<p id="fixed-server-timer" class="contest-timer"></p>
|
||||
<div id="scroll-page-top" style="display: none">
|
||||
<span class="glyphicon glyphicon-arrow-up" aria-hidden="true"></span> Page
|
||||
Top
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
1902
tests/fixtures/atcoder/contests.html
vendored
Normal file
1902
tests/fixtures/atcoder/contests.html
vendored
Normal file
File diff suppressed because it is too large
Load diff
885
tests/fixtures/atcoder/task_abc100_a.html
vendored
Normal file
885
tests/fixtures/atcoder/task_abc100_a.html
vendored
Normal file
|
|
@ -0,0 +1,885 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>A - Happy Birthday!</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="Content-Language" content="en" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta
|
||||
name="google-site-verification"
|
||||
content="nXGC_JxO0yoP1qBzMnYD_xgufO6leSLw1kyNo2HZltM"
|
||||
/>
|
||||
|
||||
<script
|
||||
async
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-RC512FD18N"
|
||||
></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || []
|
||||
function gtag() {
|
||||
dataLayer.push(arguments)
|
||||
}
|
||||
gtag('js', new Date())
|
||||
gtag('set', 'user_properties', {
|
||||
login_status: 'logged_out'
|
||||
})
|
||||
gtag('config', 'G-RC512FD18N')
|
||||
</script>
|
||||
|
||||
<meta
|
||||
name="description"
|
||||
content="AtCoder is a programming contest site for anyone from beginners to experts. We hold weekly programming contests online."
|
||||
/>
|
||||
<meta name="author" content="AtCoder Inc." />
|
||||
|
||||
<meta property="og:site_name" content="AtCoder" />
|
||||
|
||||
<meta property="og:title" content="A - Happy Birthday!" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="AtCoder is a programming contest site for anyone from beginners to experts. We hold weekly programming contests online."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:url"
|
||||
content="https://atcoder.jp/contests/abc100/tasks/abc100_a"
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://img.atcoder.jp/assets/atcoder.png"
|
||||
/>
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="@atcoder" />
|
||||
|
||||
<meta property="twitter:title" content="A - Happy Birthday!" />
|
||||
|
||||
<link
|
||||
href="//fonts.googleapis.com/css?family=Lato:400,700"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="//img.atcoder.jp/public/6372bb3/css/bootstrap.min.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="//img.atcoder.jp/public/6372bb3/css/base.css"
|
||||
/>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/png"
|
||||
href="//img.atcoder.jp/assets/favicon.png"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="//img.atcoder.jp/assets/atcoder.png" />
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/lib/jquery-1.9.1.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/lib/bootstrap.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/js.cookie.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/moment.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/moment_js-ja.js"></script>
|
||||
<script>
|
||||
var LANG = 'en'
|
||||
var userScreenName = ''
|
||||
var csrfToken = 'RRHWPnDZqM+tdZpgbKRjH5FxiX5spw9S3/HKRbnyqok='
|
||||
</script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/utils.js"></script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/contest.js"></script>
|
||||
<link
|
||||
href="//img.atcoder.jp/public/6372bb3/css/contest.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script>
|
||||
var contestScreenName = 'abc100'
|
||||
var remainingText = 'Remaining Time'
|
||||
var countDownText = 'Contest begins in'
|
||||
var startTime = moment('2018-06-16T21:00:00+09:00')
|
||||
var endTime = moment('2018-06-16T22:40:00+09:00')
|
||||
</script>
|
||||
<style></style>
|
||||
|
||||
<link
|
||||
href="//img.atcoder.jp/public/6372bb3/css/cdn/select2.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="//img.atcoder.jp/public/6372bb3/css/cdn/select2-bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/lib/select2.min.js"></script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/ace/ace.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/ace/ext-language_tools.js"></script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/run_prettify.js"></script>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="//img.atcoder.jp/public/6372bb3/css/cdn/katex.min.css"
|
||||
/>
|
||||
<script
|
||||
defer
|
||||
src="//img.atcoder.jp/public/6372bb3/js/cdn/katex.min.js"
|
||||
></script>
|
||||
<script
|
||||
defer
|
||||
src="//img.atcoder.jp/public/6372bb3/js/cdn/auto-render.min.js"
|
||||
></script>
|
||||
<script>
|
||||
$(function () {
|
||||
$('var').each(function () {
|
||||
var html = $(this)
|
||||
.html()
|
||||
.replace(/<sub>/g, '_{')
|
||||
.replace(/<\/sub>/g, '}')
|
||||
$(this).html('\\(' + html + '\\)')
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<script>
|
||||
var katexOptions = {
|
||||
delimiters: [
|
||||
{ left: '$$', right: '$$', display: true },
|
||||
|
||||
{ left: '\\(', right: '\\)', display: false },
|
||||
{ left: '\\[', right: '\\]', display: true }
|
||||
],
|
||||
ignoredTags: [
|
||||
'script',
|
||||
'noscript',
|
||||
'style',
|
||||
'textarea',
|
||||
'code',
|
||||
'option'
|
||||
],
|
||||
ignoredClasses: ['prettyprint', 'source-code-for-copy'],
|
||||
throwOnError: false
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
renderMathInElement(document.body, katexOptions)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/base.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
var __pParams = __pParams || []
|
||||
__pParams.push({
|
||||
client_id: '468',
|
||||
c_1: 'atcodercontest',
|
||||
c_2: 'ClientSite'
|
||||
})
|
||||
</script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://cdn.d2-apps.net/js/tr.js"
|
||||
async
|
||||
></script>
|
||||
|
||||
<div
|
||||
id="modal-contest-start"
|
||||
class="modal fade"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
>
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Contest started</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>AtCoder Beginner Contest 100 has begun.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-contest-end" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Contest is over</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>AtCoder Beginner Contest 100 has ended.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="main-div" class="float-container">
|
||||
<nav class="navbar navbar-inverse navbar-fixed-top">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button
|
||||
type="button"
|
||||
class="navbar-toggle collapsed"
|
||||
data-toggle="collapse"
|
||||
data-target="#navbar-collapse"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="icon-bar"></span><span class="icon-bar"></span
|
||||
><span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="/home"></a>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse" id="navbar-collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li>
|
||||
<a class="contest-title" href="/contests/abc100"
|
||||
>AtCoder Beginner Contest 100</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<a
|
||||
class="dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
href="#"
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<img src="//img.atcoder.jp/assets/top/img/flag-lang/en.png" />
|
||||
English <span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="/contests/abc100/tasks/abc100_a?lang=ja"
|
||||
><img
|
||||
src="//img.atcoder.jp/assets/top/img/flag-lang/ja.png"
|
||||
/>
|
||||
日本語</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contests/abc100/tasks/abc100_a?lang=en"
|
||||
><img
|
||||
src="//img.atcoder.jp/assets/top/img/flag-lang/en.png"
|
||||
/>
|
||||
English</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
href="/register?continue=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc100%2Ftasks%2Fabc100_a"
|
||||
>Sign Up</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/login?continue=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc100%2Ftasks%2Fabc100_a"
|
||||
>Sign In</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
name="form_logout"
|
||||
action="/logout?continue=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc100%2Ftasks%2Fabc100_a"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="csrf_token"
|
||||
value="RRHWPnDZqM+tdZpgbKRjH5FxiX5spw9S3/HKRbnyqok="
|
||||
/>
|
||||
</form>
|
||||
<div id="main-container" class="container" style="padding-top: 50px">
|
||||
<div class="row">
|
||||
<div id="contest-nav-tabs" class="col-sm-12 mb-2 cnvtb-fixed">
|
||||
<div>
|
||||
<small class="contest-duration">
|
||||
Contest Duration:
|
||||
<a
|
||||
href="http://www.timeanddate.com/worldclock/fixedtime.html?iso=20180616T2100&p1=248"
|
||||
target="blank"
|
||||
><time class="fixtime fixtime-full"
|
||||
>2018-06-16 21:00:00+0900</time
|
||||
></a
|
||||
>
|
||||
-
|
||||
<a
|
||||
href="http://www.timeanddate.com/worldclock/fixedtime.html?iso=20180616T2240&p1=248"
|
||||
target="blank"
|
||||
><time class="fixtime fixtime-full"
|
||||
>2018-06-16 22:40:00+0900</time
|
||||
></a
|
||||
>
|
||||
(local time) (100 minutes)
|
||||
</small>
|
||||
<small class="back-to-home pull-right"
|
||||
><a href="/home">Back to Home</a></small
|
||||
>
|
||||
</div>
|
||||
<ul class="nav nav-tabs">
|
||||
<li>
|
||||
<a href="/contests/abc100"
|
||||
><span
|
||||
class="glyphicon glyphicon-home"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Top</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li class="active">
|
||||
<a href="/contests/abc100/tasks"
|
||||
><span
|
||||
class="glyphicon glyphicon-tasks"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Tasks</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/clarifications"
|
||||
><span
|
||||
class="glyphicon glyphicon-question-sign"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Clarifications <span id="clar-badge" class="badge"></span
|
||||
></a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
href="#"
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
><span
|
||||
class="glyphicon glyphicon-list"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Results<span class="caret"></span
|
||||
></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="/contests/abc100/submissions"
|
||||
><span
|
||||
class="glyphicon glyphicon-globe"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
All Submissions</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/standings"
|
||||
><span
|
||||
class="glyphicon glyphicon-sort-by-attributes-alt"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Standings</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/standings/virtual"
|
||||
><span
|
||||
class="glyphicon glyphicon-sort-by-attributes-alt"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Virtual Standings</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/editorial"
|
||||
><span
|
||||
class="glyphicon glyphicon-book"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Editorial</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li class="pull-right">
|
||||
<a id="fix-cnvtb" href="javascript:void(0)"
|
||||
><span
|
||||
class="glyphicon glyphicon-pushpin"
|
||||
aria-hidden="true"
|
||||
></span
|
||||
></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<span class="h2">
|
||||
A - Happy Birthday!
|
||||
<a
|
||||
class="btn btn-default btn-sm"
|
||||
href="/contests/abc100/tasks/abc100_a/editorial"
|
||||
>Editorial</a
|
||||
>
|
||||
</span>
|
||||
<span id="task-lang-btn" class="pull-right"
|
||||
><span data-lang="ja"
|
||||
><img src="//img.atcoder.jp/assets/top/img/flag-lang/ja.png"
|
||||
/></span>
|
||||
/
|
||||
<span data-lang="en"
|
||||
><img
|
||||
src="//img.atcoder.jp/assets/top/img/flag-lang/en.png" /></span
|
||||
></span>
|
||||
<script>
|
||||
$(function () {
|
||||
var ts = $('#task-statement span.lang')
|
||||
if (ts.children('span').size() <= 1) {
|
||||
$('#task-lang-btn').hide()
|
||||
ts.children('span').show()
|
||||
return
|
||||
}
|
||||
|
||||
var REMEMBER_LB = 5
|
||||
var LS_KEY = 'task_lang'
|
||||
var taskLang = getLS(LS_KEY) || ''
|
||||
function isTaskLangSet(taskLang) {
|
||||
return taskLang === 'ja' || taskLang === 'en'
|
||||
}
|
||||
if (isTaskLangSet(taskLang)) {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
if (params.get('lang')) {
|
||||
setLS(LS_KEY, REMEMBER_LB)
|
||||
taskLang = LANG
|
||||
}
|
||||
} else {
|
||||
taskLang = LANG
|
||||
}
|
||||
ts.children('span.lang-' + taskLang).show()
|
||||
|
||||
$('#task-lang-btn span').click(function () {
|
||||
var l = $(this).data('lang')
|
||||
ts.children('span').hide()
|
||||
ts.children('span.lang-' + l).show()
|
||||
|
||||
taskLang = getLS(LS_KEY) || ''
|
||||
let changeTimes = 0
|
||||
if (isTaskLangSet(taskLang)) {
|
||||
changeTimes = REMEMBER_LB
|
||||
} else {
|
||||
changeTimes = parseInt(taskLang, 10)
|
||||
if (isNaN(changeTimes)) changeTimes = 0
|
||||
changeTimes++
|
||||
}
|
||||
if (changeTimes < REMEMBER_LB) setLS(LS_KEY, changeTimes)
|
||||
else setLS(LS_KEY, l)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<hr />
|
||||
<p>Time Limit: 2 sec / Memory Limit: 976 MiB</p>
|
||||
|
||||
<div id="task-statement">
|
||||
<span class="lang">
|
||||
<span class="lang-ja">
|
||||
<p>配点: <var>100</var> 点</p>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>問題文</h3>
|
||||
<p>
|
||||
もうすぐ E869120 君と square1001 君の
|
||||
<var>16</var> 才の誕生日が来る.<br />
|
||||
そこで, AtCoder 王国の高橋君は, 円形のケーキ
|
||||
<var>1</var> 個に放射状に切れ目を入れ
|
||||
<var>16</var> 等分したものを, 彼らにプレゼントした.
|
||||
</p>
|
||||
<p>
|
||||
E869120 君はそのうち <var>A</var> 切れ、square1001 君は
|
||||
<var>B</var> 切れを食べようとした.<br />
|
||||
しかし, ケーキと一緒についていた紙を見ると,
|
||||
「同じ人が隣り合う
|
||||
<var>2</var>
|
||||
切れのケーキを両方取ってはならない」と書かれていた.
|
||||
</p>
|
||||
<p>
|
||||
さて、彼らは紙に書かれたことを守って、<var>2</var>
|
||||
人とも食べたい数のケーキを取ることができるだろうか?
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>制約</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<var>A, B</var> は <var>1</var> 以上
|
||||
<var>16</var> 以下の整数
|
||||
</li>
|
||||
<li><var>A+B</var> は <var>16</var> 以下である.</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="io-style">
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>入力</h3>
|
||||
<p>入力は以下の形式で標準入力から与えられる.</p>
|
||||
<pre><var>A</var> <var>B</var>
|
||||
</pre>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>出力</h3>
|
||||
<p>
|
||||
紙に書かれたことを守って, E869120 君と square1001
|
||||
君両方が, 食べたい数のケーキを取ることができるならば
|
||||
<code>Yay!</code>, そうでなければ
|
||||
<code>:(</code> と出力しなさい.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>入力例 1</h3>
|
||||
<pre>
|
||||
5 4
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>出力例 1</h3>
|
||||
<pre>
|
||||
Yay!
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
下の図のようにケーキを取れば、<var>2</var>
|
||||
人とも目標を達成することができる.<br />
|
||||
<img
|
||||
alt=" "
|
||||
src="https://img.atcoder.jp/abc100/e87fa456a900ac9ae36671ae8bd5eeea.png"
|
||||
/>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>入力例 2</h3>
|
||||
<pre>
|
||||
8 8
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>出力例 2</h3>
|
||||
<pre>
|
||||
Yay!
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
下の図のようにケーキを取れば、<var>2</var>
|
||||
人とも目標を達成することができる.<br />
|
||||
<img
|
||||
alt=" "
|
||||
src="https://img.atcoder.jp/abc100/a7989ac033e6ba86e14078864c20d9c5.png"
|
||||
/>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>入力例 3</h3>
|
||||
<pre>
|
||||
11 4
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>出力例 3</h3>
|
||||
<pre>
|
||||
:(
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
この場合, 残念ながら目標を達成する方法は
|
||||
<var>1</var> つもない.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</span>
|
||||
<span class="lang-en">
|
||||
<p>Score: <var>100</var> points</p>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Problem Statement</h3>
|
||||
<p>
|
||||
E869120's and square1001's <var>16</var>-th birthday is
|
||||
coming soon.<br />
|
||||
Takahashi from AtCoder Kingdom gave them a round cake
|
||||
cut into <var>16</var> equal fan-shaped pieces.
|
||||
</p>
|
||||
<p>
|
||||
E869120 and square1001 were just about to eat
|
||||
<var>A</var> and <var>B</var> of those pieces,
|
||||
respectively,<br />
|
||||
when they found a note attached to the cake saying that
|
||||
"the same person should not take two adjacent pieces of
|
||||
cake".
|
||||
</p>
|
||||
<p>
|
||||
Can both of them obey the instruction in the note and
|
||||
take desired numbers of pieces of cake?
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Constraints</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<var>A</var> and <var>B</var> are integers between
|
||||
<var>1</var> and <var>16</var> (inclusive).
|
||||
</li>
|
||||
<li><var>A+B</var> is at most <var>16</var>.</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="io-style">
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Input</h3>
|
||||
<p>
|
||||
Input is given from Standard Input in the following
|
||||
format:
|
||||
</p>
|
||||
<pre><var>A</var> <var>B</var>
|
||||
</pre>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Output</h3>
|
||||
<p>
|
||||
If both E869120 and square1001 can obey the
|
||||
instruction in the note and take desired numbers of
|
||||
pieces of cake, print <code>Yay!</code>; otherwise,
|
||||
print <code>:(</code>.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Input 1</h3>
|
||||
<pre>
|
||||
5 4
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Output 1</h3>
|
||||
<pre>
|
||||
Yay!
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
Both of them can take desired number of pieces as
|
||||
follows:
|
||||
<img
|
||||
alt=" "
|
||||
src="https://img.atcoder.jp/abc100/e87fa456a900ac9ae36671ae8bd5eeea.png"
|
||||
/>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Input 2</h3>
|
||||
<pre>
|
||||
8 8
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Output 2</h3>
|
||||
<pre>
|
||||
Yay!
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
Both of them can take desired number of pieces as
|
||||
follows:
|
||||
<img
|
||||
alt=" "
|
||||
src="https://img.atcoder.jp/abc100/a7989ac033e6ba86e14078864c20d9c5.png"
|
||||
/>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Input 3</h3>
|
||||
<pre>
|
||||
11 4
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Output 3</h3>
|
||||
<pre>
|
||||
:(
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
In this case, there is no way for them to take desired
|
||||
number of pieces, unfortunately.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div
|
||||
class="a2a_kit a2a_kit_size_20 a2a_default_style pull-right"
|
||||
data-a2a-url="https://atcoder.jp/contests/abc100/tasks/abc100_a?lang=en"
|
||||
data-a2a-title="A - Happy Birthday!"
|
||||
>
|
||||
<a class="a2a_button_facebook"></a>
|
||||
<a class="a2a_button_twitter"></a>
|
||||
|
||||
<a class="a2a_button_telegram"></a>
|
||||
|
||||
<a class="a2a_dd" href="https://www.addtoany.com/share"></a>
|
||||
</div>
|
||||
|
||||
<script async src="//static.addtoany.com/menu/page.js"></script>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<div class="container" style="margin-bottom: 80px">
|
||||
<footer class="footer">
|
||||
<ul>
|
||||
<li><a href="/contests/abc100/rules">Rule</a></li>
|
||||
<li><a href="/contests/abc100/glossary">Glossary</a></li>
|
||||
</ul>
|
||||
|
||||
<ul>
|
||||
<li><a href="/tos">Terms of service</a></li>
|
||||
<li><a href="/privacy">Privacy Policy</a></li>
|
||||
<li><a href="/personal">Information Protection Policy</a></li>
|
||||
<li><a href="/company">Company</a></li>
|
||||
<li><a href="/faq">FAQ</a></li>
|
||||
<li><a href="/contact">Contact</a></li>
|
||||
</ul>
|
||||
<div class="text-center">
|
||||
<small id="copyright"
|
||||
>Copyright Since 2012 ©<a href="http://atcoder.co.jp"
|
||||
>AtCoder Inc.</a
|
||||
>
|
||||
All rights reserved.</small
|
||||
>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<p id="fixed-server-timer" class="contest-timer"></p>
|
||||
<div id="scroll-page-top" style="display: none">
|
||||
<span class="glyphicon glyphicon-arrow-up" aria-hidden="true"></span> Page
|
||||
Top
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
887
tests/fixtures/atcoder/task_abc100_b.html
vendored
Normal file
887
tests/fixtures/atcoder/task_abc100_b.html
vendored
Normal file
|
|
@ -0,0 +1,887 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>B - Ringo's Favorite Numbers</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="Content-Language" content="en" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta
|
||||
name="google-site-verification"
|
||||
content="nXGC_JxO0yoP1qBzMnYD_xgufO6leSLw1kyNo2HZltM"
|
||||
/>
|
||||
|
||||
<script
|
||||
async
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-RC512FD18N"
|
||||
></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || []
|
||||
function gtag() {
|
||||
dataLayer.push(arguments)
|
||||
}
|
||||
gtag('js', new Date())
|
||||
gtag('set', 'user_properties', {
|
||||
login_status: 'logged_out'
|
||||
})
|
||||
gtag('config', 'G-RC512FD18N')
|
||||
</script>
|
||||
|
||||
<meta
|
||||
name="description"
|
||||
content="AtCoder is a programming contest site for anyone from beginners to experts. We hold weekly programming contests online."
|
||||
/>
|
||||
<meta name="author" content="AtCoder Inc." />
|
||||
|
||||
<meta property="og:site_name" content="AtCoder" />
|
||||
|
||||
<meta property="og:title" content="B - Ringo's Favorite Numbers" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="AtCoder is a programming contest site for anyone from beginners to experts. We hold weekly programming contests online."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:url"
|
||||
content="https://atcoder.jp/contests/abc100/tasks/abc100_b"
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://img.atcoder.jp/assets/atcoder.png"
|
||||
/>
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="@atcoder" />
|
||||
|
||||
<meta property="twitter:title" content="B - Ringo's Favorite Numbers" />
|
||||
|
||||
<link
|
||||
href="//fonts.googleapis.com/css?family=Lato:400,700"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="//img.atcoder.jp/public/6372bb3/css/bootstrap.min.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="//img.atcoder.jp/public/6372bb3/css/base.css"
|
||||
/>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/png"
|
||||
href="//img.atcoder.jp/assets/favicon.png"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="//img.atcoder.jp/assets/atcoder.png" />
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/lib/jquery-1.9.1.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/lib/bootstrap.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/js.cookie.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/moment.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/moment_js-ja.js"></script>
|
||||
<script>
|
||||
var LANG = 'en'
|
||||
var userScreenName = ''
|
||||
var csrfToken = 'jcqZoHtgdlDPapU/uheo04+cw+2+EssGGNrF7tJr004='
|
||||
</script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/utils.js"></script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/contest.js"></script>
|
||||
<link
|
||||
href="//img.atcoder.jp/public/6372bb3/css/contest.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script>
|
||||
var contestScreenName = 'abc100'
|
||||
var remainingText = 'Remaining Time'
|
||||
var countDownText = 'Contest begins in'
|
||||
var startTime = moment('2018-06-16T21:00:00+09:00')
|
||||
var endTime = moment('2018-06-16T22:40:00+09:00')
|
||||
</script>
|
||||
<style></style>
|
||||
|
||||
<link
|
||||
href="//img.atcoder.jp/public/6372bb3/css/cdn/select2.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="//img.atcoder.jp/public/6372bb3/css/cdn/select2-bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/lib/select2.min.js"></script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/ace/ace.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/ace/ext-language_tools.js"></script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/run_prettify.js"></script>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="//img.atcoder.jp/public/6372bb3/css/cdn/katex.min.css"
|
||||
/>
|
||||
<script
|
||||
defer
|
||||
src="//img.atcoder.jp/public/6372bb3/js/cdn/katex.min.js"
|
||||
></script>
|
||||
<script
|
||||
defer
|
||||
src="//img.atcoder.jp/public/6372bb3/js/cdn/auto-render.min.js"
|
||||
></script>
|
||||
<script>
|
||||
$(function () {
|
||||
$('var').each(function () {
|
||||
var html = $(this)
|
||||
.html()
|
||||
.replace(/<sub>/g, '_{')
|
||||
.replace(/<\/sub>/g, '}')
|
||||
$(this).html('\\(' + html + '\\)')
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<script>
|
||||
var katexOptions = {
|
||||
delimiters: [
|
||||
{ left: '$$', right: '$$', display: true },
|
||||
|
||||
{ left: '\\(', right: '\\)', display: false },
|
||||
{ left: '\\[', right: '\\]', display: true }
|
||||
],
|
||||
ignoredTags: [
|
||||
'script',
|
||||
'noscript',
|
||||
'style',
|
||||
'textarea',
|
||||
'code',
|
||||
'option'
|
||||
],
|
||||
ignoredClasses: ['prettyprint', 'source-code-for-copy'],
|
||||
throwOnError: false
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
renderMathInElement(document.body, katexOptions)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/base.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
var __pParams = __pParams || []
|
||||
__pParams.push({
|
||||
client_id: '468',
|
||||
c_1: 'atcodercontest',
|
||||
c_2: 'ClientSite'
|
||||
})
|
||||
</script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://cdn.d2-apps.net/js/tr.js"
|
||||
async
|
||||
></script>
|
||||
|
||||
<div
|
||||
id="modal-contest-start"
|
||||
class="modal fade"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
>
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Contest started</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>AtCoder Beginner Contest 100 has begun.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-contest-end" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Contest is over</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>AtCoder Beginner Contest 100 has ended.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="main-div" class="float-container">
|
||||
<nav class="navbar navbar-inverse navbar-fixed-top">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button
|
||||
type="button"
|
||||
class="navbar-toggle collapsed"
|
||||
data-toggle="collapse"
|
||||
data-target="#navbar-collapse"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="icon-bar"></span><span class="icon-bar"></span
|
||||
><span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="/home"></a>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse" id="navbar-collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li>
|
||||
<a class="contest-title" href="/contests/abc100"
|
||||
>AtCoder Beginner Contest 100</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<a
|
||||
class="dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
href="#"
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<img src="//img.atcoder.jp/assets/top/img/flag-lang/en.png" />
|
||||
English <span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="/contests/abc100/tasks/abc100_b?lang=ja"
|
||||
><img
|
||||
src="//img.atcoder.jp/assets/top/img/flag-lang/ja.png"
|
||||
/>
|
||||
日本語</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contests/abc100/tasks/abc100_b?lang=en"
|
||||
><img
|
||||
src="//img.atcoder.jp/assets/top/img/flag-lang/en.png"
|
||||
/>
|
||||
English</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
href="/register?continue=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc100%2Ftasks%2Fabc100_b"
|
||||
>Sign Up</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/login?continue=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc100%2Ftasks%2Fabc100_b"
|
||||
>Sign In</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
name="form_logout"
|
||||
action="/logout?continue=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc100%2Ftasks%2Fabc100_b"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="csrf_token"
|
||||
value="jcqZoHtgdlDPapU/uheo04+cw+2+EssGGNrF7tJr004="
|
||||
/>
|
||||
</form>
|
||||
<div id="main-container" class="container" style="padding-top: 50px">
|
||||
<div class="row">
|
||||
<div id="contest-nav-tabs" class="col-sm-12 mb-2 cnvtb-fixed">
|
||||
<div>
|
||||
<small class="contest-duration">
|
||||
Contest Duration:
|
||||
<a
|
||||
href="http://www.timeanddate.com/worldclock/fixedtime.html?iso=20180616T2100&p1=248"
|
||||
target="blank"
|
||||
><time class="fixtime fixtime-full"
|
||||
>2018-06-16 21:00:00+0900</time
|
||||
></a
|
||||
>
|
||||
-
|
||||
<a
|
||||
href="http://www.timeanddate.com/worldclock/fixedtime.html?iso=20180616T2240&p1=248"
|
||||
target="blank"
|
||||
><time class="fixtime fixtime-full"
|
||||
>2018-06-16 22:40:00+0900</time
|
||||
></a
|
||||
>
|
||||
(local time) (100 minutes)
|
||||
</small>
|
||||
<small class="back-to-home pull-right"
|
||||
><a href="/home">Back to Home</a></small
|
||||
>
|
||||
</div>
|
||||
<ul class="nav nav-tabs">
|
||||
<li>
|
||||
<a href="/contests/abc100"
|
||||
><span
|
||||
class="glyphicon glyphicon-home"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Top</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li class="active">
|
||||
<a href="/contests/abc100/tasks"
|
||||
><span
|
||||
class="glyphicon glyphicon-tasks"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Tasks</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/clarifications"
|
||||
><span
|
||||
class="glyphicon glyphicon-question-sign"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Clarifications <span id="clar-badge" class="badge"></span
|
||||
></a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
href="#"
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
><span
|
||||
class="glyphicon glyphicon-list"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Results<span class="caret"></span
|
||||
></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="/contests/abc100/submissions"
|
||||
><span
|
||||
class="glyphicon glyphicon-globe"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
All Submissions</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/standings"
|
||||
><span
|
||||
class="glyphicon glyphicon-sort-by-attributes-alt"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Standings</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/standings/virtual"
|
||||
><span
|
||||
class="glyphicon glyphicon-sort-by-attributes-alt"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Virtual Standings</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/editorial"
|
||||
><span
|
||||
class="glyphicon glyphicon-book"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Editorial</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li class="pull-right">
|
||||
<a id="fix-cnvtb" href="javascript:void(0)"
|
||||
><span
|
||||
class="glyphicon glyphicon-pushpin"
|
||||
aria-hidden="true"
|
||||
></span
|
||||
></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<span class="h2">
|
||||
B - Ringo's Favorite Numbers
|
||||
<a
|
||||
class="btn btn-default btn-sm"
|
||||
href="/contests/abc100/tasks/abc100_b/editorial"
|
||||
>Editorial</a
|
||||
>
|
||||
</span>
|
||||
<span id="task-lang-btn" class="pull-right"
|
||||
><span data-lang="ja"
|
||||
><img src="//img.atcoder.jp/assets/top/img/flag-lang/ja.png"
|
||||
/></span>
|
||||
/
|
||||
<span data-lang="en"
|
||||
><img
|
||||
src="//img.atcoder.jp/assets/top/img/flag-lang/en.png" /></span
|
||||
></span>
|
||||
<script>
|
||||
$(function () {
|
||||
var ts = $('#task-statement span.lang')
|
||||
if (ts.children('span').size() <= 1) {
|
||||
$('#task-lang-btn').hide()
|
||||
ts.children('span').show()
|
||||
return
|
||||
}
|
||||
|
||||
var REMEMBER_LB = 5
|
||||
var LS_KEY = 'task_lang'
|
||||
var taskLang = getLS(LS_KEY) || ''
|
||||
function isTaskLangSet(taskLang) {
|
||||
return taskLang === 'ja' || taskLang === 'en'
|
||||
}
|
||||
if (isTaskLangSet(taskLang)) {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
if (params.get('lang')) {
|
||||
setLS(LS_KEY, REMEMBER_LB)
|
||||
taskLang = LANG
|
||||
}
|
||||
} else {
|
||||
taskLang = LANG
|
||||
}
|
||||
ts.children('span.lang-' + taskLang).show()
|
||||
|
||||
$('#task-lang-btn span').click(function () {
|
||||
var l = $(this).data('lang')
|
||||
ts.children('span').hide()
|
||||
ts.children('span.lang-' + l).show()
|
||||
|
||||
taskLang = getLS(LS_KEY) || ''
|
||||
let changeTimes = 0
|
||||
if (isTaskLangSet(taskLang)) {
|
||||
changeTimes = REMEMBER_LB
|
||||
} else {
|
||||
changeTimes = parseInt(taskLang, 10)
|
||||
if (isNaN(changeTimes)) changeTimes = 0
|
||||
changeTimes++
|
||||
}
|
||||
if (changeTimes < REMEMBER_LB) setLS(LS_KEY, changeTimes)
|
||||
else setLS(LS_KEY, l)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<hr />
|
||||
<p>Time Limit: 2 sec / Memory Limit: 976 MiB</p>
|
||||
|
||||
<div id="task-statement">
|
||||
<span class="lang">
|
||||
<span class="lang-ja">
|
||||
<p>配点: <var>200</var> 点</p>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>問題文</h3>
|
||||
<p>
|
||||
今日は, 記念すべき AtCoder Beginner Contest 100
|
||||
が開催される. そのため, 高橋君はりんごさんに,
|
||||
ある整数をプレゼントしようと思った.<br />
|
||||
今日のコンテストは「AtCoder Beginner Contest
|
||||
100」なので, りんごさんは <var>100</var> で
|
||||
<strong>ちょうど</strong>
|
||||
<var>D</var>
|
||||
回割りきれる正の整数をプレゼントされると喜ぶ.
|
||||
</p>
|
||||
<p>
|
||||
さて, りんごさんがプレゼントされると喜ぶような整数のうち
|
||||
<var>N</var> 番目に小さいものを求めなさい.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>制約</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<var>D</var> は <var>0, 1, 2</var> のいずれかである
|
||||
</li>
|
||||
<li>
|
||||
<var>N</var> は <var>1</var> 以上
|
||||
<var>100</var> 以下の整数
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="io-style">
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>入力</h3>
|
||||
<p>入力は以下の形式で標準入力から与えられる.</p>
|
||||
<pre><var>D</var> <var>N</var>
|
||||
</pre>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>出力</h3>
|
||||
<p>
|
||||
<var>100</var> でちょうど
|
||||
<var>D</var> 回割りきれる正の整数の中で
|
||||
<var>N</var> 番目に小さいものを出力しなさい.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>入力例 1</h3>
|
||||
<pre>
|
||||
0 5
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>出力例 1</h3>
|
||||
<pre>
|
||||
5
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
<var>100</var> でちょうど
|
||||
<var>0</var> 回割り切れる(すなわち,
|
||||
<var>100</var> で割り切れない)整数は, <var>1</var>,
|
||||
<var>2</var>, <var>3</var>, <var>4</var>, <var>5</var>,
|
||||
<var>6</var>, <var>7</var>, ... と続く.<br />
|
||||
よって, <var>5</var> 番目に小さいりんごさんが喜ぶ整数は
|
||||
<var>5</var> である.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>入力例 2</h3>
|
||||
<pre>
|
||||
1 11
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>出力例 2</h3>
|
||||
<pre>
|
||||
1100
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
<var>100</var> でちょうど
|
||||
<var>1</var> 回割り切れる整数は, <var>100</var>,
|
||||
<var>200</var>, <var>300</var>, <var>400</var>,
|
||||
<var>500</var>, <var>600</var>, <var>700</var>,
|
||||
<var>800</var>, <var>900</var>, <var>1 \ 000</var>,
|
||||
<var>1 \ 100</var>, ... と続く.<br />
|
||||
よって, 求めたい整数は <var>1 \ 100</var> である.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>入力例 3</h3>
|
||||
<pre>
|
||||
2 85
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>出力例 3</h3>
|
||||
<pre>
|
||||
850000
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
<var>100</var> でちょうど
|
||||
<var>2</var> 回割り切れる整数は, <var>10 \ 000</var>,
|
||||
<var>20 \ 000</var>, <var>30 \ 000</var>, ... と続く.<br />
|
||||
よって, 求めたい整数は <var>850 \ 000</var> である.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</span>
|
||||
<span class="lang-en">
|
||||
<p>Score: <var>200</var> points</p>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Problem Statement</h3>
|
||||
<p>
|
||||
Today, the memorable AtCoder Beginner Contest 100 takes
|
||||
place. On this occasion, Takahashi would like to give an
|
||||
integer to Ringo.<br />
|
||||
As the name of the contest is AtCoder Beginner Contest
|
||||
100, Ringo would be happy if he is given a positive
|
||||
integer that can be divided by <var>100</var>
|
||||
<strong>exactly</strong> <var>D</var> times.
|
||||
</p>
|
||||
<p>
|
||||
Find the <var>N</var>-th smallest integer that would
|
||||
make Ringo happy.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Constraints</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<var>D</var> is <var>0</var>, <var>1</var> or
|
||||
<var>2</var>.
|
||||
</li>
|
||||
<li>
|
||||
<var>N</var> is an integer between <var>1</var> and
|
||||
<var>100</var> (inclusive).
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="io-style">
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Input</h3>
|
||||
<p>
|
||||
Input is given from Standard Input in the following
|
||||
format:
|
||||
</p>
|
||||
<pre><var>D</var> <var>N</var>
|
||||
</pre>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Output</h3>
|
||||
<p>
|
||||
Print the <var>N</var>-th smallest integer that can be
|
||||
divided by <var>100</var> exactly <var>D</var> times.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Input 1</h3>
|
||||
<pre>
|
||||
0 5
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Output 1</h3>
|
||||
<pre>
|
||||
5
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
The integers that can be divided by
|
||||
<var>100</var> exactly <var>0</var> times (that is, not
|
||||
divisible by <var>100</var>) are as follows:
|
||||
<var>1</var>, <var>2</var>, <var>3</var>, <var>4</var>,
|
||||
<var>5</var>, <var>6</var>, <var>7</var>, ...<br />
|
||||
Thus, the <var>5</var>-th smallest integer that would
|
||||
make Ringo happy is <var>5</var>.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Input 2</h3>
|
||||
<pre>
|
||||
1 11
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Output 2</h3>
|
||||
<pre>
|
||||
1100
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
The integers that can be divided by
|
||||
<var>100</var> exactly once are as follows:
|
||||
<var>100</var>, <var>200</var>, <var>300</var>,
|
||||
<var>400</var>, <var>500</var>, <var>600</var>,
|
||||
<var>700</var>, <var>800</var>, <var>900</var>,
|
||||
<var>1 \ 000</var>, <var>1 \ 100</var>, ...<br />
|
||||
Thus, the integer we are seeking is <var>1 \ 100</var>.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Input 3</h3>
|
||||
<pre>
|
||||
2 85
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Output 3</h3>
|
||||
<pre>
|
||||
850000
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
The integers that can be divided by
|
||||
<var>100</var> exactly twice are as follows:
|
||||
<var>10 \ 000</var>, <var>20 \ 000</var>,
|
||||
<var>30 \ 000</var>, ...<br />
|
||||
Thus, the integer we are seeking is
|
||||
<var>850 \ 000</var>.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div
|
||||
class="a2a_kit a2a_kit_size_20 a2a_default_style pull-right"
|
||||
data-a2a-url="https://atcoder.jp/contests/abc100/tasks/abc100_b?lang=en"
|
||||
data-a2a-title="B - Ringo's Favorite Numbers"
|
||||
>
|
||||
<a class="a2a_button_facebook"></a>
|
||||
<a class="a2a_button_twitter"></a>
|
||||
|
||||
<a class="a2a_button_telegram"></a>
|
||||
|
||||
<a class="a2a_dd" href="https://www.addtoany.com/share"></a>
|
||||
</div>
|
||||
|
||||
<script async src="//static.addtoany.com/menu/page.js"></script>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<div class="container" style="margin-bottom: 80px">
|
||||
<footer class="footer">
|
||||
<ul>
|
||||
<li><a href="/contests/abc100/rules">Rule</a></li>
|
||||
<li><a href="/contests/abc100/glossary">Glossary</a></li>
|
||||
</ul>
|
||||
|
||||
<ul>
|
||||
<li><a href="/tos">Terms of service</a></li>
|
||||
<li><a href="/privacy">Privacy Policy</a></li>
|
||||
<li><a href="/personal">Information Protection Policy</a></li>
|
||||
<li><a href="/company">Company</a></li>
|
||||
<li><a href="/faq">FAQ</a></li>
|
||||
<li><a href="/contact">Contact</a></li>
|
||||
</ul>
|
||||
<div class="text-center">
|
||||
<small id="copyright"
|
||||
>Copyright Since 2012 ©<a href="http://atcoder.co.jp"
|
||||
>AtCoder Inc.</a
|
||||
>
|
||||
All rights reserved.</small
|
||||
>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<p id="fixed-server-timer" class="contest-timer"></p>
|
||||
<div id="scroll-page-top" style="display: none">
|
||||
<span class="glyphicon glyphicon-arrow-up" aria-hidden="true"></span> Page
|
||||
Top
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
904
tests/fixtures/atcoder/task_abc100_c.html
vendored
Normal file
904
tests/fixtures/atcoder/task_abc100_c.html
vendored
Normal file
|
|
@ -0,0 +1,904 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>C - *3 or /2</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="Content-Language" content="en" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta
|
||||
name="google-site-verification"
|
||||
content="nXGC_JxO0yoP1qBzMnYD_xgufO6leSLw1kyNo2HZltM"
|
||||
/>
|
||||
|
||||
<script
|
||||
async
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-RC512FD18N"
|
||||
></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || []
|
||||
function gtag() {
|
||||
dataLayer.push(arguments)
|
||||
}
|
||||
gtag('js', new Date())
|
||||
gtag('set', 'user_properties', {
|
||||
login_status: 'logged_out'
|
||||
})
|
||||
gtag('config', 'G-RC512FD18N')
|
||||
</script>
|
||||
|
||||
<meta
|
||||
name="description"
|
||||
content="AtCoder is a programming contest site for anyone from beginners to experts. We hold weekly programming contests online."
|
||||
/>
|
||||
<meta name="author" content="AtCoder Inc." />
|
||||
|
||||
<meta property="og:site_name" content="AtCoder" />
|
||||
|
||||
<meta property="og:title" content="C - *3 or /2" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="AtCoder is a programming contest site for anyone from beginners to experts. We hold weekly programming contests online."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:url"
|
||||
content="https://atcoder.jp/contests/abc100/tasks/abc100_c"
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://img.atcoder.jp/assets/atcoder.png"
|
||||
/>
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="@atcoder" />
|
||||
|
||||
<meta property="twitter:title" content="C - *3 or /2" />
|
||||
|
||||
<link
|
||||
href="//fonts.googleapis.com/css?family=Lato:400,700"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="//img.atcoder.jp/public/6372bb3/css/bootstrap.min.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="//img.atcoder.jp/public/6372bb3/css/base.css"
|
||||
/>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/png"
|
||||
href="//img.atcoder.jp/assets/favicon.png"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="//img.atcoder.jp/assets/atcoder.png" />
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/lib/jquery-1.9.1.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/lib/bootstrap.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/js.cookie.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/moment.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/moment_js-ja.js"></script>
|
||||
<script>
|
||||
var LANG = 'en'
|
||||
var userScreenName = ''
|
||||
var csrfToken = 'KwoiS7wTPLvccvgUDoQZ6H++fkjXMCchJrW6/YFqOJM='
|
||||
</script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/utils.js"></script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/contest.js"></script>
|
||||
<link
|
||||
href="//img.atcoder.jp/public/6372bb3/css/contest.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script>
|
||||
var contestScreenName = 'abc100'
|
||||
var remainingText = 'Remaining Time'
|
||||
var countDownText = 'Contest begins in'
|
||||
var startTime = moment('2018-06-16T21:00:00+09:00')
|
||||
var endTime = moment('2018-06-16T22:40:00+09:00')
|
||||
</script>
|
||||
<style></style>
|
||||
|
||||
<link
|
||||
href="//img.atcoder.jp/public/6372bb3/css/cdn/select2.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="//img.atcoder.jp/public/6372bb3/css/cdn/select2-bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/lib/select2.min.js"></script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/ace/ace.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/ace/ext-language_tools.js"></script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/run_prettify.js"></script>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="//img.atcoder.jp/public/6372bb3/css/cdn/katex.min.css"
|
||||
/>
|
||||
<script
|
||||
defer
|
||||
src="//img.atcoder.jp/public/6372bb3/js/cdn/katex.min.js"
|
||||
></script>
|
||||
<script
|
||||
defer
|
||||
src="//img.atcoder.jp/public/6372bb3/js/cdn/auto-render.min.js"
|
||||
></script>
|
||||
<script>
|
||||
$(function () {
|
||||
$('var').each(function () {
|
||||
var html = $(this)
|
||||
.html()
|
||||
.replace(/<sub>/g, '_{')
|
||||
.replace(/<\/sub>/g, '}')
|
||||
$(this).html('\\(' + html + '\\)')
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<script>
|
||||
var katexOptions = {
|
||||
delimiters: [
|
||||
{ left: '$$', right: '$$', display: true },
|
||||
|
||||
{ left: '\\(', right: '\\)', display: false },
|
||||
{ left: '\\[', right: '\\]', display: true }
|
||||
],
|
||||
ignoredTags: [
|
||||
'script',
|
||||
'noscript',
|
||||
'style',
|
||||
'textarea',
|
||||
'code',
|
||||
'option'
|
||||
],
|
||||
ignoredClasses: ['prettyprint', 'source-code-for-copy'],
|
||||
throwOnError: false
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
renderMathInElement(document.body, katexOptions)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/base.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
var __pParams = __pParams || []
|
||||
__pParams.push({
|
||||
client_id: '468',
|
||||
c_1: 'atcodercontest',
|
||||
c_2: 'ClientSite'
|
||||
})
|
||||
</script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://cdn.d2-apps.net/js/tr.js"
|
||||
async
|
||||
></script>
|
||||
|
||||
<div
|
||||
id="modal-contest-start"
|
||||
class="modal fade"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
>
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Contest started</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>AtCoder Beginner Contest 100 has begun.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-contest-end" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Contest is over</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>AtCoder Beginner Contest 100 has ended.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="main-div" class="float-container">
|
||||
<nav class="navbar navbar-inverse navbar-fixed-top">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button
|
||||
type="button"
|
||||
class="navbar-toggle collapsed"
|
||||
data-toggle="collapse"
|
||||
data-target="#navbar-collapse"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="icon-bar"></span><span class="icon-bar"></span
|
||||
><span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="/home"></a>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse" id="navbar-collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li>
|
||||
<a class="contest-title" href="/contests/abc100"
|
||||
>AtCoder Beginner Contest 100</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<a
|
||||
class="dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
href="#"
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<img src="//img.atcoder.jp/assets/top/img/flag-lang/en.png" />
|
||||
English <span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="/contests/abc100/tasks/abc100_c?lang=ja"
|
||||
><img
|
||||
src="//img.atcoder.jp/assets/top/img/flag-lang/ja.png"
|
||||
/>
|
||||
日本語</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contests/abc100/tasks/abc100_c?lang=en"
|
||||
><img
|
||||
src="//img.atcoder.jp/assets/top/img/flag-lang/en.png"
|
||||
/>
|
||||
English</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
href="/register?continue=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc100%2Ftasks%2Fabc100_c"
|
||||
>Sign Up</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/login?continue=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc100%2Ftasks%2Fabc100_c"
|
||||
>Sign In</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
name="form_logout"
|
||||
action="/logout?continue=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc100%2Ftasks%2Fabc100_c"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="csrf_token"
|
||||
value="KwoiS7wTPLvccvgUDoQZ6H++fkjXMCchJrW6/YFqOJM="
|
||||
/>
|
||||
</form>
|
||||
<div id="main-container" class="container" style="padding-top: 50px">
|
||||
<div class="row">
|
||||
<div id="contest-nav-tabs" class="col-sm-12 mb-2 cnvtb-fixed">
|
||||
<div>
|
||||
<small class="contest-duration">
|
||||
Contest Duration:
|
||||
<a
|
||||
href="http://www.timeanddate.com/worldclock/fixedtime.html?iso=20180616T2100&p1=248"
|
||||
target="blank"
|
||||
><time class="fixtime fixtime-full"
|
||||
>2018-06-16 21:00:00+0900</time
|
||||
></a
|
||||
>
|
||||
-
|
||||
<a
|
||||
href="http://www.timeanddate.com/worldclock/fixedtime.html?iso=20180616T2240&p1=248"
|
||||
target="blank"
|
||||
><time class="fixtime fixtime-full"
|
||||
>2018-06-16 22:40:00+0900</time
|
||||
></a
|
||||
>
|
||||
(local time) (100 minutes)
|
||||
</small>
|
||||
<small class="back-to-home pull-right"
|
||||
><a href="/home">Back to Home</a></small
|
||||
>
|
||||
</div>
|
||||
<ul class="nav nav-tabs">
|
||||
<li>
|
||||
<a href="/contests/abc100"
|
||||
><span
|
||||
class="glyphicon glyphicon-home"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Top</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li class="active">
|
||||
<a href="/contests/abc100/tasks"
|
||||
><span
|
||||
class="glyphicon glyphicon-tasks"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Tasks</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/clarifications"
|
||||
><span
|
||||
class="glyphicon glyphicon-question-sign"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Clarifications <span id="clar-badge" class="badge"></span
|
||||
></a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
href="#"
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
><span
|
||||
class="glyphicon glyphicon-list"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Results<span class="caret"></span
|
||||
></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="/contests/abc100/submissions"
|
||||
><span
|
||||
class="glyphicon glyphicon-globe"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
All Submissions</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/standings"
|
||||
><span
|
||||
class="glyphicon glyphicon-sort-by-attributes-alt"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Standings</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/standings/virtual"
|
||||
><span
|
||||
class="glyphicon glyphicon-sort-by-attributes-alt"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Virtual Standings</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/editorial"
|
||||
><span
|
||||
class="glyphicon glyphicon-book"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Editorial</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li class="pull-right">
|
||||
<a id="fix-cnvtb" href="javascript:void(0)"
|
||||
><span
|
||||
class="glyphicon glyphicon-pushpin"
|
||||
aria-hidden="true"
|
||||
></span
|
||||
></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<span class="h2">
|
||||
C - *3 or /2
|
||||
<a
|
||||
class="btn btn-default btn-sm"
|
||||
href="/contests/abc100/tasks/abc100_c/editorial"
|
||||
>Editorial</a
|
||||
>
|
||||
</span>
|
||||
<span id="task-lang-btn" class="pull-right"
|
||||
><span data-lang="ja"
|
||||
><img src="//img.atcoder.jp/assets/top/img/flag-lang/ja.png"
|
||||
/></span>
|
||||
/
|
||||
<span data-lang="en"
|
||||
><img
|
||||
src="//img.atcoder.jp/assets/top/img/flag-lang/en.png" /></span
|
||||
></span>
|
||||
<script>
|
||||
$(function () {
|
||||
var ts = $('#task-statement span.lang')
|
||||
if (ts.children('span').size() <= 1) {
|
||||
$('#task-lang-btn').hide()
|
||||
ts.children('span').show()
|
||||
return
|
||||
}
|
||||
|
||||
var REMEMBER_LB = 5
|
||||
var LS_KEY = 'task_lang'
|
||||
var taskLang = getLS(LS_KEY) || ''
|
||||
function isTaskLangSet(taskLang) {
|
||||
return taskLang === 'ja' || taskLang === 'en'
|
||||
}
|
||||
if (isTaskLangSet(taskLang)) {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
if (params.get('lang')) {
|
||||
setLS(LS_KEY, REMEMBER_LB)
|
||||
taskLang = LANG
|
||||
}
|
||||
} else {
|
||||
taskLang = LANG
|
||||
}
|
||||
ts.children('span.lang-' + taskLang).show()
|
||||
|
||||
$('#task-lang-btn span').click(function () {
|
||||
var l = $(this).data('lang')
|
||||
ts.children('span').hide()
|
||||
ts.children('span.lang-' + l).show()
|
||||
|
||||
taskLang = getLS(LS_KEY) || ''
|
||||
let changeTimes = 0
|
||||
if (isTaskLangSet(taskLang)) {
|
||||
changeTimes = REMEMBER_LB
|
||||
} else {
|
||||
changeTimes = parseInt(taskLang, 10)
|
||||
if (isNaN(changeTimes)) changeTimes = 0
|
||||
changeTimes++
|
||||
}
|
||||
if (changeTimes < REMEMBER_LB) setLS(LS_KEY, changeTimes)
|
||||
else setLS(LS_KEY, l)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<hr />
|
||||
<p>Time Limit: 2 sec / Memory Limit: 976 MiB</p>
|
||||
|
||||
<div id="task-statement">
|
||||
<span class="lang">
|
||||
<span class="lang-ja">
|
||||
<p>配点: <var>300</var> 点</p>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>問題文</h3>
|
||||
<p>
|
||||
AtCoder Beginner Contest 100 の開催にともなって, AtCoder
|
||||
社では長さ <var>N</var> の数列 <var>a = </var>{<var
|
||||
>a_1, a_2, a_3, ..., a_N</var
|
||||
>} が飾られることになった. <br />
|
||||
社員のすぬけ君は, この数列で遊んでみようと思った.
|
||||
</p>
|
||||
<p>
|
||||
具体的には,
|
||||
以下の操作をできるだけ多くの回数繰り返そうと思った.
|
||||
</p>
|
||||
<pre><var>1 \leq i \leq N</var> を満たす全ての <var>i</var> に対して, それぞれ「<var>a_i</var> の値を <var>2</var> で割る」「<var>a_i</var> の値を <var>3</var> 倍する」のどちらかを行う.
|
||||
ただし, 全ての <var>i</var> に対して <var>3</var> 倍することはできず, 操作後の <var>a_i</var> の値は整数でなければならない.
|
||||
</pre>
|
||||
|
||||
<p>最大で何回の操作が可能か, 求めなさい.</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>制約</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<var>N</var> は <var>1</var> 以上
|
||||
<var>10 \ 000</var> 以下の整数
|
||||
</li>
|
||||
<li>
|
||||
<var>a_i</var> は <var>1</var> 以上
|
||||
<var>1 \ 000 \ 000 \ 000</var> 以下の整数
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="io-style">
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>入力</h3>
|
||||
<p>入力は以下の形式で標準入力から与えられる.</p>
|
||||
<pre><var>N</var>
|
||||
<var>a_1</var> <var>a_2</var> <var>a_3</var> <var>...</var> <var>a_N</var>
|
||||
</pre>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>出力</h3>
|
||||
<p>すぬけ君が行える最大の操作回数を出力しなさい.</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>入力例 1</h3>
|
||||
<pre>
|
||||
3
|
||||
5 2 4
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>出力例 1</h3>
|
||||
<pre>
|
||||
3
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
最初, 数列は <var>{5, 2, 4}</var> であるが,
|
||||
以下のように操作すれば
|
||||
<var>3</var> 回の操作を行うことができる.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
最初に, <var>a_1</var> を <var>3</var> 倍し,
|
||||
<var>a_2</var> を <var>3</var> 倍し, <var>a_3</var> を
|
||||
<var>2</var> で割る. すると数列は
|
||||
<var>{15, 6, 2}</var> となる.
|
||||
</li>
|
||||
<li>
|
||||
次に, <var>a_1</var> を <var>3</var> 倍し,
|
||||
<var>a_2</var> を <var>2</var> で割り,
|
||||
<var>a_3</var> を <var>3</var> 倍する. すると数列は
|
||||
<var>{45, 3, 6}</var> となる.
|
||||
</li>
|
||||
<li>
|
||||
最後に, <var>a_1</var> を <var>3</var> 倍し,
|
||||
<var>a_2</var> を <var>3</var> 倍し, <var>a_3</var> を
|
||||
<var>2</var> で割る. すると数列は
|
||||
<var>{135, 9, 3}</var> となる.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>入力例 2</h3>
|
||||
<pre>
|
||||
4
|
||||
631 577 243 199
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>出力例 2</h3>
|
||||
<pre>
|
||||
0
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
全ての要素が奇数なので, 操作はできない. よって答えは
|
||||
<var>0</var> である.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>入力例 3</h3>
|
||||
<pre>
|
||||
10
|
||||
2184 2126 1721 1800 1024 2528 3360 1945 1280 1776
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>出力例 3</h3>
|
||||
<pre>
|
||||
39
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
</span>
|
||||
<span class="lang-en">
|
||||
<p>Score: <var>300</var> points</p>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Problem Statement</h3>
|
||||
<p>
|
||||
As AtCoder Beginner Contest 100 is taking place, the
|
||||
office of AtCoder, Inc. is decorated with a sequence of
|
||||
length <var>N</var>, <var>a = </var>{<var
|
||||
>a_1, a_2, a_3, ..., a_N</var
|
||||
>}.<br />
|
||||
Snuke, an employee, would like to play with this
|
||||
sequence.
|
||||
</p>
|
||||
<p>
|
||||
Specifically, he would like to repeat the following
|
||||
operation as many times as possible:
|
||||
</p>
|
||||
<pre>For every <var>i</var> satisfying <var>1 \leq i \leq N</var>, perform one of the following: "divide <var>a_i</var> by <var>2</var>" and "multiply <var>a_i</var> by <var>3</var>".
|
||||
Here, choosing "multiply <var>a_i</var> by <var>3</var>" for every <var>i</var> is not allowed, and the value of <var>a_i</var> after the operation must be an integer.
|
||||
</pre>
|
||||
|
||||
<p>At most how many operations can be performed?</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Constraints</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<var>N</var> is an integer between <var>1</var> and
|
||||
<var>10 \ 000</var> (inclusive).
|
||||
</li>
|
||||
<li>
|
||||
<var>a_i</var> is an integer between <var>1</var> and
|
||||
<var>1 \ 000 \ 000 \ 000</var> (inclusive).
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="io-style">
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Input</h3>
|
||||
<p>
|
||||
Input is given from Standard Input in the following
|
||||
format:
|
||||
</p>
|
||||
<pre><var>N</var>
|
||||
<var>a_1</var> <var>a_2</var> <var>a_3</var> <var>...</var> <var>a_N</var>
|
||||
</pre>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Output</h3>
|
||||
<p>
|
||||
Print the maximum number of operations that Snuke can
|
||||
perform.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Input 1</h3>
|
||||
<pre>
|
||||
3
|
||||
5 2 4
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Output 1</h3>
|
||||
<pre>
|
||||
3
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
The sequence is initially <var>{5, 2, 4}</var>. Three
|
||||
operations can be performed as follows:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
First, multiply <var>a_1</var> by <var>3</var>,
|
||||
multiply <var>a_2</var> by <var>3</var> and divide
|
||||
<var>a_3</var> by <var>2</var>. The sequence is now
|
||||
<var>{15, 6, 2}</var>.
|
||||
</li>
|
||||
<li>
|
||||
Next, multiply <var>a_1</var> by <var>3</var>, divide
|
||||
<var>a_2</var> by <var>2</var> and multiply
|
||||
<var>a_3</var> by <var>3</var>. The sequence is now
|
||||
<var>{45, 3, 6}</var>.
|
||||
</li>
|
||||
<li>
|
||||
Finally, multiply <var>a_1</var> by <var>3</var>,
|
||||
multiply <var>a_2</var> by <var>3</var> and divide
|
||||
<var>a_3</var> by <var>2</var>. The sequence is now
|
||||
<var>{135, 9, 3}</var>.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Input 2</h3>
|
||||
<pre>
|
||||
4
|
||||
631 577 243 199
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Output 2</h3>
|
||||
<pre>
|
||||
0
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
No operation can be performed since all the elements are
|
||||
odd. Thus, the answer is <var>0</var>.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Input 3</h3>
|
||||
<pre>
|
||||
10
|
||||
2184 2126 1721 1800 1024 2528 3360 1945 1280 1776
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Output 3</h3>
|
||||
<pre>
|
||||
39
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div
|
||||
class="a2a_kit a2a_kit_size_20 a2a_default_style pull-right"
|
||||
data-a2a-url="https://atcoder.jp/contests/abc100/tasks/abc100_c?lang=en"
|
||||
data-a2a-title="C - *3 or /2"
|
||||
>
|
||||
<a class="a2a_button_facebook"></a>
|
||||
<a class="a2a_button_twitter"></a>
|
||||
|
||||
<a class="a2a_button_telegram"></a>
|
||||
|
||||
<a class="a2a_dd" href="https://www.addtoany.com/share"></a>
|
||||
</div>
|
||||
|
||||
<script async src="//static.addtoany.com/menu/page.js"></script>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<div class="container" style="margin-bottom: 80px">
|
||||
<footer class="footer">
|
||||
<ul>
|
||||
<li><a href="/contests/abc100/rules">Rule</a></li>
|
||||
<li><a href="/contests/abc100/glossary">Glossary</a></li>
|
||||
</ul>
|
||||
|
||||
<ul>
|
||||
<li><a href="/tos">Terms of service</a></li>
|
||||
<li><a href="/privacy">Privacy Policy</a></li>
|
||||
<li><a href="/personal">Information Protection Policy</a></li>
|
||||
<li><a href="/company">Company</a></li>
|
||||
<li><a href="/faq">FAQ</a></li>
|
||||
<li><a href="/contact">Contact</a></li>
|
||||
</ul>
|
||||
<div class="text-center">
|
||||
<small id="copyright"
|
||||
>Copyright Since 2012 ©<a href="http://atcoder.co.jp"
|
||||
>AtCoder Inc.</a
|
||||
>
|
||||
All rights reserved.</small
|
||||
>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<p id="fixed-server-timer" class="contest-timer"></p>
|
||||
<div id="scroll-page-top" style="display: none">
|
||||
<span class="glyphicon glyphicon-arrow-up" aria-hidden="true"></span> Page
|
||||
Top
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
1086
tests/fixtures/atcoder/task_abc100_d.html
vendored
Normal file
1086
tests/fixtures/atcoder/task_abc100_d.html
vendored
Normal file
File diff suppressed because it is too large
Load diff
4343
tests/fixtures/codechef/P1209.html
vendored
Normal file
4343
tests/fixtures/codechef/P1209.html
vendored
Normal file
File diff suppressed because it is too large
Load diff
116
tests/fixtures/codechef/START209.json
vendored
Normal file
116
tests/fixtures/codechef/START209.json
vendored
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
{
|
||||
"status": "success",
|
||||
"user": { "username": null },
|
||||
"code": "START209",
|
||||
"isRatedContest": "1",
|
||||
"isParentContestRated": "0",
|
||||
"name": "Starters 209 (Rated till 5 star)",
|
||||
"problems": [],
|
||||
"banner": "https:\/\/cdn.codechef.com\/download\/small-banner\/START209\/1760933061.png",
|
||||
"rules": "<h4>CodeChef: A Platform for Aspiring Programmers<\/h4>\n<p class=\"last\">CodeChef was created as a platform to help programmers make it big in the world of algorithms, computer programming, and programming contests. At CodeChef, our dedicated efforts are aimed at reviving the inner geek within you, as we proudly host a thrilling programming (coding) contest every Wednesday.<\/p>\n<h4>About CodeChef Starters:<\/h4>\n<p>CodeChef Starters is a short programming contest which takes place on every Wednesday\u00a0<\/p>\n<h4>Contest Details:<\/h4>\n<ul class=\"last\">\n<li><strong>D<\/strong><strong>uration: <\/strong>\u00a02.00 hours\u00a0<\/li>\n<li><strong>Start Date: <\/strong>Wednesday, 22nd October , 2025 at 20:00 HRS (IST)<\/li>\n<li><strong>End Date: <\/strong>Wednesday, 22nd October, 2025 at 22:00 HRS (IST)<\/li>\n<li>Check your timezone <a href=\"https:\/\/www.timeanddate.com\/worldclock\/fixedtime.html?msg=CodeChef+Starters+209&iso=20251022T20&p1=44&ah=2\" target=\"_blank\" rel=\"nofollow noreferrer noopener\">here<\/a>.<\/li>\n<\/ul>\n<h4>Eligibility Criteria: Anyone with a knack for programming<\/h4>\n<p class=\"last\">Our contests are open to all programmers across the globe.<\/p>\n<h4>What's in it for you?<\/h4>\n<p>The idea behind these programming contests is that we want you to learn while competing. Also, we believe that it is alright to refer to tutorials, books, and other materials, learn a concept, and then apply the same to solve a problem during a contest. But it is <strong>not alright to copy other people's solutions or seek other people's help to solve a problem. <\/strong>All the participants are expected to abide to <a class=\"button blue\" href=\"..\/codeofconduct\">CodeChef's Code Of Conduct<\/a>.<\/p>\n<h4>Rules and Regulations:<\/h4>\n<ul>\n<li>This is an IOI-style contest. This means that the problems will be partially graded. You will get the score for passing certain test data.<\/li>\n<li>The details of the failed test cases will also be visible on your solution page.<\/li>\n<li>You can submit solutions as many times as you'd like, there are no penalties for incorrect submissions. Only your best correct submission will be considered.<\/li>\n<li>Those who achieve the score first will be placed higher in the ranklist in case of a tie.<\/li>\n<li><strong>We have removed all the Institutions that we could not identify from our database. We request you to update your institutions once again by going to your profile page.<\/strong><\/li>\n<li>You can also send in your queries in an email to <a href=\"mailto:help@codechef.com\" target=\"_blank\" rel=\"noreferrer noopener\">help@codechef.com<\/a>, during the contest.<\/li>\n<li>Please do not discuss strategy, suggestions, or tips in the comments during a live contest. Posting questions clarifying the problem statement is ok. If you are unsure, email us at <a href=\"mailto:feedback@codechef.com\" target=\"_blank\" rel=\"noreferrer noopener\"> feedback@codechef.com<\/a>.<\/li>\n<li>Discussing CodeChef's problems or any aspect of a problem, on any other platform on the web, on identification, could lead to the disabling of the respective account and banning from the community.<\/li>\n<\/ul>\n<p><strong>Note: You can now \"Code, Compile, and Run\" your codes on our <a href=\"..\/ide\">Online IDE<\/a>.<\/strong><\/p>\n<p>However, if you are using any other online development environment, make sure that other contestants don't have access to your code. As a contestant, you are responsible for making sure others don't access the code that you submit. If you use Ideone, make sure to mark your submission \"private\" (not secret)\".<\/p>",
|
||||
"time": {
|
||||
"start": 1761143400,
|
||||
"end": 1761150600,
|
||||
"freezing": 0,
|
||||
"current": 1761370410
|
||||
},
|
||||
"ip": "2603:7000:3900:1358:3959:b692:6cf3:cb03",
|
||||
"announcements": "<p><strong>CodeChef \u00d7 Coding Club League (2025-26)<\/strong><br \/><br \/>Partner with CodeChef to build a strong coding culture on campus!<\/p>\n<p><strong>Benefits for Clubs:<\/strong><\/p>\n<ul>\n<li>Platform access and support for Annual Technical events \/ hackathons<\/li>\n<li>Pro access for winners<\/li>\n<li>Dashboard to track member progress<\/li>\n<li>Discounts on CodeChef Pro for all members<\/li>\n<li>Co-branding & promotion on CodeChef channels<br \/><br \/>\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0<strong style=\"text-align:center;\"><a class=\"button blue\" href=\"codechef-coding-club\" target=\"_blank\" rel=\"noreferrer noopener\">\u00a0Click Here To Know More<\/a><\/strong><\/li>\n<\/ul>\n<p><strong>\u00a0<\/strong><\/p>",
|
||||
"problemsstats": {
|
||||
"attempted": [],
|
||||
"partially_solved": [],
|
||||
"solved": [],
|
||||
"locked": []
|
||||
},
|
||||
"todos": [],
|
||||
"stats": null,
|
||||
"partial_scores": [],
|
||||
"isRanklistFrozen": false,
|
||||
"rank_and_score": { "score": "NA", "rank": "NA" },
|
||||
"is_a_parent_contest": true,
|
||||
"is_contest_elements_visible": true,
|
||||
"is_OTP_required": false,
|
||||
"is_linked_problems_contest": "0",
|
||||
"custom_contest_page_title": "",
|
||||
"custom_contest_page_meta_desc": "",
|
||||
"contest_introduction": "https:\/\/discuss.codechef.com\/t\/invitation-to-codechef-starters-209-rated-upto-5-stars-22nd-october\/124401",
|
||||
"contest_editorials": "https:\/\/discuss.codechef.com\/tag\/start209",
|
||||
"contest_video_editorials": "",
|
||||
"is_older_rating_based_division_system": false,
|
||||
"division_generation": 3,
|
||||
"isAssessmentContest": false,
|
||||
"penalisedUsersCount": 0,
|
||||
"ttl": 60,
|
||||
"child_contests": {
|
||||
"div_1": {
|
||||
"div": {
|
||||
"div_number": "1",
|
||||
"code": "div_1",
|
||||
"min_rating": 2000,
|
||||
"max_rating": 50000,
|
||||
"name": "Division 1",
|
||||
"description": "Users with rating above 2000"
|
||||
},
|
||||
"division_generation": 3,
|
||||
"contest_code": "START209A",
|
||||
"contest_link": "\/START209A"
|
||||
},
|
||||
"div_2": {
|
||||
"div": {
|
||||
"div_number": "2",
|
||||
"code": "div_2",
|
||||
"min_rating": 1600,
|
||||
"max_rating": 1999,
|
||||
"name": "Division 2",
|
||||
"description": "Users with rating between 1600 and 1999"
|
||||
},
|
||||
"division_generation": 3,
|
||||
"contest_code": "START209B",
|
||||
"contest_link": "\/START209B"
|
||||
},
|
||||
"div_3": {
|
||||
"div": {
|
||||
"div_number": "3",
|
||||
"code": "div_3",
|
||||
"min_rating": 1400,
|
||||
"max_rating": 1599,
|
||||
"name": "Division 3",
|
||||
"description": "Users with rating upto 1599"
|
||||
},
|
||||
"division_generation": 3,
|
||||
"contest_code": "START209C",
|
||||
"contest_link": "\/START209C"
|
||||
},
|
||||
"div_4": {
|
||||
"div": {
|
||||
"div_number": "4",
|
||||
"code": "div_4",
|
||||
"min_rating": 0,
|
||||
"max_rating": 1399,
|
||||
"name": "Division 4",
|
||||
"description": "Users with rating upto 1399"
|
||||
},
|
||||
"division_generation": 3,
|
||||
"contest_code": "START209D",
|
||||
"contest_link": "\/START209D"
|
||||
}
|
||||
},
|
||||
"user_rating_div": {
|
||||
"rating": -1,
|
||||
"div": {
|
||||
"code": "all",
|
||||
"min_rating": 0,
|
||||
"max_rating": 50000,
|
||||
"name": "All",
|
||||
"description": "All the users"
|
||||
}
|
||||
},
|
||||
"user_contest_code": null,
|
||||
"show_div_based_contest": false,
|
||||
"is_registration_enabled_contest": false,
|
||||
"is_flexi_time_contest": false,
|
||||
"duration": "120",
|
||||
"is_proctored": false,
|
||||
"autoRefresh": true,
|
||||
"visitedContests": []
|
||||
}
|
||||
202
tests/fixtures/codechef/START209D.json
vendored
Normal file
202
tests/fixtures/codechef/START209D.json
vendored
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
{
|
||||
"status": "success",
|
||||
"user": { "username": null },
|
||||
"code": "START209D",
|
||||
"isRatedContest": "1",
|
||||
"isParentContestRated": "1",
|
||||
"name": "Starters 209 (Rated)",
|
||||
"problems": {
|
||||
"P1209": {
|
||||
"code": "P1209",
|
||||
"name": "Bitcoin Market",
|
||||
"type": "3",
|
||||
"successful_submissions": "25131",
|
||||
"allow_submission": false,
|
||||
"accuracy": 85.680000000000007,
|
||||
"problem_url": "\/problems\/P1209",
|
||||
"submit_url": "\/problems\/P1209",
|
||||
"status_url": "\/status\/P1209",
|
||||
"is_added_to_practice": true,
|
||||
"total_submissions": "33093",
|
||||
"category_name": "main",
|
||||
"is_direct_submittable": false
|
||||
},
|
||||
"P2209": {
|
||||
"code": "P2209",
|
||||
"name": "Divisible Duel",
|
||||
"type": "3",
|
||||
"successful_submissions": "21888",
|
||||
"allow_submission": false,
|
||||
"accuracy": 64.159999999999997,
|
||||
"problem_url": "\/problems\/P2209",
|
||||
"submit_url": "\/problems\/P2209",
|
||||
"status_url": "\/status\/P2209",
|
||||
"is_added_to_practice": true,
|
||||
"total_submissions": "37437",
|
||||
"category_name": "main",
|
||||
"is_direct_submittable": false
|
||||
},
|
||||
"P3209": {
|
||||
"code": "P3209",
|
||||
"name": "Small GCD Sort",
|
||||
"type": "3",
|
||||
"successful_submissions": "13450",
|
||||
"allow_submission": false,
|
||||
"accuracy": 76.239999999999995,
|
||||
"problem_url": "\/problems\/P3209",
|
||||
"submit_url": "\/problems\/P3209",
|
||||
"status_url": "\/status\/P3209",
|
||||
"is_added_to_practice": true,
|
||||
"total_submissions": "19164",
|
||||
"category_name": "main",
|
||||
"is_direct_submittable": false
|
||||
},
|
||||
"P4209": {
|
||||
"code": "P4209",
|
||||
"name": "Tactical Conversion",
|
||||
"type": "3",
|
||||
"successful_submissions": "1567",
|
||||
"allow_submission": false,
|
||||
"accuracy": 8.4499999999999993,
|
||||
"problem_url": "\/problems\/P4209",
|
||||
"submit_url": "\/problems\/P4209",
|
||||
"status_url": "\/status\/P4209",
|
||||
"is_added_to_practice": true,
|
||||
"total_submissions": "20535",
|
||||
"category_name": "main",
|
||||
"is_direct_submittable": false
|
||||
},
|
||||
"P5209": {
|
||||
"code": "P5209",
|
||||
"name": "Binary Love",
|
||||
"type": "3",
|
||||
"successful_submissions": "3271",
|
||||
"allow_submission": false,
|
||||
"accuracy": 33.530000000000001,
|
||||
"problem_url": "\/problems\/P5209",
|
||||
"submit_url": "\/problems\/P5209",
|
||||
"status_url": "\/status\/P5209",
|
||||
"is_added_to_practice": true,
|
||||
"total_submissions": "11128",
|
||||
"category_name": "main",
|
||||
"is_direct_submittable": false
|
||||
},
|
||||
"P6209E": {
|
||||
"code": "P6209E",
|
||||
"name": "High Score (Easy Version)",
|
||||
"type": "3",
|
||||
"successful_submissions": "285",
|
||||
"allow_submission": false,
|
||||
"accuracy": 7.2800000000000002,
|
||||
"problem_url": "\/problems\/P6209E",
|
||||
"submit_url": "\/problems\/P6209E",
|
||||
"status_url": "\/status\/P6209E",
|
||||
"is_added_to_practice": true,
|
||||
"total_submissions": "4535",
|
||||
"category_name": "main",
|
||||
"is_direct_submittable": false
|
||||
},
|
||||
"P6209": {
|
||||
"code": "P6209",
|
||||
"name": "High Score (Hard Version)",
|
||||
"type": "3",
|
||||
"successful_submissions": "34",
|
||||
"allow_submission": false,
|
||||
"accuracy": 3.1899999999999999,
|
||||
"problem_url": "\/problems\/P6209",
|
||||
"submit_url": "\/problems\/P6209",
|
||||
"status_url": "\/status\/P6209",
|
||||
"is_added_to_practice": true,
|
||||
"total_submissions": "1159",
|
||||
"category_name": "main",
|
||||
"is_direct_submittable": false
|
||||
},
|
||||
"P7209": {
|
||||
"code": "P7209",
|
||||
"name": "Easy Grid Game",
|
||||
"type": "3",
|
||||
"successful_submissions": "80",
|
||||
"allow_submission": false,
|
||||
"accuracy": 5.1100000000000003,
|
||||
"problem_url": "\/problems\/P7209",
|
||||
"submit_url": "\/problems\/P7209",
|
||||
"status_url": "\/status\/P7209",
|
||||
"is_added_to_practice": true,
|
||||
"total_submissions": "1740",
|
||||
"category_name": "main",
|
||||
"is_direct_submittable": false
|
||||
},
|
||||
"P8209": {
|
||||
"code": "P8209",
|
||||
"name": "Counting Is Fun",
|
||||
"type": "3",
|
||||
"successful_submissions": "22",
|
||||
"allow_submission": false,
|
||||
"accuracy": 1.8200000000000001,
|
||||
"problem_url": "\/problems\/P8209",
|
||||
"submit_url": "\/problems\/P8209",
|
||||
"status_url": "\/status\/P8209",
|
||||
"is_added_to_practice": true,
|
||||
"total_submissions": "1261",
|
||||
"category_name": "main",
|
||||
"is_direct_submittable": false
|
||||
}
|
||||
},
|
||||
"banner": "https:\/\/cdn.codechef.com\/download\/small-banner\/START209D\/1760933097.png",
|
||||
"rules": "<h4>CodeChef: A Platform for Aspiring Programmers<\/h4>\n<p class=\"last\">CodeChef was created as a platform to help programmers make it big in the world of algorithms, computer programming, and programming contests. At CodeChef, our dedicated efforts are aimed at reviving the inner geek within you, as we proudly host a thrilling programming (coding) contest every Wednesday.<\/p>\n<h4>About CodeChef Starters:<\/h4>\n<p>CodeChef Starters is a short programming contest which takes place on every Wednesday\u00a0<\/p>\n<h4>Contest Details:<\/h4>\n<ul class=\"last\">\n<li><strong>D<\/strong><strong>uration: <\/strong>\u00a02.00 hours\u00a0<\/li>\n<li><strong>Start Date: <\/strong>Wednesday, 22nd October , 2025 at 20:00 HRS (IST)<\/li>\n<li><strong>End Date: <\/strong>Wednesday, 22nd October, 2025 at 22:00 HRS (IST)<\/li>\n<li>Check your timezone <a href=\"https:\/\/www.timeanddate.com\/worldclock\/fixedtime.html?msg=CodeChef+Starters+209&iso=20251022T20&p1=44&ah=2\" target=\"_blank\" rel=\"nofollow noreferrer noopener\">here<\/a>.<\/li>\n<\/ul>\n<h4>Eligibility Criteria: Anyone with a knack for programming<\/h4>\n<p class=\"last\">Our contests are open to all programmers across the globe.<\/p>\n<h4>What's in it for you?<\/h4>\n<p>The idea behind these programming contests is that we want you to learn while competing. Also, we believe that it is alright to refer to tutorials, books, and other materials, learn a concept, and then apply the same to solve a problem during a contest. But it is <strong>not alright to copy other people's solutions or seek other people's help to solve a problem. <\/strong>All the participants are expected to abide to <a class=\"button blue\" href=\"..\/codeofconduct\">CodeChef's Code Of Conduct<\/a>.<\/p>\n<h4>Rules and Regulations:<\/h4>\n<ul>\n<li>This is an IOI-style contest. This means that the problems will be partially graded. You will get the score for passing certain test data.<\/li>\n<li>The details of the failed test cases will also be visible on your solution page.<\/li>\n<li>You can submit solutions as many times as you'd like, there are no penalties for incorrect submissions. Only your best correct submission will be considered.<\/li>\n<li>Those who achieve the score first will be placed higher in the ranklist in case of a tie.<\/li>\n<li><strong>We have removed all the Institutions that we could not identify from our database. We request you to update your institutions once again by going to your profile page.<\/strong><\/li>\n<li>You can also send in your queries in an email to <a href=\"mailto:help@codechef.com\" target=\"_blank\" rel=\"noreferrer noopener\">help@codechef.com<\/a>, during the contest.<\/li>\n<li>Please do not discuss strategy, suggestions, or tips in the comments during a live contest. Posting questions clarifying the problem statement is ok. If you are unsure, email us at <a href=\"mailto:feedback@codechef.com\" target=\"_blank\" rel=\"noreferrer noopener\"> feedback@codechef.com<\/a>.<\/li>\n<li>Discussing CodeChef's problems or any aspect of a problem, on any other platform on the web, on identification, could lead to the disabling of the respective account and banning from the community.<\/li>\n<\/ul>\n<p><strong>Note: You can now \"Code, Compile, and Run\" your codes on our <a href=\"..\/ide\">Online IDE<\/a>.<\/strong><\/p>\n<p>However, if you are using any other online development environment, make sure that other contestants don't have access to your code. As a contestant, you are responsible for making sure others don't access the code that you submit. If you use Ideone, make sure to mark your submission \"private\" (not secret)\".<\/p>",
|
||||
"time": {
|
||||
"start": 1761143406,
|
||||
"end": 1761150606,
|
||||
"freezing": 0,
|
||||
"current": 1761365589
|
||||
},
|
||||
"ip": "2603:7000:3900:1358:3959:b692:6cf3:cb03",
|
||||
"announcements": "<p><strong>CodeChef \u00d7 Coding Club League (2025-26)<\/strong><br \/><br \/>Partner with CodeChef to build a strong coding culture on campus!<\/p>\n<p><strong>Benefits for Clubs:<\/strong><\/p>\n<ul>\n<li>Platform access and support for Annual Technical events \/ hackathons<\/li>\n<li>Pro access for winners<\/li>\n<li>Dashboard to track member progress<\/li>\n<li>Discounts on CodeChef Pro for all members<\/li>\n<li>Co-branding & promotion on CodeChef channels<br \/><br \/>\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0<strong style=\"text-align:center;\"><a class=\"button blue\" href=\"codechef-coding-club\" target=\"_blank\" rel=\"noreferrer noopener\">\u00a0Click Here To Know More<\/a><\/strong><\/li>\n<\/ul>\n<p><strong>\u00a0<\/strong><\/p>\n<p>\u00a0<\/p>",
|
||||
"problemsstats": {
|
||||
"attempted": [],
|
||||
"partially_solved": [],
|
||||
"solved": [],
|
||||
"locked": []
|
||||
},
|
||||
"todos": [],
|
||||
"stats": null,
|
||||
"partial_scores": {
|
||||
"P7209": [{ "score": "100", "count": "80" }],
|
||||
"P5209": [{ "score": "100", "count": "3271" }],
|
||||
"P4209": [{ "score": "100", "count": "1567" }],
|
||||
"P1209": [{ "score": "100", "count": "25131" }],
|
||||
"P3209": [{ "score": "100", "count": "13450" }],
|
||||
"P2209": [{ "score": "100", "count": "21888" }],
|
||||
"P8209": [{ "score": "100", "count": "22" }],
|
||||
"P6209": [{ "score": "100", "count": "34" }],
|
||||
"P6209E": [{ "score": "100", "count": "285" }]
|
||||
},
|
||||
"isRanklistFrozen": false,
|
||||
"rank_and_score": { "score": "NA", "rank": "NA" },
|
||||
"is_a_parent_contest": false,
|
||||
"is_contest_elements_visible": true,
|
||||
"is_OTP_required": false,
|
||||
"is_linked_problems_contest": "0",
|
||||
"custom_contest_page_title": "",
|
||||
"custom_contest_page_meta_desc": "",
|
||||
"contest_introduction": "https:\/\/discuss.codechef.com\/t\/invitation-to-codechef-starters-209-rated-upto-5-stars-22nd-october\/124401",
|
||||
"contest_editorials": "https:\/\/discuss.codechef.com\/tag\/start209",
|
||||
"contest_video_editorials": "",
|
||||
"is_older_rating_based_division_system": false,
|
||||
"division_generation": 3,
|
||||
"isAssessmentContest": false,
|
||||
"penalisedUsersCount": 0,
|
||||
"ttl": 60,
|
||||
"scorable_heading": "Scorable Problems for Division 4",
|
||||
"scorable_message": "",
|
||||
"division": "Division 4",
|
||||
"non_scorable_heading": "Non Scorable Problems for Practice",
|
||||
"non_scorable_message": "<p>The following problems are <b>NOT part of the contest<\/b>, and will not be counted towards your rankings and ratings. These are problems from the other Division(s), made available for you to practice. Click <a href='\/blogs\/how-does-codechef-rating-system-work'>here<\/a> to know more. They will be considered for plagiarism though.<\/p>",
|
||||
"is_registration_enabled_contest": false,
|
||||
"is_flexi_time_contest": false,
|
||||
"duration": "120",
|
||||
"is_proctored": false,
|
||||
"autoRefresh": true,
|
||||
"visitedContests": [],
|
||||
"user_live_ratings_update_frequency": 15
|
||||
}
|
||||
99
tests/fixtures/codechef/START209D_P1209.json
vendored
Normal file
99
tests/fixtures/codechef/START209D_P1209.json
vendored
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
{
|
||||
"category_name": "main",
|
||||
"contest_code": "START209D",
|
||||
"contest_name": "Starters 209 (Rated)",
|
||||
"status": "success",
|
||||
"submit_error": "You need to login to submit.",
|
||||
"is_verified": false,
|
||||
"problem_code": "P1209",
|
||||
"contest_category": "9",
|
||||
"problem_name": "Bitcoin Market",
|
||||
"intended_contest_code": "START209",
|
||||
"body": "This is an example problem statement in markdown, and a mini guide on writing statements. Please make sure to remove everything here before publishing your problem.\n\n- Codechef uses markdown for its problem statements. Markdown syntax can be found [here](https:\/\/github.com\/showdownjs\/showdown\/wiki\/Showdown's-Markdown-syntax). Note the `[text](link)` syntax to insert a hyperlink.\n- Codechef also uses $\\LaTeX$ to render mathematical expressions, and you are advised to make liberal use of it to make your statement look good.\n- Text can be made **bold** or *italicized*.\n- **Do not** use HTML tags (p, ul, li, pre, br, ...) in the statement.\n- To insert an image, first upload it to an online hosting service (for an official contest, ask a Codechef admin to do this for you \u2014 this is important) and then use the following syntax: ``.\n- If your problem doesn't contain subtasks, ensure that the Subtasks section below is disabled and **all content is deleted from it**.\n\nIf you face any issues, either contact a Codechef admin directly or send us an email at help@codechef.com.\n\nBelow is an example problem statement that uses some of the above-mentioned features.\n\n---------\n\nChef has a simple undirected graph $G$ with $N$ vertices and $M$ edges. A [subgraph](https:\/\/mathworld.wolfram.com\/Subgraph.html) $H$ of $G$ is called *good* if:\n- $H$ is connected\n- $H$ contains all $N$ vertices of $G$\n- There is a unique path between any two vertices in $H$, using only edges in $H$\n\nCount the number of *good* subgraphs of $G$. Since this number might be large, report it modulo $10^9 + 7$.\n\nIn other news, here's a completely unrelated image:\n\n.\n\n\n<aside style='background: #f8f8f8;padding: 10px 15px;'><div>All submissions for this problem are available.<\/div><\/aside>",
|
||||
"problemComponents": {
|
||||
"constraints": "- $1 \\leq R \\leq 10$",
|
||||
"constraintsState": true,
|
||||
"subtasks": "- **Subtask 1 (10 points):** $1 \\leq M \\leq 10$\n- **Subtask 2 (20 points):** The sum of $N$ across all test cases won't exceed $20$.\n- **Subtask 3 (70 points):** No further constraints.",
|
||||
"subtasksState": false,
|
||||
"statement": "Chef has recently started investing in **Bitcoin**. \nHe assigns a **market risk level** $R$ (from $1$ to $10$), where: \n\n- $1$ means the market is *very safe*, \n- $10$ means the market is *very risky*. \n\nChef will **buy Bitcoin** only if the risk level is **$4$ or less**. \n\nGiven the current risk level $R$, determine whether Chef should buy Bitcoin.\n\nPrint **\"YES\"** if Chef should buy, otherwise print **\"NO\"**.",
|
||||
"inputFormat": "- The first and only line of input contains a single integer $R$ \u2014 the current market risk level.",
|
||||
"inputFormatState": true,
|
||||
"outputFormat": "Print `YES` if Chef should buy Bitcoin, Otherwise, print `NO`.\n\nYou may print each character of the string in uppercase or lowercase (for example, the strings `YES`, `yEs`, `yes`, and `yeS` will all be treated as identical).\n",
|
||||
"outputFormatState": true,
|
||||
"sampleTestCases": [
|
||||
{
|
||||
"id": "1",
|
||||
"input": "2",
|
||||
"output": "YES",
|
||||
"explanation": "The current market risk is $2$. \nSince $2$ is not larger than $4$, the risk is small enough, and Chef will buy Bitcoin.",
|
||||
"isDeleted": false
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"input": "4",
|
||||
"output": "YES",
|
||||
"explanation": "The current market risk is $4$. \nSince $4$ is not larger than $4$, the risk is small enough, and Chef will buy Bitcoin.",
|
||||
"isDeleted": false
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"input": "5",
|
||||
"output": "NO",
|
||||
"explanation": "The current market risk is $5$. \nSince $5$ is larger than $4$, the risk is too much, and Chef will **not** buy Bitcoin.",
|
||||
"isDeleted": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"gumlet_video_url": "",
|
||||
"video_editorial_url": "https:\/\/youtu.be\/tjUCV9Ld1Kw?si=minop9943wecj1bh",
|
||||
"text_editorial_body": "<h1><a name=\"problem-link-1\" class=\"anchor\" href=\"#problem-link-1\"><\/a>PROBLEM LINK:<\/h1>\n<p><a href=\"https:\/\/www.codechef.com\/problems\/P1209\">Practice<\/a><br>\n<a href=\"https:\/\/www.codechef.com\/START209A\/problems\/P1209\">Contest: Division 1<\/a><br>\n<a href=\"https:\/\/www.codechef.com\/START209B\/problems\/P1209\">Contest: Division 2<\/a><br>\n<a href=\"https:\/\/www.codechef.com\/START209C\/problems\/P1209\">Contest: Division 3<\/a><br>\n<a href=\"https:\/\/www.codechef.com\/START209D\/problems\/P1209\">Contest: Division 4<\/a><\/p>\n<p><em><strong>Author:<\/strong><\/em> <a href=\"https:\/\/www.codechef.com\/users\/pols_agyi_pols\">pols_agyi_pols<\/a><br>\n<em><strong>Tester:<\/strong><\/em> <a href=\"https:\/\/www.codechef.com\/users\/kingmessi\">kingmessi<\/a><br>\n<em><strong>Editorialist:<\/strong><\/em> <a href=\"https:\/\/www.codechef.com\/users\/iceknight1093\">iceknight1093<\/a><\/p>\n<h1><a name=\"difficulty-2\" class=\"anchor\" href=\"#difficulty-2\"><\/a>DIFFICULTY:<\/h1>\n<p>Cakewalk<\/p>\n<h1><a name=\"prerequisites-3\" class=\"anchor\" href=\"#prerequisites-3\"><\/a>PREREQUISITES:<\/h1>\n<p>None<\/p>\n<h1><a name=\"problem-4\" class=\"anchor\" href=\"#problem-4\"><\/a>PROBLEM:<\/h1>\n<p>Chef will buy bitcoin if the market risk level is no more than <span class=\"math\">4<\/span>.<br>\nThe current market risk level is <span class=\"math\">R<\/span>.<br>\nWill Chef buy bitcoin?<\/p>\n<h1><a name=\"explanation-5\" class=\"anchor\" href=\"#explanation-5\"><\/a>EXPLANATION:<\/h1>\n<p>The answer is <code>Yes<\/code> if <span class=\"math\">R \\le 4<\/span> and <code>No<\/code> otherwise.<br>\nThis can be checked using an <code>if<\/code> condition.<\/p>\n<h1><a name=\"time-complexity-6\" class=\"anchor\" href=\"#time-complexity-6\"><\/a>TIME COMPLEXITY:<\/h1>\n<p><span class=\"math\">\\mathcal{O}(1)<\/span> per testcase.<\/p>\n<h1><a name=\"code-7\" class=\"anchor\" href=\"#code-7\"><\/a>CODE:<\/h1>\n<details>\n<summary>\nEditorialist's code (PyPy3)<\/summary>\n<pre><code class=\"lang-python\">r = int(input())\nprint('Yes' if r <= 4 else 'No')\n<\/code><\/pre>\n<\/details>",
|
||||
"text_editorial_is_markdown": 0,
|
||||
"text_editorial_topic_id": 124410,
|
||||
"languages_supported": "CPP20, PYTH 3, C, JAVA, PYP3, CS2, NODEJS, GO, TS, PHP, kotlin, rust, R",
|
||||
"max_timelimit": "1",
|
||||
"source_sizelimit": "50000",
|
||||
"problem_author": "archit_adm",
|
||||
"problem_display_authors": ["archit_adm"],
|
||||
"problem_display_authors_html_handle": "<div class=\"multiple-usernames-container\"><a href='\/users\/archit_adm'>archit_adm<\/a><\/div>",
|
||||
"problem_tester": null,
|
||||
"problem_testers_usernames": ["kingmessi"],
|
||||
"problem_tester_html_handle": "<div class=\"multiple-usernames-container\"><a href='\/users\/kingmessi'><span \n class='rating' \n style='display: inline-block; \n font-size: 10px; \n background: #D0011B;\n padding: 0 3px; \n line-height: 1.3; \n color: white;\n margin-right: 2px;'>7★<\/span><span class='m-username--link'>kingmessi<\/span><\/a><\/div>",
|
||||
"problem_editorialist": "iceknight1093",
|
||||
"date_added": "20-10-2025",
|
||||
"ready_for_debug": false,
|
||||
"problem_stats": {
|
||||
"accuracy": 85.780000000000001,
|
||||
"successful_submissions": "25325",
|
||||
"total_submissions": "33327"
|
||||
},
|
||||
"user_tags": ["archit_adm", "cakewalk", "start209"],
|
||||
"computed_tags": [],
|
||||
"difficulty_rating": "172",
|
||||
"best_tag": "",
|
||||
"editorial_url": "",
|
||||
"time": {
|
||||
"view_start_date": 1761143406,
|
||||
"submit_start_date": 1761143406,
|
||||
"visible_start_date": 1761150606,
|
||||
"end_date": 1761150606,
|
||||
"current": 1761365589,
|
||||
"practice_submission_allowed": false
|
||||
},
|
||||
"user": { "username": null, "access": "default", "isPremiumUser": false },
|
||||
"bookmark_status": false,
|
||||
"contest_problem_status": "unattempted",
|
||||
"problem_status": "unattempted",
|
||||
"is_direct_submittable": false,
|
||||
"problemDiscussURL": "https:\/\/discuss.codechef.com\/search?q=P1209",
|
||||
"is_a_practice_or_college_contest": false,
|
||||
"votes_data": {
|
||||
"SolutionVoteData": { "upvote_count": 0, "user_vote": 0 },
|
||||
"HintsVoteData": { "upvote_count": 0, "user_vote": 0 },
|
||||
"ProblemStatementVoteData": { "upvote_count": 26, "user_vote": 0 },
|
||||
"DoubtSupportVoteData": { "upvote_count": 0, "user_vote": 0 }
|
||||
},
|
||||
"is_proctored": false,
|
||||
"is_user_verified_for_proctoring": false,
|
||||
"visitedContests": [],
|
||||
"isSupportedByJudge": true
|
||||
}
|
||||
330
tests/fixtures/codechef/contests.json
vendored
Normal file
330
tests/fixtures/codechef/contests.json
vendored
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
{
|
||||
"status": "success",
|
||||
"message": "All contests list",
|
||||
"present_contests": [
|
||||
{
|
||||
"contest_code": "DEVWEEKEND21",
|
||||
"contest_name": "Weekend Dev Challenge 21: Full Stack Projects using MERN",
|
||||
"contest_start_date": "25 Oct 2025 00:00:00",
|
||||
"contest_end_date": "27 Oct 2025 00:00:00",
|
||||
"contest_start_date_iso": "2025-10-25T00:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-10-27T00:00:00+05:30",
|
||||
"contest_duration": "2880",
|
||||
"distinct_users": 8
|
||||
}
|
||||
],
|
||||
"future_contests": [
|
||||
{
|
||||
"contest_code": "START210",
|
||||
"contest_name": "Starters 210",
|
||||
"contest_start_date": "29 Oct 2025 20:00:00",
|
||||
"contest_end_date": "29 Oct 2025 22:00:00",
|
||||
"contest_start_date_iso": "2025-10-29T20:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-10-29T22:00:00+05:30",
|
||||
"contest_duration": "120",
|
||||
"distinct_users": 0
|
||||
},
|
||||
{
|
||||
"contest_code": "START211",
|
||||
"contest_name": "Starters 211",
|
||||
"contest_start_date": "05 Nov 2025 20:00:00",
|
||||
"contest_end_date": "05 Nov 2025 22:00:00",
|
||||
"contest_start_date_iso": "2025-11-05T20:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-11-05T22:00:00+05:30",
|
||||
"contest_duration": "120",
|
||||
"distinct_users": 0
|
||||
}
|
||||
],
|
||||
"practice_contests": [],
|
||||
"past_contests": [
|
||||
{
|
||||
"contest_code": "START209",
|
||||
"contest_name": "Starters 209 (Rated till 5 star)",
|
||||
"contest_start_date": "22 Oct 2025 20:00:00",
|
||||
"contest_end_date": "22 Oct 2025 22:00:00",
|
||||
"contest_start_date_iso": "2025-10-22T20:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-10-22T22:00:00+05:30",
|
||||
"contest_duration": "120",
|
||||
"distinct_users": 30408
|
||||
},
|
||||
{
|
||||
"contest_code": "DSAMONDAY08",
|
||||
"contest_name": "Monday Munch - DSA Challenge 08",
|
||||
"contest_start_date": "20 Oct 2025 18:00:31",
|
||||
"contest_end_date": "20 Oct 2025 21:00:31",
|
||||
"contest_start_date_iso": "2025-10-20T18:00:31+05:30",
|
||||
"contest_end_date_iso": "2025-10-20T21:00:31+05:30",
|
||||
"contest_duration": "180",
|
||||
"distinct_users": 653
|
||||
},
|
||||
{
|
||||
"contest_code": "DEVWEEKEND20",
|
||||
"contest_name": "Weekend Dev Challenge 20: Full Stack Projects using MERN",
|
||||
"contest_start_date": "18 Oct 2025 00:00:00",
|
||||
"contest_end_date": "20 Oct 2025 00:00:00",
|
||||
"contest_start_date_iso": "2025-10-18T00:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-10-20T00:00:00+05:30",
|
||||
"contest_duration": "2880",
|
||||
"distinct_users": 318
|
||||
},
|
||||
{
|
||||
"contest_code": "START208",
|
||||
"contest_name": "Starters 208 (Rated till 6 star)",
|
||||
"contest_start_date": "15 Oct 2025 20:00:00",
|
||||
"contest_end_date": "15 Oct 2025 22:00:00",
|
||||
"contest_start_date_iso": "2025-10-15T20:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-10-15T22:00:00+05:30",
|
||||
"contest_duration": "120",
|
||||
"distinct_users": 37727
|
||||
},
|
||||
{
|
||||
"contest_code": "DSAMONDAY07",
|
||||
"contest_name": "Monday Munch - DSA Challenge 07",
|
||||
"contest_start_date": "13 Oct 2025 18:00:00",
|
||||
"contest_end_date": "13 Oct 2025 21:00:00",
|
||||
"contest_start_date_iso": "2025-10-13T18:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-10-13T21:00:00+05:30",
|
||||
"contest_duration": "180",
|
||||
"distinct_users": 4934
|
||||
},
|
||||
{
|
||||
"contest_code": "DEVWEEKEND19",
|
||||
"contest_name": "Weekend Dev Challenge 19: Full Stack Projects using MERN",
|
||||
"contest_start_date": "11 Oct 2025 00:00:00",
|
||||
"contest_end_date": "13 Oct 2025 00:00:00",
|
||||
"contest_start_date_iso": "2025-10-11T00:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-10-13T00:00:00+05:30",
|
||||
"contest_duration": "2880",
|
||||
"distinct_users": 5376
|
||||
},
|
||||
{
|
||||
"contest_code": "START207",
|
||||
"contest_name": "Starters 207 (Rated till 5 star)",
|
||||
"contest_start_date": "08 Oct 2025 20:00:00",
|
||||
"contest_end_date": "08 Oct 2025 22:00:00",
|
||||
"contest_start_date_iso": "2025-10-08T20:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-10-08T22:00:00+05:30",
|
||||
"contest_duration": "120",
|
||||
"distinct_users": 32785
|
||||
},
|
||||
{
|
||||
"contest_code": "DSAMONDAY06",
|
||||
"contest_name": "Monday Munch - DSA Challenge 06",
|
||||
"contest_start_date": "06 Oct 2025 18:00:02",
|
||||
"contest_end_date": "06 Oct 2025 21:00:02",
|
||||
"contest_start_date_iso": "2025-10-06T18:00:02+05:30",
|
||||
"contest_end_date_iso": "2025-10-06T21:00:02+05:30",
|
||||
"contest_duration": "180",
|
||||
"distinct_users": 892
|
||||
},
|
||||
{
|
||||
"contest_code": "DEVWEEKEND18",
|
||||
"contest_name": "Weekend Dev Challenge 18: Full Stack Projects using MERN",
|
||||
"contest_start_date": "04 Oct 2025 00:00:00",
|
||||
"contest_end_date": "06 Oct 2025 00:00:00",
|
||||
"contest_start_date_iso": "2025-10-04T00:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-10-06T00:00:00+05:30",
|
||||
"contest_duration": "2880",
|
||||
"distinct_users": 223
|
||||
},
|
||||
{
|
||||
"contest_code": "START206",
|
||||
"contest_name": "Starters 206 (Rated till 5 star)",
|
||||
"contest_start_date": "01 Oct 2025 20:00:00",
|
||||
"contest_end_date": "01 Oct 2025 22:00:00",
|
||||
"contest_start_date_iso": "2025-10-01T20:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-10-01T22:00:00+05:30",
|
||||
"contest_duration": "120",
|
||||
"distinct_users": 23977
|
||||
},
|
||||
{
|
||||
"contest_code": "DSAMONDAY05",
|
||||
"contest_name": "Monday Munch - DSA Challenge 05",
|
||||
"contest_start_date": "29 Sep 2025 18:00:00",
|
||||
"contest_end_date": "29 Sep 2025 21:00:00",
|
||||
"contest_start_date_iso": "2025-09-29T18:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-09-29T21:00:00+05:30",
|
||||
"contest_duration": "180",
|
||||
"distinct_users": 1160
|
||||
},
|
||||
{
|
||||
"contest_code": "DEVWEEKEND17",
|
||||
"contest_name": "Weekend Dev Challenge 17: GenAI Projects using LLM",
|
||||
"contest_start_date": "27 Sep 2025 00:00:00",
|
||||
"contest_end_date": "29 Sep 2025 00:00:00",
|
||||
"contest_start_date_iso": "2025-09-27T00:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-09-29T00:00:00+05:30",
|
||||
"contest_duration": "2880",
|
||||
"distinct_users": 130
|
||||
},
|
||||
{
|
||||
"contest_code": "START205",
|
||||
"contest_name": "Starters 205 (Rated till 6 star)",
|
||||
"contest_start_date": "24 Sep 2025 20:00:00",
|
||||
"contest_end_date": "24 Sep 2025 22:00:00",
|
||||
"contest_start_date_iso": "2025-09-24T20:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-09-24T22:00:00+05:30",
|
||||
"contest_duration": "120",
|
||||
"distinct_users": 32552
|
||||
},
|
||||
{
|
||||
"contest_code": "DSAMONDAY04",
|
||||
"contest_name": "Monday Munch - DSA Challenge 04",
|
||||
"contest_start_date": "22 Sep 2025 18:00:00",
|
||||
"contest_end_date": "22 Sep 2025 21:00:00",
|
||||
"contest_start_date_iso": "2025-09-22T18:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-09-22T21:00:00+05:30",
|
||||
"contest_duration": "180",
|
||||
"distinct_users": 759
|
||||
},
|
||||
{
|
||||
"contest_code": "DEVWEEKEND16",
|
||||
"contest_name": "Weekend Dev Challenge 16: GenAI Projects using LLM",
|
||||
"contest_start_date": "20 Sep 2025 00:00:00",
|
||||
"contest_end_date": "22 Sep 2025 00:00:00",
|
||||
"contest_start_date_iso": "2025-09-20T00:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-09-22T00:00:00+05:30",
|
||||
"contest_duration": "2880",
|
||||
"distinct_users": 171
|
||||
},
|
||||
{
|
||||
"contest_code": "START204",
|
||||
"contest_name": "Starters 204 (Rated till 5 star)",
|
||||
"contest_start_date": "17 Sep 2025 20:00:00",
|
||||
"contest_end_date": "17 Sep 2025 22:00:00",
|
||||
"contest_start_date_iso": "2025-09-17T20:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-09-17T22:00:00+05:30",
|
||||
"contest_duration": "120",
|
||||
"distinct_users": 36282
|
||||
},
|
||||
{
|
||||
"contest_code": "DSAMONDAY03",
|
||||
"contest_name": "Monday Munch - DSA Challenge 03",
|
||||
"contest_start_date": "15 Sep 2025 18:00:00",
|
||||
"contest_end_date": "15 Sep 2025 21:00:00",
|
||||
"contest_start_date_iso": "2025-09-15T18:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-09-15T21:00:00+05:30",
|
||||
"contest_duration": "180",
|
||||
"distinct_users": 657
|
||||
},
|
||||
{
|
||||
"contest_code": "DEVWEEKEND15",
|
||||
"contest_name": "Weekend Dev Challenge 15: Classify images using Deep Learning",
|
||||
"contest_start_date": "13 Sep 2025 00:00:00",
|
||||
"contest_end_date": "14 Sep 2025 00:00:00",
|
||||
"contest_start_date_iso": "2025-09-13T00:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-09-14T00:00:00+05:30",
|
||||
"contest_duration": "1440",
|
||||
"distinct_users": 112
|
||||
},
|
||||
{
|
||||
"contest_code": "START203",
|
||||
"contest_name": "Starters 203 (Rated till 5 star)",
|
||||
"contest_start_date": "10 Sep 2025 20:00:00",
|
||||
"contest_end_date": "10 Sep 2025 22:00:00",
|
||||
"contest_start_date_iso": "2025-09-10T20:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-09-10T22:00:00+05:30",
|
||||
"contest_duration": "120",
|
||||
"distinct_users": 36512
|
||||
},
|
||||
{
|
||||
"contest_code": "DSAMONDAY02",
|
||||
"contest_name": "Monday Munch - DSA Challenge 02",
|
||||
"contest_start_date": "08 Sep 2025 18:00:00",
|
||||
"contest_end_date": "08 Sep 2025 21:00:00",
|
||||
"contest_start_date_iso": "2025-09-08T18:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-09-08T21:00:00+05:30",
|
||||
"contest_duration": "180",
|
||||
"distinct_users": 737
|
||||
}
|
||||
],
|
||||
"skill_tests": [
|
||||
{
|
||||
"contest_code": "basic-python",
|
||||
"contest_name": "Python Online Test & Quiz",
|
||||
"contest_start_date": "27 Mar 2024 15:00:00",
|
||||
"contest_end_date": "01 Jan 2027 01:30:00",
|
||||
"contest_start_date_iso": "2024-03-27T15:00:00+05:30",
|
||||
"contest_end_date_iso": "2027-01-01T01:30:00+05:30",
|
||||
"contest_duration": "90",
|
||||
"problem_count": 30,
|
||||
"distinct_users": 61244
|
||||
},
|
||||
{
|
||||
"contest_code": "basic-java",
|
||||
"contest_name": "Java Online Test & Quiz",
|
||||
"contest_start_date": "28 Mar 2024 00:00:00",
|
||||
"contest_end_date": "01 Jan 2027 01:30:00",
|
||||
"contest_start_date_iso": "2024-03-28T00:00:00+05:30",
|
||||
"contest_end_date_iso": "2027-01-01T01:30:00+05:30",
|
||||
"contest_duration": "90",
|
||||
"problem_count": 30,
|
||||
"distinct_users": 49993
|
||||
},
|
||||
{
|
||||
"contest_code": "basic-c-language",
|
||||
"contest_name": "C language online test",
|
||||
"contest_start_date": "28 Mar 2024 00:00:00",
|
||||
"contest_end_date": "01 Jan 2027 01:30:00",
|
||||
"contest_start_date_iso": "2024-03-28T00:00:00+05:30",
|
||||
"contest_end_date_iso": "2027-01-01T01:30:00+05:30",
|
||||
"contest_duration": "90",
|
||||
"problem_count": 30,
|
||||
"distinct_users": 41373
|
||||
},
|
||||
{
|
||||
"contest_code": "basic-c-plus-plus",
|
||||
"contest_name": "C++ Online Test and Quiz",
|
||||
"contest_start_date": "28 Mar 2024 00:00:00",
|
||||
"contest_end_date": "01 Jan 2027 01:30:00",
|
||||
"contest_start_date_iso": "2024-03-28T00:00:00+05:30",
|
||||
"contest_end_date_iso": "2027-01-01T01:30:00+05:30",
|
||||
"contest_duration": "90",
|
||||
"problem_count": 30,
|
||||
"distinct_users": 32507
|
||||
},
|
||||
{
|
||||
"contest_code": "basic-sql",
|
||||
"contest_name": "SQL Online Test and Quiz",
|
||||
"contest_start_date": "01 Jun 2024 00:00:00",
|
||||
"contest_end_date": "01 Jan 2027 01:00:00",
|
||||
"contest_start_date_iso": "2024-06-01T00:00:00+05:30",
|
||||
"contest_end_date_iso": "2027-01-01T01:00:00+05:30",
|
||||
"contest_duration": "60",
|
||||
"problem_count": 17,
|
||||
"distinct_users": 17426
|
||||
},
|
||||
{
|
||||
"contest_code": "operating-systems",
|
||||
"contest_name": "Operating Systems Skill Test",
|
||||
"contest_start_date": "01 Jun 2024 00:00:00",
|
||||
"contest_end_date": "01 Jan 2027 00:45:00",
|
||||
"contest_start_date_iso": "2024-06-01T00:00:00+05:30",
|
||||
"contest_end_date_iso": "2027-01-01T00:45:00+05:30",
|
||||
"contest_duration": "45",
|
||||
"problem_count": 30,
|
||||
"distinct_users": 8751
|
||||
},
|
||||
{
|
||||
"contest_code": "c-language-dsa",
|
||||
"contest_name": "Data structures and Algorithms in C test",
|
||||
"contest_start_date": "01 Apr 2024 12:00:00",
|
||||
"contest_end_date": "01 Jan 2027 02:00:00",
|
||||
"contest_start_date_iso": "2024-04-01T12:00:00+05:30",
|
||||
"contest_end_date_iso": "2027-01-01T02:00:00+05:30",
|
||||
"contest_duration": "120",
|
||||
"problem_count": 28,
|
||||
"distinct_users": 6611
|
||||
}
|
||||
],
|
||||
"banners": [
|
||||
{
|
||||
"image": "1760933050.png",
|
||||
"link": "https:\/\/www.codechef.com\/START209"
|
||||
},
|
||||
{
|
||||
"image": "1719492535.png",
|
||||
"link": "https:\/\/www.codechef.com\/roadmap\/data-structures-and-algorithms"
|
||||
}
|
||||
]
|
||||
}
|
||||
8210
tests/fixtures/codeforces/1550_A.html
vendored
Normal file
8210
tests/fixtures/codeforces/1550_A.html
vendored
Normal file
File diff suppressed because it is too large
Load diff
4724
tests/fixtures/codeforces/1550_B.html
vendored
Normal file
4724
tests/fixtures/codeforces/1550_B.html
vendored
Normal file
File diff suppressed because it is too large
Load diff
10743
tests/fixtures/codeforces/1550_problems.html
vendored
Normal file
10743
tests/fixtures/codeforces/1550_problems.html
vendored
Normal file
File diff suppressed because it is too large
Load diff
10
tests/fixtures/codeforces/contests.html
vendored
Normal file
10
tests/fixtures/codeforces/contests.html
vendored
Normal file
File diff suppressed because one or more lines are too long
2141
tests/fixtures/cses/contests.html
vendored
Normal file
2141
tests/fixtures/cses/contests.html
vendored
Normal file
File diff suppressed because it is too large
Load diff
156
tests/fixtures/cses/task_1068.html
vendored
Normal file
156
tests/fixtures/cses/task_1068.html
vendored
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<link rel="stylesheet " type="text/css" href="/cses.css?0" id="styles" />
|
||||
<link
|
||||
rel="stylesheet alternate"
|
||||
type="text/css"
|
||||
href="/cses-dark.css?0"
|
||||
id="styles-dark"
|
||||
/>
|
||||
<meta name="theme-color" content="white" id="theme-color" />
|
||||
<script type="application/json" id="darkmode-enabled">
|
||||
false
|
||||
</script>
|
||||
<script src="/ui.js"></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="/lib/fontawesome/css/all.min.css"
|
||||
/>
|
||||
</head>
|
||||
<body class="with-sidebar">
|
||||
<div class="header">
|
||||
<div>
|
||||
<a href="/" class="logo"><img src="/logo.png?1" alt="CSES" /></a>
|
||||
<a
|
||||
class="menu-toggle"
|
||||
onclick="document.body.classList.toggle('menu-open')"
|
||||
>
|
||||
<i class="fas fa-bars"></i>
|
||||
</a>
|
||||
<div class="controls">
|
||||
<a class="account" href="/login">Login</a>
|
||||
<span>—</span>
|
||||
<a
|
||||
href="/darkmode"
|
||||
title="Toggle dark mode"
|
||||
onclick="return toggle_theme()"
|
||||
><i aria-label="Dark mode" class="fas fa-adjust"></i
|
||||
><span>Dark mode</span></a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skeleton">
|
||||
<div class="navigation">
|
||||
<div class="title-block">
|
||||
<h3><a href="/problemset/list/">CSES Problem Set</a></h3>
|
||||
<h1>Weird Algorithm</h1>
|
||||
<ul class="nav">
|
||||
<li><a href="/problemset/task/1068/" class="current">Task</a></li>
|
||||
<li><a href="/problemset/stats/1068/">Statistics</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="content">
|
||||
<title>CSES - Weird Algorithm</title
|
||||
><link rel="stylesheet" href="/lib/katex/katex.min.css" />
|
||||
<script defer src="/lib/katex/katex.min.js"></script>
|
||||
<script defer src="/lib/katex/contrib/copy-tex.min.js"></script>
|
||||
<script
|
||||
defer
|
||||
src="/lib/google-code-prettify/run_prettify.js"
|
||||
></script>
|
||||
<script>
|
||||
addEventListener('DOMContentLoaded', function (e) {
|
||||
const mathElements = document.getElementsByClassName('math')
|
||||
const macros = {}
|
||||
for (let element of mathElements) {
|
||||
katex.render(element.textContent, element, {
|
||||
displayMode: element.classList.contains('math-display'),
|
||||
throwOnError: false,
|
||||
globalGroup: true,
|
||||
macros
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<ul class="task-constraints">
|
||||
<li><b>Time limit:</b> 1.00 s</li>
|
||||
<li><b>Memory limit:</b> 512 MB</li>
|
||||
</ul>
|
||||
<div class="md">
|
||||
<p>
|
||||
Consider an algorithm that takes as input a positive integer
|
||||
<span class="math math-inline">n</span>. If
|
||||
<span class="math math-inline">n</span> is even, the algorithm
|
||||
divides it by two, and if
|
||||
<span class="math math-inline">n</span> is odd, the algorithm
|
||||
multiplies it by three and adds one. The algorithm repeats this,
|
||||
until <span class="math math-inline">n</span> is one. For example,
|
||||
the sequence for <span class="math math-inline">n=3</span> is as
|
||||
follows:
|
||||
<span class="math math-display">
|
||||
3 \rightarrow 10 \rightarrow 5 \rightarrow 16 \rightarrow 8
|
||||
\rightarrow 4 \rightarrow 2 \rightarrow 1</span
|
||||
>
|
||||
Your task is to simulate the execution of the algorithm for a
|
||||
given value of <span class="math math-inline">n</span>.
|
||||
</p>
|
||||
<h1 id="input">Input</h1>
|
||||
<p>
|
||||
The only input line contains an integer
|
||||
<span class="math math-inline">n</span>.
|
||||
</p>
|
||||
<h1 id="output">Output</h1>
|
||||
<p>
|
||||
Print a line that contains all values of
|
||||
<span class="math math-inline">n</span> during the algorithm.
|
||||
</p>
|
||||
<h1 id="constraints">Constraints</h1>
|
||||
<ul>
|
||||
<li><span class="math math-inline">1 \le n \le 10^6</span></li>
|
||||
</ul>
|
||||
<h1 id="example">Example</h1>
|
||||
<p>Input:</p>
|
||||
<pre>
|
||||
3
|
||||
</pre
|
||||
>
|
||||
<p>Output:</p>
|
||||
<pre>
|
||||
3 10 5 16 8 4 2 1
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav sidebar">
|
||||
<h4>Introductory Problems</h4>
|
||||
<a class="current" href="/problemset/task/1068"
|
||||
>Weird Algorithm<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1083"
|
||||
>Missing Number<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1069"
|
||||
>Repetitions<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1094"
|
||||
>Increasing Array<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1070"
|
||||
>Permutations<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1071"
|
||||
>Number Spiral<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1072"
|
||||
>Two Knights<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1092"
|
||||
>Two Sets<span class="task-score icon"></span></a
|
||||
>...
|
||||
<hr />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
150
tests/fixtures/cses/task_1621.html
vendored
Normal file
150
tests/fixtures/cses/task_1621.html
vendored
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<link rel="stylesheet " type="text/css" href="/cses.css?0" id="styles" />
|
||||
<link
|
||||
rel="stylesheet alternate"
|
||||
type="text/css"
|
||||
href="/cses-dark.css?0"
|
||||
id="styles-dark"
|
||||
/>
|
||||
<meta name="theme-color" content="white" id="theme-color" />
|
||||
<script type="application/json" id="darkmode-enabled">
|
||||
false
|
||||
</script>
|
||||
<script src="/ui.js"></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="/lib/fontawesome/css/all.min.css"
|
||||
/>
|
||||
</head>
|
||||
<body class="with-sidebar">
|
||||
<div class="header">
|
||||
<div>
|
||||
<a href="/" class="logo"><img src="/logo.png?1" alt="CSES" /></a>
|
||||
<a
|
||||
class="menu-toggle"
|
||||
onclick="document.body.classList.toggle('menu-open')"
|
||||
>
|
||||
<i class="fas fa-bars"></i>
|
||||
</a>
|
||||
<div class="controls">
|
||||
<a class="account" href="/login">Login</a>
|
||||
<span>—</span>
|
||||
<a
|
||||
href="/darkmode"
|
||||
title="Toggle dark mode"
|
||||
onclick="return toggle_theme()"
|
||||
><i aria-label="Dark mode" class="fas fa-adjust"></i
|
||||
><span>Dark mode</span></a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skeleton">
|
||||
<div class="navigation">
|
||||
<div class="title-block">
|
||||
<h3><a href="/problemset/list/">CSES Problem Set</a></h3>
|
||||
<h1>Distinct Numbers</h1>
|
||||
<ul class="nav">
|
||||
<li><a href="/problemset/task/1621/" class="current">Task</a></li>
|
||||
<li><a href="/problemset/stats/1621/">Statistics</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="content">
|
||||
<title>CSES - Distinct Numbers</title
|
||||
><link rel="stylesheet" href="/lib/katex/katex.min.css" />
|
||||
<script defer src="/lib/katex/katex.min.js"></script>
|
||||
<script defer src="/lib/katex/contrib/copy-tex.min.js"></script>
|
||||
<script
|
||||
defer
|
||||
src="/lib/google-code-prettify/run_prettify.js"
|
||||
></script>
|
||||
<script>
|
||||
addEventListener('DOMContentLoaded', function (e) {
|
||||
const mathElements = document.getElementsByClassName('math')
|
||||
const macros = {}
|
||||
for (let element of mathElements) {
|
||||
katex.render(element.textContent, element, {
|
||||
displayMode: element.classList.contains('math-display'),
|
||||
throwOnError: false,
|
||||
globalGroup: true,
|
||||
macros
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<ul class="task-constraints">
|
||||
<li><b>Time limit:</b> 1.00 s</li>
|
||||
<li><b>Memory limit:</b> 512 MB</li>
|
||||
</ul>
|
||||
<div class="md">
|
||||
<p>
|
||||
You are given a list of
|
||||
<span class="math math-inline">n</span> integers, and your task is
|
||||
to calculate the number of <em>distinct</em> values in the list.
|
||||
</p>
|
||||
<h1 id="input">Input</h1>
|
||||
<p>
|
||||
The first input line has an integer
|
||||
<span class="math math-inline">n</span>: the number of values.
|
||||
</p>
|
||||
<p>
|
||||
The second line has
|
||||
<span class="math math-inline">n</span> integers
|
||||
<span class="math math-inline">x_1,x_2,\dots,x_n</span>.
|
||||
</p>
|
||||
<h1 id="output">Output</h1>
|
||||
<p>Print one integers: the number of distinct values.</p>
|
||||
<h1 id="constraints">Constraints</h1>
|
||||
<ul>
|
||||
<li>
|
||||
<span class="math math-inline">1 \le n \le 2 \cdot 10^5</span>
|
||||
</li>
|
||||
<li><span class="math math-inline">1 \le x_i \le 10^9</span></li>
|
||||
</ul>
|
||||
<h1 id="example">Example</h1>
|
||||
<p>Input:</p>
|
||||
<pre>
|
||||
5
|
||||
2 3 2 2 3
|
||||
</pre
|
||||
>
|
||||
<p>Output:</p>
|
||||
<pre>
|
||||
2
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav sidebar">
|
||||
<h4>Sorting and Searching</h4>
|
||||
<a class="current" href="/problemset/task/1621"
|
||||
>Distinct Numbers<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1084"
|
||||
>Apartments<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1090"
|
||||
>Ferris Wheel<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1091"
|
||||
>Concert Tickets<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1619"
|
||||
>Restaurant Customers<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1629"
|
||||
>Movie Festival<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1640"
|
||||
>Sum of Two Values<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1643"
|
||||
>Maximum Subarray Sum<span class="task-score icon"></span></a
|
||||
>...
|
||||
<hr />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
89
tests/test_scrapers.py
Normal file
89
tests/test_scrapers.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import pytest
|
||||
|
||||
from scrapers.models import (
|
||||
ContestListResult,
|
||||
MetadataResult,
|
||||
TestsResult,
|
||||
)
|
||||
|
||||
MATRIX = {
|
||||
"cses": {
|
||||
"metadata": ("introductory_problems",),
|
||||
"tests": ("introductory_problems",),
|
||||
"contests": tuple(),
|
||||
},
|
||||
"atcoder": {
|
||||
"metadata": ("abc100",),
|
||||
"tests": ("abc100",),
|
||||
"contests": tuple(),
|
||||
},
|
||||
"codeforces": {
|
||||
"metadata": ("1550",),
|
||||
"tests": ("1550",),
|
||||
"contests": tuple(),
|
||||
},
|
||||
"codechef": {
|
||||
"metadata": ("START209D",),
|
||||
"tests": ("START209D",),
|
||||
"contests": tuple(),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scraper", MATRIX.keys())
|
||||
@pytest.mark.parametrize("mode", ["metadata", "tests", "contests"])
|
||||
def test_scraper_offline_fixture_matrix(run_scraper_offline, scraper, mode):
|
||||
args = MATRIX[scraper][mode]
|
||||
rc, objs = run_scraper_offline(scraper, mode, *args)
|
||||
assert rc in (0, 1), f"Bad exit code {rc}"
|
||||
assert objs, f"No JSON output for {scraper}:{mode}"
|
||||
|
||||
if mode == "metadata":
|
||||
model = MetadataResult.model_validate(objs[-1])
|
||||
assert model.success is True
|
||||
assert model.url
|
||||
assert len(model.problems) >= 1
|
||||
assert all(isinstance(p.id, str) and p.id for p in model.problems)
|
||||
elif mode == "contests":
|
||||
model = ContestListResult.model_validate(objs[-1])
|
||||
assert model.success is True
|
||||
assert len(model.contests) >= 1
|
||||
else:
|
||||
assert len(objs) >= 1, "No test objects returned"
|
||||
validated_any = False
|
||||
for obj in objs:
|
||||
if "success" in obj and "tests" in obj and "problem_id" in obj:
|
||||
tr = TestsResult.model_validate(obj)
|
||||
assert tr.problem_id != ""
|
||||
assert isinstance(tr.tests, list)
|
||||
assert hasattr(tr, "combined"), "Missing combined field"
|
||||
assert tr.combined is not None, "combined field is None"
|
||||
assert hasattr(tr.combined, "input"), "combined missing input"
|
||||
assert hasattr(tr.combined, "expected"), "combined missing expected"
|
||||
assert isinstance(tr.combined.input, str), "combined.input not string"
|
||||
assert isinstance(tr.combined.expected, str), (
|
||||
"combined.expected not string"
|
||||
)
|
||||
assert hasattr(tr, "multi_test"), "Missing multi_test field"
|
||||
assert isinstance(tr.multi_test, bool), "multi_test not boolean"
|
||||
validated_any = True
|
||||
else:
|
||||
assert "problem_id" in obj
|
||||
assert "tests" in obj and isinstance(obj["tests"], list)
|
||||
assert (
|
||||
"timeout_ms" in obj and "memory_mb" in obj and "interactive" in obj
|
||||
)
|
||||
assert "combined" in obj, "Missing combined field in raw JSON"
|
||||
assert isinstance(obj["combined"], dict), "combined not a dict"
|
||||
assert "input" in obj["combined"], "combined missing input key"
|
||||
assert "expected" in obj["combined"], "combined missing expected key"
|
||||
assert isinstance(obj["combined"]["input"], str), (
|
||||
"combined.input not string"
|
||||
)
|
||||
assert isinstance(obj["combined"]["expected"], str), (
|
||||
"combined.expected not string"
|
||||
)
|
||||
assert "multi_test" in obj, "Missing multi_test field in raw JSON"
|
||||
assert isinstance(obj["multi_test"], bool), "multi_test not boolean"
|
||||
validated_any = True
|
||||
assert validated_any, "No valid tests payloads validated"
|
||||
820
uv.lock
generated
820
uv.lock
generated
|
|
@ -2,111 +2,623 @@ version = 1
|
|||
revision = 3
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backoff"
|
||||
version = "2.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "basedpyright"
|
||||
version = "1.35.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nodejs-wheel-binaries" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/97/45805d2432221d3b86fc9220ecc7f5a72acf9a688b5a80fb0f81ae146133/basedpyright-1.35.0.tar.gz", hash = "sha256:2a7e0bd476623d48499e2b18ff6ed19dc28c51909cf9e1152ad355b5809049ad", size = 22814712, upload-time = "2025-12-03T14:17:13.293Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/91/b0/5d33b280b787bd972895e7c42f08dfd8cd960e680f386e3a950ccce411ad/basedpyright-1.35.0-py3-none-any.whl", hash = "sha256:4f4f84023df5a0cd4ee154916ba698596682ac98bacfa22c941ed6aaf07bba4e", size = 11872872, upload-time = "2025-12-03T14:17:09.749Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "beautifulsoup4"
|
||||
version = "4.13.5"
|
||||
version = "4.14.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "soupsieve" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/85/2e/3e5079847e653b1f6dc647aa24549d68c6addb4c595cc0d902d1b19308ad/beautifulsoup4-4.13.5.tar.gz", hash = "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695", size = 622954, upload-time = "2025-08-24T14:06:13.168Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/eb/f4151e0c7377a6e08a38108609ba5cede57986802757848688aeedd1b9e8/beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a", size = 105113, upload-time = "2025-08-24T14:06:14.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.8.3"
|
||||
version = "2025.11.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfgv"
|
||||
version = "3.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.3"
|
||||
version = "3.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cloudscraper"
|
||||
version = "1.2.71"
|
||||
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 = "curl-cffi"
|
||||
version = "0.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyparsing" },
|
||||
{ name = "requests" },
|
||||
{ name = "requests-toolbelt" },
|
||||
{ name = "certifi" },
|
||||
{ name = "cffi" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/25/6d0481860583f44953bd791de0b7c4f6d7ead7223f8a17e776247b34a5b4/cloudscraper-1.2.71.tar.gz", hash = "sha256:429c6e8aa6916d5bad5c8a5eac50f3ea53c9ac22616f6cb21b18dcc71517d0d3", size = 93261, upload-time = "2023-04-25T23:20:19.467Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4e/3d/f39ca1f8fdf14408888e7c25e15eed63eac5f47926e206fb93300d28378c/curl_cffi-0.13.0.tar.gz", hash = "sha256:62ecd90a382bd5023750e3606e0aa7cb1a3a8ba41c14270b8e5e149ebf72c5ca", size = 151303, upload-time = "2025-08-06T13:05:42.988Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/81/97/fc88803a451029688dffd7eb446dc1b529657577aec13aceff1cc9628c5d/cloudscraper-1.2.71-py2.py3-none-any.whl", hash = "sha256:76f50ca529ed2279e220837befdec892626f9511708e200d48d5bb76ded679b0", size = 99652, upload-time = "2023-04-25T23:20:15.974Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/d1/acabfd460f1de26cad882e5ef344d9adde1507034528cb6f5698a2e6a2f1/curl_cffi-0.13.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:434cadbe8df2f08b2fc2c16dff2779fb40b984af99c06aa700af898e185bb9db", size = 5686337, upload-time = "2025-08-06T13:05:28.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/1c/cdb4fb2d16a0e9de068e0e5bc02094e105ce58a687ff30b4c6f88e25a057/curl_cffi-0.13.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:59afa877a9ae09efa04646a7d068eeea48915a95d9add0a29854e7781679fcd7", size = 2994613, upload-time = "2025-08-06T13:05:31.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/3e/fdf617c1ec18c3038b77065d484d7517bb30f8fb8847224eb1f601a4e8bc/curl_cffi-0.13.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d06ed389e45a7ca97b17c275dbedd3d6524560270e675c720e93a2018a766076", size = 7931353, upload-time = "2025-08-06T13:05:32.273Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/10/6f30c05d251cf03ddc2b9fd19880f3cab8c193255e733444a2df03b18944/curl_cffi-0.13.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4e0de45ab3b7a835c72bd53640c2347415111b43421b5c7a1a0b18deae2e541", size = 7486378, upload-time = "2025-08-06T13:05:33.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/81/5bdb7dd0d669a817397b2e92193559bf66c3807f5848a48ad10cf02bf6c7/curl_cffi-0.13.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8eb4083371bbb94e9470d782de235fb5268bf43520de020c9e5e6be8f395443f", size = 8328585, upload-time = "2025-08-06T13:05:35.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/c1/df5c6b4cfad41c08442e0f727e449f4fb5a05f8aa564d1acac29062e9e8e/curl_cffi-0.13.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:28911b526e8cd4aa0e5e38401bfe6887e8093907272f1f67ca22e6beb2933a51", size = 8739831, upload-time = "2025-08-06T13:05:37.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/91/6dd1910a212f2e8eafe57877bcf97748eb24849e1511a266687546066b8a/curl_cffi-0.13.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6d433ffcb455ab01dd0d7bde47109083aa38b59863aa183d29c668ae4c96bf8e", size = 8711908, upload-time = "2025-08-06T13:05:38.741Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/e4/15a253f9b4bf8d008c31e176c162d2704a7e0c5e24d35942f759df107b68/curl_cffi-0.13.0-cp39-abi3-win_amd64.whl", hash = "sha256:66a6b75ce971de9af64f1b6812e275f60b88880577bac47ef1fa19694fa21cd3", size = 1614510, upload-time = "2025-08-06T13:05:40.451Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/0f/9c5275f17ad6ff5be70edb8e0120fdc184a658c9577ca426d4230f654beb/curl_cffi-0.13.0-cp39-abi3-win_arm64.whl", hash = "sha256:d438a3b45244e874794bc4081dc1e356d2bb926dcc7021e5a8fef2e2105ef1d8", size = 1365753, upload-time = "2025-08-06T13:05:41.879Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distlib"
|
||||
version = "0.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.6.15"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
version = "3.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.2.3"
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndjson"
|
||||
version = "0.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b4/d5/209b6ca94566f9c94c0ec41cee1681c0a3b92a306a84a9b0fcd662088dc3/ndjson-0.3.1.tar.gz", hash = "sha256:bf9746cb6bb1cb53d172cda7f154c07c786d665ff28341e4e689b796b229e5d6", size = 6448, upload-time = "2020-02-25T05:01:07.873Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/70/c9/04ba0056011ba96a58163ebfd666d8385300bd12da1afe661a5a147758d7/ndjson-0.3.1-py2.py3-none-any.whl", hash = "sha256:839c22275e6baa3040077b83c005ac24199b94973309a8a1809be962c753a410", size = 5305, upload-time = "2020-02-25T05:01:06.39Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodeenv"
|
||||
version = "1.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodejs-wheel-binaries"
|
||||
version = "24.11.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/89/da307731fdbb05a5f640b26de5b8ac0dc463fef059162accfc89e32f73bc/nodejs_wheel_binaries-24.11.1.tar.gz", hash = "sha256:413dfffeadfb91edb4d8256545dea797c237bba9b3faefea973cde92d96bb922", size = 8059, upload-time = "2025-11-18T18:21:58.207Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/5f/be5a4112e678143d4c15264d918f9a2dc086905c6426eb44515cf391a958/nodejs_wheel_binaries-24.11.1-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:0e14874c3579def458245cdbc3239e37610702b0aa0975c1dc55e2cb80e42102", size = 55114309, upload-time = "2025-11-18T18:21:21.697Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/1c/2e9d6af2ea32b65928c42b3e5baa7a306870711d93c3536cb25fc090a80d/nodejs_wheel_binaries-24.11.1-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:c2741525c9874b69b3e5a6d6c9179a6fe484ea0c3d5e7b7c01121c8e5d78b7e2", size = 55285957, upload-time = "2025-11-18T18:21:27.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/79/35696d7ba41b1bd35ef8682f13d46ba38c826c59e58b86b267458eb53d87/nodejs_wheel_binaries-24.11.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:5ef598101b0fb1c2bf643abb76dfbf6f76f1686198ed17ae46009049ee83c546", size = 59645875, upload-time = "2025-11-18T18:21:33.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/98/2a9694adee0af72bc602a046b0632a0c89e26586090c558b1c9199b187cc/nodejs_wheel_binaries-24.11.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:cde41d5e4705266688a8d8071debf4f8a6fcea264c61292782672ee75a6905f9", size = 60140941, upload-time = "2025-11-18T18:21:37.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/d6/573e5e2cba9d934f5f89d0beab00c3315e2e6604eb4df0fcd1d80c5a07a8/nodejs_wheel_binaries-24.11.1-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:78bc5bb889313b565df8969bb7423849a9c7fc218bf735ff0ce176b56b3e96f0", size = 61644243, upload-time = "2025-11-18T18:21:43.325Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/e6/643234d5e94067df8ce8d7bba10f3804106668f7a1050aeb10fdd226ead4/nodejs_wheel_binaries-24.11.1-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c79a7e43869ccecab1cae8183778249cceb14ca2de67b5650b223385682c6239", size = 62225657, upload-time = "2025-11-18T18:21:47.708Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/1c/2fb05127102a80225cab7a75c0e9edf88a0a1b79f912e1e36c7c1aaa8f4e/nodejs_wheel_binaries-24.11.1-py2.py3-none-win_amd64.whl", hash = "sha256:10197b1c9c04d79403501766f76508b0dac101ab34371ef8a46fcf51773497d0", size = 41322308, upload-time = "2025-11-18T18:21:51.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/b7/bc0cdbc2cc3a66fcac82c79912e135a0110b37b790a14c477f18e18d90cd/nodejs_wheel_binaries-24.11.1-py2.py3-none-win_arm64.whl", hash = "sha256:376b9ea1c4bc1207878975dfeb604f7aa5668c260c6154dcd2af9d42f7734116", size = 39026497, upload-time = "2025-11-18T18:21:54.634Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "4.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cfgv" },
|
||||
{ name = "identify" },
|
||||
{ name = "nodeenv" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "virtualenv" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f4/9b/6a4ffb4ed980519da959e1cf3122fc6cb41211daa58dbae1c73c0e519a37/pre_commit-4.5.0.tar.gz", hash = "sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b", size = 198428, upload-time = "2025-11-22T21:02:42.304Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429, upload-time = "2025-11-22T21:02:40.836Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.23"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.12.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.41.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
|
||||
]
|
||||
|
||||
[[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 = "pytest"
|
||||
version = "9.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-mock"
|
||||
version = "3.15.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" },
|
||||
]
|
||||
|
||||
[[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/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
||||
{ 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]]
|
||||
|
|
@ -125,15 +637,29 @@ wheels = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests-toolbelt"
|
||||
version = "1.0.0"
|
||||
name = "ruff"
|
||||
version = "0.14.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ed/d9/f7a0c4b3a2bf2556cd5d99b05372c29980249ef71e8e32669ba77428c82c/ruff-0.14.8.tar.gz", hash = "sha256:774ed0dd87d6ce925e3b8496feb3a00ac564bea52b9feb551ecd17e0a23d1eed", size = 5765385, upload-time = "2025-12-04T15:06:17.669Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/b8/9537b52010134b1d2b72870cc3f92d5fb759394094741b09ceccae183fbe/ruff-0.14.8-py3-none-linux_armv6l.whl", hash = "sha256:ec071e9c82eca417f6111fd39f7043acb53cd3fde9b1f95bbed745962e345afb", size = 13441540, upload-time = "2025-12-04T15:06:14.896Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/00/99031684efb025829713682012b6dd37279b1f695ed1b01725f85fd94b38/ruff-0.14.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8cdb162a7159f4ca36ce980a18c43d8f036966e7f73f866ac8f493b75e0c27e9", size = 13669384, upload-time = "2025-12-04T15:06:51.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/64/3eb5949169fc19c50c04f28ece2c189d3b6edd57e5b533649dae6ca484fe/ruff-0.14.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e2fcbefe91f9fad0916850edf0854530c15bd1926b6b779de47e9ab619ea38f", size = 12806917, upload-time = "2025-12-04T15:06:08.925Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/08/5250babb0b1b11910f470370ec0cbc67470231f7cdc033cee57d4976f941/ruff-0.14.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9d70721066a296f45786ec31916dc287b44040f553da21564de0ab4d45a869b", size = 13256112, upload-time = "2025-12-04T15:06:23.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/4c/6c588e97a8e8c2d4b522c31a579e1df2b4d003eddfbe23d1f262b1a431ff/ruff-0.14.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c87e09b3cd9d126fc67a9ecd3b5b1d3ded2b9c7fce3f16e315346b9d05cfb52", size = 13227559, upload-time = "2025-12-04T15:06:33.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/ce/5f78cea13eda8eceac71b5f6fa6e9223df9b87bb2c1891c166d1f0dce9f1/ruff-0.14.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d62cb310c4fbcb9ee4ac023fe17f984ae1e12b8a4a02e3d21489f9a2a5f730c", size = 13896379, upload-time = "2025-12-04T15:06:02.687Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/79/13de4517c4dadce9218a20035b21212a4c180e009507731f0d3b3f5df85a/ruff-0.14.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1af35c2d62633d4da0521178e8a2641c636d2a7153da0bac1b30cfd4ccd91344", size = 15372786, upload-time = "2025-12-04T15:06:29.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/06/33df72b3bb42be8a1c3815fd4fae83fa2945fc725a25d87ba3e42d1cc108/ruff-0.14.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25add4575ffecc53d60eed3f24b1e934493631b48ebbc6ebaf9d8517924aca4b", size = 14990029, upload-time = "2025-12-04T15:06:36.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/61/0f34927bd90925880394de0e081ce1afab66d7b3525336f5771dcf0cb46c/ruff-0.14.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c943d847b7f02f7db4201a0600ea7d244d8a404fbb639b439e987edcf2baf9a", size = 14407037, upload-time = "2025-12-04T15:06:39.979Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/bc/058fe0aefc0fbf0d19614cb6d1a3e2c048f7dc77ca64957f33b12cfdc5ef/ruff-0.14.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb6e8bf7b4f627548daa1b69283dac5a296bfe9ce856703b03130732e20ddfe2", size = 14102390, upload-time = "2025-12-04T15:06:46.372Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/a4/e4f77b02b804546f4c17e8b37a524c27012dd6ff05855d2243b49a7d3cb9/ruff-0.14.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7aaf2974f378e6b01d1e257c6948207aec6a9b5ba53fab23d0182efb887a0e4a", size = 14230793, upload-time = "2025-12-04T15:06:20.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/52/bb8c02373f79552e8d087cedaffad76b8892033d2876c2498a2582f09dcf/ruff-0.14.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e5758ca513c43ad8a4ef13f0f081f80f08008f410790f3611a21a92421ab045b", size = 13160039, upload-time = "2025-12-04T15:06:49.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/ad/b69d6962e477842e25c0b11622548df746290cc6d76f9e0f4ed7456c2c31/ruff-0.14.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f74f7ba163b6e85a8d81a590363bf71618847e5078d90827749bfda1d88c9cdf", size = 13205158, upload-time = "2025-12-04T15:06:54.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/63/54f23da1315c0b3dfc1bc03fbc34e10378918a20c0b0f086418734e57e74/ruff-0.14.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eed28f6fafcc9591994c42254f5a5c5ca40e69a30721d2ab18bb0bb3baac3ab6", size = 13469550, upload-time = "2025-12-04T15:05:59.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/7d/a4d7b1961e4903bc37fffb7ddcfaa7beb250f67d97cfd1ee1d5cddb1ec90/ruff-0.14.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:21d48fa744c9d1cb8d71eb0a740c4dd02751a5de9db9a730a8ef75ca34cf138e", size = 14211332, upload-time = "2025-12-04T15:06:06.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/93/2a5063341fa17054e5c86582136e9895db773e3c2ffb770dde50a09f35f0/ruff-0.14.8-py3-none-win32.whl", hash = "sha256:15f04cb45c051159baebb0f0037f404f1dc2f15a927418f29730f411a79bc4e7", size = 13151890, upload-time = "2025-12-04T15:06:11.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/1c/65c61a0859c0add13a3e1cbb6024b42de587456a43006ca2d4fd3d1618fe/ruff-0.14.8-py3-none-win_amd64.whl", hash = "sha256:9eeb0b24242b5bbff3011409a739929f497f3fb5fe3b5698aba5e77e8c833097", size = 14537826, upload-time = "2025-12-04T15:06:26.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/63/8b41cea3afd7f58eb64ac9251668ee0073789a3bc9ac6f816c8c6fef986d/ruff-0.14.8-py3-none-win_arm64.whl", hash = "sha256:965a582c93c63fe715fd3e3f8aa37c4b776777203d8e1d8aa3cc0c14424a4b99", size = 13634522, upload-time = "2025-12-04T15:06:43.212Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -141,18 +667,50 @@ name = "scrapers"
|
|||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "backoff" },
|
||||
{ name = "beautifulsoup4" },
|
||||
{ name = "cloudscraper" },
|
||||
{ name = "curl-cffi" },
|
||||
{ name = "httpx" },
|
||||
{ name = "ndjson" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "basedpyright" },
|
||||
{ name = "pre-commit" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-mock" },
|
||||
{ name = "ruff" },
|
||||
{ name = "ty" },
|
||||
{ name = "types-beautifulsoup4" },
|
||||
{ name = "types-requests" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "backoff", specifier = ">=2.2.1" },
|
||||
{ name = "beautifulsoup4", specifier = ">=4.13.5" },
|
||||
{ name = "cloudscraper", specifier = ">=1.2.71" },
|
||||
{ name = "curl-cffi", specifier = ">=0.13.0" },
|
||||
{ name = "httpx", specifier = ">=0.28.1" },
|
||||
{ name = "ndjson", specifier = ">=0.3.1" },
|
||||
{ name = "pydantic", specifier = ">=2.11.10" },
|
||||
{ name = "requests", specifier = ">=2.32.5" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "basedpyright", specifier = ">=1.31.6" },
|
||||
{ name = "pre-commit", specifier = ">=4.3.0" },
|
||||
{ name = "pytest", specifier = ">=8.0.0" },
|
||||
{ name = "pytest-mock", specifier = ">=3.12.0" },
|
||||
{ name = "ruff", specifier = ">=0.14.2" },
|
||||
{ name = "ty", specifier = ">=0.0.1a32" },
|
||||
{ name = "types-beautifulsoup4", specifier = ">=4.12.0.20250516" },
|
||||
{ name = "types-requests", specifier = ">=2.32.4.20250913" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "soupsieve"
|
||||
version = "2.8"
|
||||
|
|
@ -163,19 +721,115 @@ wheels = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.14.1"
|
||||
name = "ty"
|
||||
version = "0.0.1a32"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/26/92/8da015685fb83734a2a83de02080e64d182509de77fa9bcf3eed12eeab4b/ty-0.0.1a32.tar.gz", hash = "sha256:12f62e8a3dd0eaeb9557d74b1c32f0616ae40eae10a4f411e1e2a73225f67ff2", size = 4689151, upload-time = "2025-12-05T21:04:26.885Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/e6/fdc35c9ba047f16afdfedf36fb51c221e0190ccde9f70ee28e77084d6612/ty-0.0.1a32-py3-none-linux_armv6l.whl", hash = "sha256:ffe595eaf616f06f58f951766477830a55c2502d2c9f77dde8f60d9a836e0645", size = 9673128, upload-time = "2025-12-05T21:04:17.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/20/eaff31048e2f309f37478f7d715c8de9f9bab03cba4758da27b9311147af/ty-0.0.1a32-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:07f1dce88ad6028fb14665aefe4e6697012c34bd48edd37d02b7eb6a833dbf62", size = 9434094, upload-time = "2025-12-05T21:04:03.383Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/d4/ea8ed57d11b81c459f23561fd6bfb0f54a8d4120cf72541e3bdf71d46202/ty-0.0.1a32-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8fab7ed12528c77ddd600a9638ca859156a53c20f1e381353fa87a255bd397eb", size = 8980296, upload-time = "2025-12-05T21:04:28.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/02/3ce98bbfbb3916678d717ee69358d38a404ca9a39391dda8874b66dd5ee7/ty-0.0.1a32-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ace395280fc21e25eff0a53cfbd68170f90a4b8ef2f85dfabe1ecbca2ced456b", size = 9263054, upload-time = "2025-12-05T21:04:05.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/be/a639638bcd1664de2d70a87da6c4fe0e3272a60b7fa3f0c108a956a456bd/ty-0.0.1a32-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2bcbeed7f5ed8e3c1c7e525fce541e7b943ac04ee7fe369a926551b5e50ea4a8", size = 9451396, upload-time = "2025-12-05T21:04:01.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/a4/2bcf54e842a3d10dc14b369f28a3bab530c5d7ddba624e910b212bda93ee/ty-0.0.1a32-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60ff2e4493f90f81a260205d87719bb1d3420928a1e4a2a7454af7cbdfed2047", size = 9862726, upload-time = "2025-12-05T21:04:08.806Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/c7/19e6719496e59f2f082f34bcac312698366cf50879fdcc3ef76298bfe6a0/ty-0.0.1a32-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:53cad50a59a0d943b06872e0b10f9f2b564805c2ea93f64c7798852bc1901954", size = 10475051, upload-time = "2025-12-05T21:04:31.059Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/77/bdf0ddb066d2b62f141d058f8a33bb7c8628cdbb8bfa75b20e296b79fb4e/ty-0.0.1a32-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:343d43cdc1d7f649ea2baa64ac2b479da3d679239b94509f1df12f7211561ea9", size = 10232712, upload-time = "2025-12-05T21:04:19.849Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/07/f73260a461762a581a007015c1019d40658828ce41576f8c1db88dee574d/ty-0.0.1a32-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f45483e4a84bcf622413712164ea687ce323a9f7013b9e7977c5d623ed937ca9", size = 10237705, upload-time = "2025-12-05T21:04:35.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/57/dbb92206cf2f798d8c51ea16504e8afb90a139d0ff105c31cec9a1db29f9/ty-0.0.1a32-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d452f30d47002a6bafc36d1b6aee42c321e9ec9f7f43a04a2ee7d48c208b86c", size = 9766469, upload-time = "2025-12-05T21:04:22.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/5e/143d93bd143abcebcbaa98c8aeec78898553d62d0a5a432cd79e0cf5bd6d/ty-0.0.1a32-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:86c4e31737fe954637890cef1f3e1b479ffb20e836cac3b76050bdbe80005010", size = 9238592, upload-time = "2025-12-05T21:04:11.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/b8/225230ae097ed88f3c92ad974dd77f8e4f86f2594d9cd0c729da39769878/ty-0.0.1a32-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:daf15fa03bc39a76a0fbc9c2d81d79d528f584e3fbe08d71981e3f7912db91d6", size = 9502161, upload-time = "2025-12-05T21:04:37.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/13/cc89955c9637f25f3aca2dd7749c6008639ef036f0b9bea3e9d89e892ff9/ty-0.0.1a32-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6128f6bab5c6dab3d08689fed1d529dc34f50f221f89c8e16064ed0c549dad7a", size = 9603058, upload-time = "2025-12-05T21:04:39.532Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/77/1fe2793c8065a02d1f70ca7da1b87db49ca621bcbbdb79a18ad79d5d0ab2/ty-0.0.1a32-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:55aab688be1b46776a5a458a1993cae0da7725932c45393399c479c2fa979337", size = 9879903, upload-time = "2025-12-05T21:04:13.567Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/47/fd58e80a3e42310b4b649340d5d97403fe796146cae8678b3a031a414b8e/ty-0.0.1a32-py3-none-win32.whl", hash = "sha256:f55ec25088a09236ad1578b656a07fa009c3a353f5923486905ba48175d142a6", size = 9077703, upload-time = "2025-12-05T21:04:15.849Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/96/209c417c69317339ea8e9b3277fd98364a0e97dd1ffd3585e143ec7b4e57/ty-0.0.1a32-py3-none-win_amd64.whl", hash = "sha256:ed8d5cbd4e47dfed86aaa27e243008aa4e82b6a5434f3ab95c26d3ee5874d9d7", size = 9922426, upload-time = "2025-12-05T21:04:33.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/1c/350fd851fb91244f8c80cec218009cbee7564d76c14e2f423b47e69a5cbc/ty-0.0.1a32-py3-none-win_arm64.whl", hash = "sha256:dbb25f9b513d34cee8ce419514eaef03313f45c3f7ab4eb6e6d427ea1f6854af", size = 9453761, upload-time = "2025-12-05T21:04:24.502Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-beautifulsoup4"
|
||||
version = "4.12.0.20250516"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "types-html5lib" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6d/d1/32b410f6d65eda94d3dfb0b3d0ca151f12cb1dc4cef731dcf7cbfd8716ff/types_beautifulsoup4-4.12.0.20250516.tar.gz", hash = "sha256:aa19dd73b33b70d6296adf92da8ab8a0c945c507e6fb7d5db553415cc77b417e", size = 16628, upload-time = "2025-05-16T03:09:09.93Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/79/d84de200a80085b32f12c5820d4fd0addcbe7ba6dce8c1c9d8605e833c8e/types_beautifulsoup4-4.12.0.20250516-py3-none-any.whl", hash = "sha256:5923399d4a1ba9cc8f0096fe334cc732e130269541d66261bb42ab039c0376ee", size = 16879, upload-time = "2025-05-16T03:09:09.051Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-html5lib"
|
||||
version = "1.1.11.20251117"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "types-webencodings" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c8/f3/d9a1bbba7b42b5558a3f9fe017d967f5338cf8108d35991d9b15fdea3e0d/types_html5lib-1.1.11.20251117.tar.gz", hash = "sha256:1a6a3ac5394aa12bf547fae5d5eff91dceec46b6d07c4367d9b39a37f42f201a", size = 18100, upload-time = "2025-11-17T03:08:00.78Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/ab/f5606db367c1f57f7400d3cb3bead6665ee2509621439af1b29c35ef6f9e/types_html5lib-1.1.11.20251117-py3-none-any.whl", hash = "sha256:2a3fc935de788a4d2659f4535002a421e05bea5e172b649d33232e99d4272d08", size = 24302, upload-time = "2025-11-17T03:07:59.996Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-requests"
|
||||
version = "2.32.4.20250913"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/36/27/489922f4505975b11de2b5ad07b4fe1dca0bca9be81a703f26c5f3acfce5/types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d", size = 23113, upload-time = "2025-09-13T02:40:02.309Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658, upload-time = "2025-09-13T02:40:01.115Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-webencodings"
|
||||
version = "0.5.0.20251108"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/d6/75e381959a2706644f02f7527d264de3216cf6ed333f98eff95954d78e07/types_webencodings-0.5.0.20251108.tar.gz", hash = "sha256:2378e2ceccced3d41bb5e21387586e7b5305e11519fc6b0659c629f23b2e5de4", size = 7470, upload-time = "2025-11-08T02:56:00.132Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/45/4e/8fcf33e193ce4af03c19d0e08483cf5f0838e883f800909c6bc61cb361be/types_webencodings-0.5.0.20251108-py3-none-any.whl", hash = "sha256:e21f81ff750795faffddaffd70a3d8bfff77d006f22c27e393eb7812586249d8", size = 8715, upload-time = "2025-11-08T02:55:59.456Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.5.0"
|
||||
version = "2.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/43/554c2569b62f49350597348fc3ac70f786e3c32e7f19d266e19817812dd3/urllib3-2.6.0.tar.gz", hash = "sha256:cb9bcef5a4b345d5da5d145dc3e30834f58e8018828cbc724d30b4cb7d4d49f1", size = 432585, upload-time = "2025-12-05T15:08:47.885Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/1a/9ffe814d317c5224166b23e7c47f606d6e473712a2fad0f704ea9b99f246/urllib3-2.6.0-py3-none-any.whl", hash = "sha256:c90f7a39f716c572c4e3e58509581ebd83f9b59cced005b7db7ad2d22b0db99f", size = 131083, upload-time = "2025-12-05T15:08:45.983Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.35.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "distlib" },
|
||||
{ name = "filelock" },
|
||||
{ name = "platformdirs" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" },
|
||||
]
|
||||
|
|
|
|||
12
vim.toml
12
vim.toml
|
|
@ -16,3 +16,15 @@ any = true
|
|||
|
||||
[it]
|
||||
any = true
|
||||
|
||||
[before_each]
|
||||
any = true
|
||||
|
||||
[after_each]
|
||||
any = true
|
||||
|
||||
[spy]
|
||||
any = true
|
||||
|
||||
[stub]
|
||||
any = true
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue