il2ks 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (506) hide show
  1. il2ks/__init__.py +3 -0
  2. il2ks/__main__.py +7 -0
  3. il2ks/cli.py +537 -0
  4. il2ks/config.py +729 -0
  5. il2ks/core/__init__.py +1 -0
  6. il2ks/core/achievement_rules.py +83 -0
  7. il2ks/core/achievements.py +473 -0
  8. il2ks/core/catalog/__init__.py +1 -0
  9. il2ks/core/catalog/data/ammo.csv +59 -0
  10. il2ks/core/catalog/data/object_aliases.csv +2 -0
  11. il2ks/core/catalog/data/object_names_de.csv +436 -0
  12. il2ks/core/catalog/data/object_names_es.csv +439 -0
  13. il2ks/core/catalog/data/object_names_fr.csv +437 -0
  14. il2ks/core/catalog/data/object_names_pt-br.csv +439 -0
  15. il2ks/core/catalog/data/object_names_ru.csv +470 -0
  16. il2ks/core/catalog/data/objects.csv +490 -0
  17. il2ks/core/catalog/data/ordnance.csv +33 -0
  18. il2ks/core/catalog/data/payload_aliases.csv +13 -0
  19. il2ks/core/catalog/data/payloads.csv +416 -0
  20. il2ks/core/catalog/data/weapon_mod_names_de.csv +24 -0
  21. il2ks/core/catalog/data/weapon_mod_names_es.csv +24 -0
  22. il2ks/core/catalog/data/weapon_mod_names_fr.csv +24 -0
  23. il2ks/core/catalog/data/weapon_mod_names_pt-br.csv +24 -0
  24. il2ks/core/catalog/data/weapon_mod_names_ru.csv +24 -0
  25. il2ks/core/catalog/data/weapon_mods.csv +34 -0
  26. il2ks/core/catalog/loader.py +737 -0
  27. il2ks/core/killboard.py +13 -0
  28. il2ks/core/logparse/__init__.py +1 -0
  29. il2ks/core/logparse/events.py +364 -0
  30. il2ks/core/logparse/files.py +148 -0
  31. il2ks/core/logparse/parser.py +950 -0
  32. il2ks/core/logparse/tail.py +69 -0
  33. il2ks/core/ratings/__init__.py +1 -0
  34. il2ks/core/ratings/elo.py +191 -0
  35. il2ks/core/ratings/score.py +171 -0
  36. il2ks/core/replay/__init__.py +1 -0
  37. il2ks/core/replay/ammo.py +604 -0
  38. il2ks/core/replay/areas.py +66 -0
  39. il2ks/core/replay/attack.py +166 -0
  40. il2ks/core/replay/breakdown.py +251 -0
  41. il2ks/core/replay/config.py +71 -0
  42. il2ks/core/replay/credit.py +87 -0
  43. il2ks/core/replay/damage.py +99 -0
  44. il2ks/core/replay/fate.py +414 -0
  45. il2ks/core/replay/hits.py +108 -0
  46. il2ks/core/replay/judge.py +264 -0
  47. il2ks/core/replay/kills.py +236 -0
  48. il2ks/core/replay/live.py +142 -0
  49. il2ks/core/replay/model.py +262 -0
  50. il2ks/core/replay/pve.py +63 -0
  51. il2ks/core/replay/rams.py +93 -0
  52. il2ks/core/replay/resolve.py +223 -0
  53. il2ks/core/replay/result.py +311 -0
  54. il2ks/core/replay/state.py +523 -0
  55. il2ks/core/replay/toggles.py +19 -0
  56. il2ks/core/stat_marks.py +304 -0
  57. il2ks/core/streaks.py +177 -0
  58. il2ks/core/tours.py +143 -0
  59. il2ks/data/il2ks.example.toml +329 -0
  60. il2ks/db/__init__.py +1 -0
  61. il2ks/db/apps.py +8 -0
  62. il2ks/db/copy.py +30 -0
  63. il2ks/db/migrations/0001_initial.py +3023 -0
  64. il2ks/db/migrations/__init__.py +0 -0
  65. il2ks/db/models.py +2039 -0
  66. il2ks/db/reprocess_requests.py +43 -0
  67. il2ks/db/site.py +59 -0
  68. il2ks/db/validators.py +29 -0
  69. il2ks/devtools/__init__.py +1 -0
  70. il2ks/devtools/anonymize.py +73 -0
  71. il2ks/devtools/assets.py +565 -0
  72. il2ks/devtools/bailout_eval.py +217 -0
  73. il2ks/devtools/bench.py +221 -0
  74. il2ks/devtools/check.py +336 -0
  75. il2ks/devtools/dbdump.py +40 -0
  76. il2ks/devtools/migrations.py +154 -0
  77. il2ks/devtools/templates.py +60 -0
  78. il2ks/devtools/translations.py +354 -0
  79. il2ks/exitcodes.py +7 -0
  80. il2ks/ingest/__init__.py +1 -0
  81. il2ks/ingest/achievements.py +479 -0
  82. il2ks/ingest/activity.py +54 -0
  83. il2ks/ingest/aggregates.py +647 -0
  84. il2ks/ingest/aircraft_mods.py +166 -0
  85. il2ks/ingest/aircraft_stats.py +788 -0
  86. il2ks/ingest/archive.py +183 -0
  87. il2ks/ingest/batch.py +168 -0
  88. il2ks/ingest/builds.py +78 -0
  89. il2ks/ingest/counters.py +135 -0
  90. il2ks/ingest/dbutil.py +92 -0
  91. il2ks/ingest/discover.py +187 -0
  92. il2ks/ingest/flight_score.py +39 -0
  93. il2ks/ingest/identity.py +97 -0
  94. il2ks/ingest/live.py +468 -0
  95. il2ks/ingest/lock.py +185 -0
  96. il2ks/ingest/pairs.py +191 -0
  97. il2ks/ingest/persist.py +911 -0
  98. il2ks/ingest/rating_games.py +43 -0
  99. il2ks/ingest/ratings.py +186 -0
  100. il2ks/ingest/reprocess.py +303 -0
  101. il2ks/ingest/reprocess_requests.py +104 -0
  102. il2ks/ingest/rollup.py +125 -0
  103. il2ks/ingest/rule_store.py +167 -0
  104. il2ks/ingest/runner.py +417 -0
  105. il2ks/ingest/score_apply.py +44 -0
  106. il2ks/ingest/scoring.py +80 -0
  107. il2ks/ingest/stat_marks.py +181 -0
  108. il2ks/ingest/streaks.py +343 -0
  109. il2ks/ingest/timeutil.py +86 -0
  110. il2ks/ingest/tours.py +425 -0
  111. il2ks/ingest/type_board.py +158 -0
  112. il2ks/ingest/watch.py +153 -0
  113. il2ks/ingest/worker.py +45 -0
  114. il2ks/locale/de/LC_MESSAGES/django.mo +0 -0
  115. il2ks/locale/de/LC_MESSAGES/django.po +6781 -0
  116. il2ks/locale/es/LC_MESSAGES/django.mo +0 -0
  117. il2ks/locale/es/LC_MESSAGES/django.po +6767 -0
  118. il2ks/locale/fr/LC_MESSAGES/django.mo +0 -0
  119. il2ks/locale/fr/LC_MESSAGES/django.po +6798 -0
  120. il2ks/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
  121. il2ks/locale/pt_BR/LC_MESSAGES/django.po +6747 -0
  122. il2ks/locale/ru/LC_MESSAGES/django.mo +0 -0
  123. il2ks/locale/ru/LC_MESSAGES/django.po +6754 -0
  124. il2ks/logsetup.py +186 -0
  125. il2ks/ops/__init__.py +1 -0
  126. il2ks/ops/admin.py +108 -0
  127. il2ks/ops/backup.py +478 -0
  128. il2ks/ops/checks.py +552 -0
  129. il2ks/ops/commands.py +293 -0
  130. il2ks/ops/dbfile.py +58 -0
  131. il2ks/ops/detect.py +181 -0
  132. il2ks/ops/doctor.py +54 -0
  133. il2ks/ops/migrate.py +250 -0
  134. il2ks/ops/prompt.py +63 -0
  135. il2ks/ops/report.py +86 -0
  136. il2ks/ops/serving_checks.py +267 -0
  137. il2ks/ops/setup.py +521 -0
  138. il2ks/ops/template.py +133 -0
  139. il2ks/queries/__init__.py +1 -0
  140. il2ks/queries/achievements.py +85 -0
  141. il2ks/queries/activity.py +24 -0
  142. il2ks/queries/aircraft.py +525 -0
  143. il2ks/queries/ammo.py +261 -0
  144. il2ks/queries/boards.py +180 -0
  145. il2ks/queries/builds.py +54 -0
  146. il2ks/queries/leaderboards.py +393 -0
  147. il2ks/queries/live.py +114 -0
  148. il2ks/queries/missions.py +207 -0
  149. il2ks/queries/paging.py +15 -0
  150. il2ks/queries/players.py +243 -0
  151. il2ks/queries/sorties.py +217 -0
  152. il2ks/queries/sorting.py +76 -0
  153. il2ks/queries/stat_marks.py +29 -0
  154. il2ks/queries/tours.py +184 -0
  155. il2ks/rule_settings.py +219 -0
  156. il2ks/serving/__init__.py +2 -0
  157. il2ks/serving/_vendor/LICENSE-rjsmin-rcssmin.txt +201 -0
  158. il2ks/serving/_vendor/__init__.py +1 -0
  159. il2ks/serving/_vendor/rcssmin.py +436 -0
  160. il2ks/serving/_vendor/rjsmin.py +594 -0
  161. il2ks/serving/bootid.py +25 -0
  162. il2ks/serving/caddy.py +219 -0
  163. il2ks/serving/commands.py +653 -0
  164. il2ks/serving/custom.py +395 -0
  165. il2ks/serving/djsettings.py +130 -0
  166. il2ks/serving/procutil.py +257 -0
  167. il2ks/serving/secret.py +50 -0
  168. il2ks/serving/service.py +160 -0
  169. il2ks/serving/setup_token.py +195 -0
  170. il2ks/serving/storage.py +70 -0
  171. il2ks/serving/supervisor.py +269 -0
  172. il2ks/serving/templateversions.py +303 -0
  173. il2ks/serving/webserver.py +71 -0
  174. il2ks/settings.py +173 -0
  175. il2ks/urls.py +11 -0
  176. il2ks/web/__init__.py +1 -0
  177. il2ks/web/achievement_config.py +112 -0
  178. il2ks/web/admin.py +802 -0
  179. il2ks/web/admin_achievements.py +196 -0
  180. il2ks/web/admin_config.py +8 -0
  181. il2ks/web/admin_quips.py +170 -0
  182. il2ks/web/admin_rules.py +178 -0
  183. il2ks/web/admin_score.py +28 -0
  184. il2ks/web/admin_site.py +298 -0
  185. il2ks/web/apps.py +6 -0
  186. il2ks/web/branding_images.py +140 -0
  187. il2ks/web/caching.py +124 -0
  188. il2ks/web/chart_data.py +84 -0
  189. il2ks/web/charts.py +197 -0
  190. il2ks/web/column_hints.py +209 -0
  191. il2ks/web/columns.py +295 -0
  192. il2ks/web/context_processors.py +119 -0
  193. il2ks/web/display.py +324 -0
  194. il2ks/web/feature_image.py +307 -0
  195. il2ks/web/flavor.py +458 -0
  196. il2ks/web/fonts.py +146 -0
  197. il2ks/web/ground.py +58 -0
  198. il2ks/web/icons.py +140 -0
  199. il2ks/web/ingest_status.py +100 -0
  200. il2ks/web/logo.py +128 -0
  201. il2ks/web/medals.py +380 -0
  202. il2ks/web/media.py +59 -0
  203. il2ks/web/object_names.py +33 -0
  204. il2ks/web/pve.py +84 -0
  205. il2ks/web/quips.py +140 -0
  206. il2ks/web/setup_forms.py +154 -0
  207. il2ks/web/site_forms.py +473 -0
  208. il2ks/web/sortie_view.py +870 -0
  209. il2ks/web/static/il2ks/il2ks.js +209 -0
  210. il2ks/web/static/il2ks/img/README.md +130 -0
  211. il2ks/web/static/il2ks/img/aircraft/generic-jet.svg +1 -0
  212. il2ks/web/static/il2ks/img/aircraft/generic-prop.svg +1 -0
  213. il2ks/web/static/il2ks/img/aircraft/unknown.svg +1 -0
  214. il2ks/web/static/il2ks/img/brand/favicon.svg +1 -0
  215. il2ks/web/static/il2ks/img/brand/logo-mark.svg +1 -0
  216. il2ks/web/static/il2ks/img/coalition/blufor.svg +11 -0
  217. il2ks/web/static/il2ks/img/coalition/insignia/kpaf.svg +1 -0
  218. il2ks/web/static/il2ks/img/coalition/insignia/plaaf.svg +1 -0
  219. il2ks/web/static/il2ks/img/coalition/insignia/rokaf.svg +1 -0
  220. il2ks/web/static/il2ks/img/coalition/insignia/un.svg +1 -0
  221. il2ks/web/static/il2ks/img/coalition/insignia/usaf.svg +1 -0
  222. il2ks/web/static/il2ks/img/coalition/insignia/vvs.svg +1 -0
  223. il2ks/web/static/il2ks/img/coalition/redfor.svg +11 -0
  224. il2ks/web/static/il2ks/img/event/assist.svg +14 -0
  225. il2ks/web/static/il2ks/img/event/bailout.svg +14 -0
  226. il2ks/web/static/il2ks/img/event/bomb-release.svg +13 -0
  227. il2ks/web/static/il2ks/img/event/damaged.svg +12 -0
  228. il2ks/web/static/il2ks/img/event/destroyed.svg +11 -0
  229. il2ks/web/static/il2ks/img/event/disconnect.svg +18 -0
  230. il2ks/web/static/il2ks/img/event/friendly-fire.svg +13 -0
  231. il2ks/web/static/il2ks/img/event/kill-air.svg +16 -0
  232. il2ks/web/static/il2ks/img/event/kill-ground.svg +13 -0
  233. il2ks/web/static/il2ks/img/event/landing.svg +12 -0
  234. il2ks/web/static/il2ks/img/event/ram.svg +18 -0
  235. il2ks/web/static/il2ks/img/event/rocket-salvo.svg +13 -0
  236. il2ks/web/static/il2ks/img/event/sortie-end.svg +13 -0
  237. il2ks/web/static/il2ks/img/event/spawn.svg +13 -0
  238. il2ks/web/static/il2ks/img/event/takeoff.svg +12 -0
  239. il2ks/web/static/il2ks/img/flag/br.svg +45 -0
  240. il2ks/web/static/il2ks/img/flag/de.svg +5 -0
  241. il2ks/web/static/il2ks/img/flag/es.svg +4 -0
  242. il2ks/web/static/il2ks/img/flag/fr.svg +5 -0
  243. il2ks/web/static/il2ks/img/flag/ru.svg +5 -0
  244. il2ks/web/static/il2ks/img/flag/us.svg +9 -0
  245. il2ks/web/static/il2ks/img/ground/aaa.svg +1 -0
  246. il2ks/web/static/il2ks/img/ground/artillery.svg +1 -0
  247. il2ks/web/static/il2ks/img/ground/building.svg +18 -0
  248. il2ks/web/static/il2ks/img/ground/other-static.svg +19 -0
  249. il2ks/web/static/il2ks/img/ground/parked-aircraft.svg +11 -0
  250. il2ks/web/static/il2ks/img/ground/ship.svg +14 -0
  251. il2ks/web/static/il2ks/img/ground/tank.svg +13 -0
  252. il2ks/web/static/il2ks/img/ground/train.svg +17 -0
  253. il2ks/web/static/il2ks/img/ground/vehicle.svg +13 -0
  254. il2ks/web/static/il2ks/img/illustration/404.svg +1 -0
  255. il2ks/web/static/il2ks/img/illustration/500.svg +1 -0
  256. il2ks/web/static/il2ks/img/illustration/empty.svg +1 -0
  257. il2ks/web/static/il2ks/img/medal/ace-in-a-day.svg +19 -0
  258. il2ks/web/static/il2ks/img/medal/career-kills.svg +16 -0
  259. il2ks/web/static/il2ks/img/medal/damaged-landing.svg +15 -0
  260. il2ks/web/static/il2ks/img/medal/elo-peak.svg +12 -0
  261. il2ks/web/static/il2ks/img/medal/first-blood.svg +11 -0
  262. il2ks/web/static/il2ks/img/medal/flight-hours.svg +12 -0
  263. il2ks/web/static/il2ks/img/medal/frequent-flyer.svg +12 -0
  264. il2ks/web/static/il2ks/img/medal/ground-score.svg +14 -0
  265. il2ks/web/static/il2ks/img/medal/ground-sortie.svg +11 -0
  266. il2ks/web/static/il2ks/img/medal/landing-streak.svg +12 -0
  267. il2ks/web/static/il2ks/img/medal/life-kills.svg +14 -0
  268. il2ks/web/static/il2ks/img/medal/multi-kill.svg +12 -0
  269. il2ks/web/static/il2ks/img/medal/ram.svg +18 -0
  270. il2ks/web/static/il2ks/img/medal/regular.svg +21 -0
  271. il2ks/web/static/il2ks/img/medal/shame-crashed.svg +13 -0
  272. il2ks/web/static/il2ks/img/medal/shame-friendly.svg +14 -0
  273. il2ks/web/static/il2ks/img/medal/shame-strafed.svg +11 -0
  274. il2ks/web/static/il2ks/img/medal/shame-taxi.svg +15 -0
  275. il2ks/web/static/il2ks/img/medal/sortie-kills.svg +15 -0
  276. il2ks/web/static/il2ks/img/medal/strike-hunter.svg +12 -0
  277. il2ks/web/static/il2ks/img/medal/survivor.svg +12 -0
  278. il2ks/web/static/il2ks/img/medal/tank-buster.svg +13 -0
  279. il2ks/web/static/il2ks/img/medal/tours-in-a-row.svg +12 -0
  280. il2ks/web/static/il2ks/img/medal/type-veteran.svg +13 -0
  281. il2ks/web/static/il2ks/img/medal/types-flown.svg +13 -0
  282. il2ks/web/static/il2ks/img/medal/types-with-kills.svg +16 -0
  283. il2ks/web/static/il2ks/img/nav/book.svg +15 -0
  284. il2ks/web/static/il2ks/img/nav/discord.svg +14 -0
  285. il2ks/web/static/il2ks/img/nav/forum.svg +12 -0
  286. il2ks/web/static/il2ks/img/nav/github.svg +11 -0
  287. il2ks/web/static/il2ks/img/nav/link.svg +13 -0
  288. il2ks/web/static/il2ks/img/nav/mail.svg +12 -0
  289. il2ks/web/static/il2ks/img/nav/patreon.svg +11 -0
  290. il2ks/web/static/il2ks/img/nav/steam.svg +12 -0
  291. il2ks/web/static/il2ks/img/nav/telegram.svg +11 -0
  292. il2ks/web/static/il2ks/img/nav/twitch.svg +13 -0
  293. il2ks/web/static/il2ks/img/nav/youtube.svg +12 -0
  294. il2ks/web/static/il2ks/img/outcome/bailed-out.svg +14 -0
  295. il2ks/web/static/il2ks/img/outcome/captured.svg +18 -0
  296. il2ks/web/static/il2ks/img/outcome/crashed.svg +11 -0
  297. il2ks/web/static/il2ks/img/outcome/dead.svg +11 -0
  298. il2ks/web/static/il2ks/img/outcome/disconnected.svg +18 -0
  299. il2ks/web/static/il2ks/img/outcome/ditched.svg +13 -0
  300. il2ks/web/static/il2ks/img/outcome/exited-on-ground.svg +13 -0
  301. il2ks/web/static/il2ks/img/outcome/in-flight.svg +12 -0
  302. il2ks/web/static/il2ks/img/outcome/landed.svg +12 -0
  303. il2ks/web/static/il2ks/img/outcome/mission-ended.svg +13 -0
  304. il2ks/web/static/il2ks/img/outcome/not-taken-off.svg +12 -0
  305. il2ks/web/static/il2ks/img/outcome/shot-down.svg +12 -0
  306. il2ks/web/static/il2ks/img/outcome/unknown.svg +13 -0
  307. il2ks/web/static/il2ks/img/outcome/wounded.svg +15 -0
  308. il2ks/web/static/il2ks/img/pattern/camo.svg +18 -0
  309. il2ks/web/static/il2ks/img/role/air-superiority.svg +14 -0
  310. il2ks/web/static/il2ks/img/role/attack.svg +13 -0
  311. il2ks/web/static/il2ks/img/stat/accuracy.svg +13 -0
  312. il2ks/web/static/il2ks/img/stat/air-kills.svg +16 -0
  313. il2ks/web/static/il2ks/img/stat/assists.svg +14 -0
  314. il2ks/web/static/il2ks/img/stat/bailouts.svg +14 -0
  315. il2ks/web/static/il2ks/img/stat/captures.svg +18 -0
  316. il2ks/web/static/il2ks/img/stat/deaths.svg +15 -0
  317. il2ks/web/static/il2ks/img/stat/elo-jet.svg +12 -0
  318. il2ks/web/static/il2ks/img/stat/elo-prop.svg +15 -0
  319. il2ks/web/static/il2ks/img/stat/flight-time.svg +12 -0
  320. il2ks/web/static/il2ks/img/stat/friendly-fire.svg +15 -0
  321. il2ks/web/static/il2ks/img/stat/ground-hour.svg +13 -0
  322. il2ks/web/static/il2ks/img/stat/ground-kills.svg +13 -0
  323. il2ks/web/static/il2ks/img/stat/interception.svg +14 -0
  324. il2ks/web/static/il2ks/img/stat/planes-lost.svg +11 -0
  325. il2ks/web/static/il2ks/img/stat/play-time.svg +14 -0
  326. il2ks/web/static/il2ks/img/stat/sorties.svg +12 -0
  327. il2ks/web/static/il2ks/img/stat/strafed.svg +15 -0
  328. il2ks/web/static/il2ks/img/stat/taxi-accidents.svg +16 -0
  329. il2ks/web/static/il2ks/leaderboards.css +26 -0
  330. il2ks/web/static/il2ks/localtime.js +68 -0
  331. il2ks/web/static/il2ks/missions.css +65 -0
  332. il2ks/web/static/il2ks/players.css +129 -0
  333. il2ks/web/static/il2ks/setup.css +24 -0
  334. il2ks/web/static/il2ks/setup.js +15 -0
  335. il2ks/web/static/il2ks/site.css +769 -0
  336. il2ks/web/static/il2ks/sortie.js +15 -0
  337. il2ks/web/static/il2ks/sorties.css +83 -0
  338. il2ks/web/static/il2ks/theme-init.js +15 -0
  339. il2ks/web/static/il2ks/vendor/BarlowCondensed-600.woff2 +0 -0
  340. il2ks/web/static/il2ks/vendor/BarlowCondensed-700.woff2 +0 -0
  341. il2ks/web/static/il2ks/vendor/LICENSE-barlow-condensed-OFL.txt +93 -0
  342. il2ks/web/static/il2ks/vendor/LICENSE-htmx.txt +13 -0
  343. il2ks/web/static/il2ks/vendor/LICENSE-pico.md +21 -0
  344. il2ks/web/static/il2ks/vendor/README.md +18 -0
  345. il2ks/web/static/il2ks/vendor/htmx.min.js +1 -0
  346. il2ks/web/static/il2ks/vendor/pico.min.css +4 -0
  347. il2ks/web/static/il2ks_admin/ingest_status.css +15 -0
  348. il2ks/web/static/il2ks_admin/override_warning.css +12 -0
  349. il2ks/web/static/il2ks_admin/theme_editor.css +18 -0
  350. il2ks/web/static/il2ks_admin/theme_editor.js +36 -0
  351. il2ks/web/svg_symbol.py +128 -0
  352. il2ks/web/template_versions.json +569 -0
  353. il2ks/web/templates/404.html +17 -0
  354. il2ks/web/templates/500.html +27 -0
  355. il2ks/web/templates/admin/_rule_fields.html +23 -0
  356. il2ks/web/templates/admin/base_site.html +22 -0
  357. il2ks/web/templates/admin/il2ks_achievements.html +74 -0
  358. il2ks/web/templates/admin/il2ks_db/tour/change_list.html +15 -0
  359. il2ks/web/templates/admin/il2ks_index.html +55 -0
  360. il2ks/web/templates/admin/il2ks_ingest_status.html +174 -0
  361. il2ks/web/templates/admin/il2ks_leaderboards.html +21 -0
  362. il2ks/web/templates/admin/il2ks_quips.html +74 -0
  363. il2ks/web/templates/admin/il2ks_reprocess_confirm.html +23 -0
  364. il2ks/web/templates/admin/il2ks_rules.html +21 -0
  365. il2ks/web/templates/admin/il2ks_score.html +40 -0
  366. il2ks/web/templates/admin/il2ks_tours.html +41 -0
  367. il2ks/web/templates/il2ks/achievements/_award.html +5 -0
  368. il2ks/web/templates/il2ks/achievements/_medal.html +12 -0
  369. il2ks/web/templates/il2ks/achievements/_ribbon.html +12 -0
  370. il2ks/web/templates/il2ks/achievements/holders.html +35 -0
  371. il2ks/web/templates/il2ks/achievements/home_feed.html +24 -0
  372. il2ks/web/templates/il2ks/achievements/overview.html +38 -0
  373. il2ks/web/templates/il2ks/aircraft/detail.html +265 -0
  374. il2ks/web/templates/il2ks/aircraft/list.html +65 -0
  375. il2ks/web/templates/il2ks/aircraft/parts/ammo_mix_row.html +10 -0
  376. il2ks/web/templates/il2ks/aircraft/top_elo.html +36 -0
  377. il2ks/web/templates/il2ks/aircraft/top_ground.html +36 -0
  378. il2ks/web/templates/il2ks/base.html +100 -0
  379. il2ks/web/templates/il2ks/components/accordion.html +8 -0
  380. il2ks/web/templates/il2ks/components/badge.html +6 -0
  381. il2ks/web/templates/il2ks/components/bar_chart.html +47 -0
  382. il2ks/web/templates/il2ks/components/breadcrumbs.html +9 -0
  383. il2ks/web/templates/il2ks/components/col_hint.html +6 -0
  384. il2ks/web/templates/il2ks/components/col_th.html +10 -0
  385. il2ks/web/templates/il2ks/components/columns_picker.html +17 -0
  386. il2ks/web/templates/il2ks/components/dropdown.html +10 -0
  387. il2ks/web/templates/il2ks/components/empty_row.html +6 -0
  388. il2ks/web/templates/il2ks/components/filter_bar.html +20 -0
  389. il2ks/web/templates/il2ks/components/filter_select.html +10 -0
  390. il2ks/web/templates/il2ks/components/filter_text.html +8 -0
  391. il2ks/web/templates/il2ks/components/kv_list.html +7 -0
  392. il2ks/web/templates/il2ks/components/language_menu.html +11 -0
  393. il2ks/web/templates/il2ks/components/mod_filter.html +12 -0
  394. il2ks/web/templates/il2ks/components/notice.html +9 -0
  395. il2ks/web/templates/il2ks/components/online_now.html +13 -0
  396. il2ks/web/templates/il2ks/components/online_now_body.html +47 -0
  397. il2ks/web/templates/il2ks/components/pagination.html +16 -0
  398. il2ks/web/templates/il2ks/components/pilot_fate.html +6 -0
  399. il2ks/web/templates/il2ks/components/results_region.html +9 -0
  400. il2ks/web/templates/il2ks/components/role_toggle.html +10 -0
  401. il2ks/web/templates/il2ks/components/sort_th.html +10 -0
  402. il2ks/web/templates/il2ks/components/stat_mark.html +9 -0
  403. il2ks/web/templates/il2ks/components/stat_mark_note.html +5 -0
  404. il2ks/web/templates/il2ks/components/stat_tile.html +12 -0
  405. il2ks/web/templates/il2ks/components/tour_absent.html +11 -0
  406. il2ks/web/templates/il2ks/components/tour_filter.html +13 -0
  407. il2ks/web/templates/il2ks/components/tour_next.html +7 -0
  408. il2ks/web/templates/il2ks/components/tour_select.html +23 -0
  409. il2ks/web/templates/il2ks/home.html +109 -0
  410. il2ks/web/templates/il2ks/home_activity.html +10 -0
  411. il2ks/web/templates/il2ks/home_feature.html +17 -0
  412. il2ks/web/templates/il2ks/leaderboards/home_block.html +38 -0
  413. il2ks/web/templates/il2ks/leaderboards/ironman_running.html +26 -0
  414. il2ks/web/templates/il2ks/leaderboards/list.html +65 -0
  415. il2ks/web/templates/il2ks/leaderboards/table.html +105 -0
  416. il2ks/web/templates/il2ks/missions/_player.html +7 -0
  417. il2ks/web/templates/il2ks/missions/_rows.html +20 -0
  418. il2ks/web/templates/il2ks/missions/detail.html +136 -0
  419. il2ks/web/templates/il2ks/missions/list.html +44 -0
  420. il2ks/web/templates/il2ks/players/_killboard_rows.html +10 -0
  421. il2ks/web/templates/il2ks/players/_type_killboard.html +40 -0
  422. il2ks/web/templates/il2ks/players/achievements.html +40 -0
  423. il2ks/web/templates/il2ks/players/detail.html +59 -0
  424. il2ks/web/templates/il2ks/players/detail_achievements.html +20 -0
  425. il2ks/web/templates/il2ks/players/detail_air_section.html +31 -0
  426. il2ks/web/templates/il2ks/players/detail_air_split.html +8 -0
  427. il2ks/web/templates/il2ks/players/detail_air_tiles.html +14 -0
  428. il2ks/web/templates/il2ks/players/detail_aircraft.html +44 -0
  429. il2ks/web/templates/il2ks/players/detail_aircraft_build.html +16 -0
  430. il2ks/web/templates/il2ks/players/detail_charts.html +12 -0
  431. il2ks/web/templates/il2ks/players/detail_ground.html +29 -0
  432. il2ks/web/templates/il2ks/players/detail_ground_scores.html +13 -0
  433. il2ks/web/templates/il2ks/players/detail_ground_section.html +17 -0
  434. il2ks/web/templates/il2ks/players/detail_ground_tiles.html +17 -0
  435. il2ks/web/templates/il2ks/players/detail_header.html +26 -0
  436. il2ks/web/templates/il2ks/players/detail_killboard.html +34 -0
  437. il2ks/web/templates/il2ks/players/detail_nav.html +13 -0
  438. il2ks/web/templates/il2ks/players/detail_overall.html +15 -0
  439. il2ks/web/templates/il2ks/players/detail_pve.html +50 -0
  440. il2ks/web/templates/il2ks/players/detail_ratios.html +16 -0
  441. il2ks/web/templates/il2ks/players/detail_recent.html +40 -0
  442. il2ks/web/templates/il2ks/players/detail_scores.html +11 -0
  443. il2ks/web/templates/il2ks/players/detail_shame.html +17 -0
  444. il2ks/web/templates/il2ks/players/detail_star_tile.html +13 -0
  445. il2ks/web/templates/il2ks/players/detail_streaks.html +26 -0
  446. il2ks/web/templates/il2ks/players/detail_tiles.html +35 -0
  447. il2ks/web/templates/il2ks/players/detail_totals.html +16 -0
  448. il2ks/web/templates/il2ks/players/detail_type_killboard.html +13 -0
  449. il2ks/web/templates/il2ks/players/killboard.html +49 -0
  450. il2ks/web/templates/il2ks/players/search.html +25 -0
  451. il2ks/web/templates/il2ks/players/search_table.html +36 -0
  452. il2ks/web/templates/il2ks/players/streak_runs.html +57 -0
  453. il2ks/web/templates/il2ks/players/streaks.html +51 -0
  454. il2ks/web/templates/il2ks/setup.html +136 -0
  455. il2ks/web/templates/il2ks/setup_done.html +48 -0
  456. il2ks/web/templates/il2ks/setup_gate.html +32 -0
  457. il2ks/web/templates/il2ks/sorties/all.html +50 -0
  458. il2ks/web/templates/il2ks/sorties/detail.html +32 -0
  459. il2ks/web/templates/il2ks/sorties/list.html +54 -0
  460. il2ks/web/templates/il2ks/sorties/parts/aircraft_cell.html +5 -0
  461. il2ks/web/templates/il2ks/sorties/parts/all_row.html +16 -0
  462. il2ks/web/templates/il2ks/sorties/parts/ammo.html +72 -0
  463. il2ks/web/templates/il2ks/sorties/parts/damage.html +20 -0
  464. il2ks/web/templates/il2ks/sorties/parts/damage_table.html +28 -0
  465. il2ks/web/templates/il2ks/sorties/parts/header.html +44 -0
  466. il2ks/web/templates/il2ks/sorties/parts/kill_table.html +17 -0
  467. il2ks/web/templates/il2ks/sorties/parts/killed_by.html +31 -0
  468. il2ks/web/templates/il2ks/sorties/parts/kills.html +51 -0
  469. il2ks/web/templates/il2ks/sorties/parts/list_row.html +18 -0
  470. il2ks/web/templates/il2ks/sorties/parts/medals.html +16 -0
  471. il2ks/web/templates/il2ks/sorties/parts/meta.html +15 -0
  472. il2ks/web/templates/il2ks/sorties/parts/notices.html +20 -0
  473. il2ks/web/templates/il2ks/sorties/parts/summary.html +39 -0
  474. il2ks/web/templates/il2ks/sorties/parts/timeline.html +11 -0
  475. il2ks/web/templates/il2ks/sorties/parts/timeline_table.html +30 -0
  476. il2ks/web/templates/il2ks/sorties/parts/who.html +9 -0
  477. il2ks/web/templates/il2ks/styleguide.html +251 -0
  478. il2ks/web/templates/il2ks_admin/theme_editor.html +22 -0
  479. il2ks/web/templatetags/il2ks.py +884 -0
  480. il2ks/web/templatetags/il2ks_achievements.py +85 -0
  481. il2ks/web/templatetags/il2ks_boards.py +71 -0
  482. il2ks/web/templatetags/il2ks_live.py +39 -0
  483. il2ks/web/templatetags/il2ks_sorties.py +25 -0
  484. il2ks/web/theme.py +384 -0
  485. il2ks/web/throttle.py +76 -0
  486. il2ks/web/urls.py +48 -0
  487. il2ks/web/views/__init__.py +1 -0
  488. il2ks/web/views/achievements.py +161 -0
  489. il2ks/web/views/aircraft.py +357 -0
  490. il2ks/web/views/boards.py +123 -0
  491. il2ks/web/views/language.py +37 -0
  492. il2ks/web/views/leaderboards.py +168 -0
  493. il2ks/web/views/live.py +34 -0
  494. il2ks/web/views/missions.py +159 -0
  495. il2ks/web/views/players.py +162 -0
  496. il2ks/web/views/setup.py +252 -0
  497. il2ks/web/views/sorties.py +240 -0
  498. il2ks/web/views/sprite.py +25 -0
  499. il2ks/web/views/styleguide.py +152 -0
  500. il2ks/wsgi.py +15 -0
  501. il2ks-0.1.0.dist-info/METADATA +119 -0
  502. il2ks-0.1.0.dist-info/RECORD +506 -0
  503. il2ks-0.1.0.dist-info/WHEEL +4 -0
  504. il2ks-0.1.0.dist-info/entry_points.txt +2 -0
  505. il2ks-0.1.0.dist-info/licenses/LICENSE +21 -0
  506. il2ks-0.1.0.dist-info/licenses/NOTICE +93 -0
il2ks/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """il2ks: self-hosted statistics for IL-2 Sturmovik: Korea dedicated servers. See design_doc/."""
2
+
3
+ __version__ = "0.1.0"
il2ks/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ """`python -m il2ks ...`: same as the `il2ks` command. `il2ks run` starts its child processes this way, because it
2
+ needs no PATH lookup and always means the Python environment il2ks is installed in."""
3
+
4
+ from il2ks.cli import main
5
+
6
+ if __name__ == "__main__": # the guard matters: web-server workers re-import the main module when they start
7
+ raise SystemExit(main())
il2ks/cli.py ADDED
@@ -0,0 +1,537 @@
1
+ """The `il2ks` command (FR-OPS-1).
2
+
3
+ Exit codes (`il2ks.exitcodes`): 0 = done, 1 = done but some missions failed, 2 = usage or configuration error or a
4
+ refused action, 3 = another writer holds the lock (FR-ING-20). `il2ks doctor` reports its verdict the same way:
5
+ 0 = all OK, 1 = warnings only, 2 = at least one error.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import contextlib
12
+ import logging
13
+ import os
14
+ import re
15
+ import sys
16
+ from collections.abc import Callable, Sequence
17
+ from datetime import date
18
+ from pathlib import Path
19
+
20
+ from il2ks import __version__
21
+ from il2ks.config import Config, ConfigError, load_config
22
+ from il2ks.exitcodes import EXIT_FAILED, EXIT_LOCKED, EXIT_OK, EXIT_USAGE
23
+ from il2ks.ops.migrate import migrate_if_needed
24
+ from il2ks.ops.setup import HTTPS_MODES
25
+ from il2ks.serving import commands as serving_commands
26
+ from il2ks.serving import procutil
27
+
28
+ PLANNED: dict[str, tuple[str, str]] = {}
29
+ """Subcommands that exist only as stubs: name -> (requirement that plans it, one-line description)."""
30
+
31
+ OPS_COMMANDS = frozenset({"setup", "createadmin", "doctor", "backup", "restore"}) # handlers: il2ks.ops.commands
32
+
33
+ log = logging.getLogger("il2ks.cli")
34
+ _DATE_RE = re.compile(r"\d{4}-\d{2}-\d{2}")
35
+
36
+
37
+ def _django_setup() -> None:
38
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "il2ks.settings")
39
+ import django
40
+
41
+ django.setup()
42
+
43
+
44
+ def _setup_pending() -> bool:
45
+ """No admin account yet (so `web` creates the setup token); a database problem is not the setup page's business."""
46
+ from django.db import DatabaseError
47
+
48
+ from il2ks.ops import admin
49
+
50
+ try:
51
+ return not admin.admin_exists()
52
+ except DatabaseError:
53
+ return False
54
+
55
+
56
+ def _iso_date(text: str) -> date:
57
+ """argparse type for `YYYY-MM-DD` (strictly: not `20260401` or `2026-W14-3`)."""
58
+ try:
59
+ if _DATE_RE.fullmatch(text) is None:
60
+ raise ValueError(text)
61
+ return date.fromisoformat(text)
62
+ except ValueError:
63
+ raise argparse.ArgumentTypeError(f"{text!r} is not a date, expected YYYY-MM-DD") from None
64
+
65
+
66
+ type SubParsers = argparse._SubParsersAction[argparse.ArgumentParser] # pyright: ignore[reportPrivateUsage]
67
+
68
+
69
+ def _add_ops_parsers(sub: SubParsers) -> None:
70
+ """setup, createadmin, doctor, backup, restore (FR-OPS-1, FR-OPS-6); handlers in `il2ks.ops.commands`."""
71
+ env_note = "Every answer can also come from an environment variable; flags win."
72
+ setup = sub.add_parser(
73
+ "setup",
74
+ help="first-time setup: asks a few questions, writes il2ks.toml, creates the database and the admin",
75
+ description=(
76
+ "First-time setup. Asks for the data folder, DServer's text log folder, the server's time zone and the "
77
+ "website address, writes il2ks.toml (the example file with your answers), creates the data folder and "
78
+ "database and the admin account. Refuses to overwrite an existing il2ks.toml unless --force (the old "
79
+ "file is then saved next to it). Without a terminal, or with --non-interactive, nothing is asked. "
80
+ + env_note
81
+ ),
82
+ )
83
+ setup.add_argument("--data-dir", help="data folder [IL2KS_DATA_DIR]; default ./.il2ks-data")
84
+ setup.add_argument("--logs-dir", help="DServer's text log folder [IL2KS_LOGS_DIR]")
85
+ setup.add_argument(
86
+ "--timezone", help="IANA name of the server's time zone, e.g. Europe/Berlin [IL2KS_SERVER_TIMEZONE]"
87
+ )
88
+ setup.add_argument("--domain", help="the website's domain name [IL2KS_HTTPS_DOMAIN]")
89
+ setup.add_argument(
90
+ "--https", choices=HTTPS_MODES, help="caddy = bundled HTTPS proxy, external = your own [IL2KS_HTTPS_MODE]"
91
+ )
92
+ setup.add_argument("--email", help="e-mail for certificate notices (caddy mode) [IL2KS_HTTPS_EMAIL]")
93
+ setup.add_argument("--admin-username", help="admin account name [IL2KS_ADMIN_USERNAME]; default admin")
94
+ setup.add_argument(
95
+ "--admin-password-file", type=Path, help="file holding the admin password [IL2KS_ADMIN_PASSWORD]"
96
+ )
97
+ setup.add_argument("--no-admin", action="store_true", help="do not create an admin account")
98
+ setup.add_argument("--non-interactive", action="store_true", help="never ask; use flags, environment and defaults")
99
+ setup.add_argument("--force", action="store_true", help="replace an existing il2ks.toml (a copy is kept)")
100
+
101
+ admin_parser = sub.add_parser(
102
+ "createadmin",
103
+ help="create an admin account, or reset the password of an existing one",
104
+ description=(
105
+ "Create an admin (superuser) account, or reset an existing account's password and make it an admin. "
106
+ "In a terminal it asks for the password twice. For scripts give --username and the password in the "
107
+ "IL2KS_ADMIN_PASSWORD environment variable or --password-file."
108
+ ),
109
+ )
110
+ admin_parser.add_argument("--username", help="account name (asked for in a terminal, default admin)")
111
+ admin_parser.add_argument("--password-file", type=Path, help="file holding the password (first line break dropped)")
112
+ admin_parser.add_argument("--email", help="e-mail address of the account (optional)")
113
+ admin_parser.add_argument(
114
+ "--if-none",
115
+ action="store_true",
116
+ help="do nothing when an admin account already exists (for first-start scripts such as the Docker image)",
117
+ )
118
+ admin_parser.add_argument(
119
+ "--wait", type=float, default=30.0, metavar="SECONDS", help="how long to wait for another writer"
120
+ )
121
+
122
+ sub.add_parser(
123
+ "doctor",
124
+ help="check the setup and say what to fix",
125
+ description=(
126
+ "Checks the configuration, data folder, database, log folder, time zone, server ID, disk space, ingestion "
127
+ "history and backups. Exit code: 0 = all OK, 1 = warnings only, 2 = at least one error."
128
+ ),
129
+ ).add_argument("--json", action="store_true", help="machine-readable output")
130
+
131
+ sub.add_parser(
132
+ "backup",
133
+ help="write a backup zip of the database, config, custom/ and media/ to <data dir>/backups",
134
+ description=(
135
+ "Backs up what the mission logs cannot rebuild: a consistent copy of the SQLite database, il2ks.toml, "
136
+ "custom/, media/ and the server ID. Not the archived mission logs. Keeps the newest [backup] keep zips. "
137
+ "Backups are also made before database updates and, by 'il2ks watch', once a day."
138
+ ),
139
+ )
140
+ restore = sub.add_parser(
141
+ "restore",
142
+ help="restore a backup zip (database, config, custom/, media/)",
143
+ description=(
144
+ "Restores a backup made by 'il2ks backup'. Backs up the current state first, refuses while another "
145
+ "il2ks writer (watch, ingest) or the stack (il2ks run) is running unless --force, and checks the result. "
146
+ "SQLite only."
147
+ ),
148
+ )
149
+ restore.add_argument("zip", type=Path, help="the backup zip")
150
+ restore.add_argument("--yes", action="store_true", help="do not ask for confirmation")
151
+ restore.add_argument(
152
+ "--force", action="store_true", help="restore even though il2ks seems to be running (stop it first if you can)"
153
+ )
154
+
155
+
156
+ def _add_translation_parsers(dev: SubParsers) -> None:
157
+ """`il2ks dev translations ...` (TD-24, docs/translating.md): pure-Python gettext workflow."""
158
+ tr = dev.add_parser("translations", help="extract, merge, compile and check the translations (docs/translating.md)")
159
+ sub = tr.add_subparsers(dest="translations_command", required=True)
160
+ sub.add_parser("update", help="re-extract the strings, merge them into every language's .po, then compile")
161
+ sub.add_parser("compile", help="compile every .po into its .mo (after editing a .po by hand)")
162
+ sub.add_parser("check", help="change nothing; exit 1 when update/compile would change a file (CI, pre-commit)")
163
+ sub.add_parser("status", help="per language: strings, translated, still llm-draft (unreviewed), untranslated")
164
+ missing = sub.add_parser("missing", help="print a language's untranslated strings as JSON (input for a draft)")
165
+ missing.add_argument("language", help="ru, de, es, fr or pt-br")
166
+ imp = sub.add_parser("import", help="fill untranslated strings from a JSON file {msgid: msgstr}, marked llm-draft")
167
+ imp.add_argument("language", help="ru, de, es, fr or pt-br")
168
+ imp.add_argument("file", type=Path, help="JSON object: msgid -> msgstr (a list of forms for plural messages)")
169
+
170
+
171
+ def _translations_command(ns: argparse.Namespace) -> int:
172
+ import json
173
+
174
+ try:
175
+ from il2ks.devtools import translations
176
+ except ModuleNotFoundError as e: # Babel is a development dependency (not in a wheel installed to run a site)
177
+ if not (e.name or "").startswith("babel"):
178
+ raise # another import broke: show it, do not blame Babel
179
+ print(
180
+ "il2ks dev translations needs the development tools (Babel): run it in a checkout after `uv sync`",
181
+ file=sys.stderr,
182
+ )
183
+ return EXIT_USAGE
184
+
185
+ try:
186
+ match ns.translations_command:
187
+ case "update":
188
+ for directory, count in translations.update().items():
189
+ print(f"{directory}: {count} strings")
190
+ translations.compile_all()
191
+ case "check":
192
+ problems = translations.check()
193
+ for problem in problems:
194
+ print(f" - {problem}")
195
+ print("translations are up to date" if not problems else "run `uv run il2ks dev translations update`")
196
+ return EXIT_FAILED if problems else EXIT_OK
197
+ case "compile":
198
+ for path in translations.compile_all():
199
+ print(f"wrote {path}")
200
+ case "status":
201
+ print(f"{'language':<9}{'strings':>8}{'translated':>11}{'llm-draft':>10}{'reviewed':>9}{'missing':>8}")
202
+ for row in translations.status():
203
+ print(
204
+ f"{row.language:<9}{row.total:>8}{row.translated:>11}{row.drafts:>10}"
205
+ f"{row.reviewed:>9}{row.untranslated:>8}"
206
+ )
207
+ case "missing":
208
+ text = json.dumps(translations.missing(ns.language), ensure_ascii=False, indent=2)
209
+ sys.stdout.buffer.write(text.encode("utf-8") + b"\n") # UTF-8 whatever the console code page is
210
+ case _: # "import"
211
+ filled = translations.import_drafts(ns.language, translations.load_drafts(ns.file))
212
+ print(
213
+ f"{filled} strings filled, marked {translations.DRAFT_MARK}; run `il2ks dev translations compile`"
214
+ )
215
+ except translations.TranslationError as exc:
216
+ print(f"il2ks dev translations: {exc}", file=sys.stderr)
217
+ return EXIT_USAGE
218
+ return EXIT_OK
219
+
220
+
221
+ def _build_parser() -> argparse.ArgumentParser:
222
+ parser = argparse.ArgumentParser(prog="il2ks", description="IL-2 Korea stats")
223
+ parser.add_argument("--config", type=Path, help="il2ks.toml to use (default: IL2KS_CONFIG, ./il2ks.toml, data dir)")
224
+ parser.add_argument("--version", action="version", version=f"il2ks {__version__}")
225
+ sub = parser.add_subparsers(dest="command", required=True)
226
+ for name, (requirement, description) in PLANNED.items():
227
+ text = f"not implemented yet (planned: {requirement}): {description}"
228
+ sub.add_parser(name, help=text, description=text)
229
+
230
+ _add_ops_parsers(sub)
231
+
232
+ wait_help = "if another writer holds the lock, wait up to this many seconds instead of exiting"
233
+ ingest = sub.add_parser("ingest", help="process every complete mission once, then exit")
234
+ ingest.add_argument(
235
+ "--from",
236
+ dest="source",
237
+ type=Path,
238
+ help="import from this folder or file instead of the log folder (concatenated .txt/.txt.zip archives or parts)",
239
+ )
240
+ ingest.add_argument("--wait", type=float, metavar="SECONDS", help=wait_help)
241
+
242
+ sub.add_parser("watch", help="run ingest every N seconds until Ctrl+C")
243
+
244
+ reprocess = sub.add_parser(
245
+ "reprocess",
246
+ help="re-run missions from their archives, then rebuild aggregates (choose: --all, --mission, --since/--until)",
247
+ description=(
248
+ "Re-runs missions from their archives with the current rules, then rebuilds the aggregates. It can take a "
249
+ "long time on a big archive, so you must choose what to reprocess: --all for every mission, or narrow it "
250
+ "with --mission and/or --since/--until. Without any of these, nothing is done."
251
+ ),
252
+ )
253
+ reprocess.set_defaults(reprocess_parser=reprocess)
254
+ reprocess.add_argument(
255
+ "--all", action="store_true", help="reprocess every mission (explicit, can take a long time)"
256
+ )
257
+ reprocess.add_argument("--mission", action="append", metavar="UID", help="only this mission (repeatable)")
258
+ span_note = "The date is the one in the mission UID, i.e. the server's local time (not UTC). Inclusive."
259
+ reprocess.add_argument(
260
+ "--since", type=_iso_date, metavar="YYYY-MM-DD", help=f"only missions on or after this date. {span_note}"
261
+ )
262
+ reprocess.add_argument(
263
+ "--until", type=_iso_date, metavar="YYYY-MM-DD", help=f"only missions on or before this date. {span_note}"
264
+ )
265
+ reprocess.add_argument("--workers", type=int, help="parse/replay worker processes (default: CPUs - 1)")
266
+ reprocess.add_argument("--wait", type=float, metavar="SECONDS", help=wait_help)
267
+
268
+ rebuild = sub.add_parser("rebuild-aggregates", help="recompute level-2 tables from level 1")
269
+ rebuild.add_argument(
270
+ "--retour",
271
+ action="store_true",
272
+ help="first move every mission to its tour under the [tours] config (after changing mode, start or timezone)",
273
+ )
274
+ rebuild.add_argument("--wait", type=float, metavar="SECONDS", help=wait_help)
275
+
276
+ sub.add_parser("manage", help="run a Django management command")
277
+ db = sub.add_parser("db", help="database tools").add_subparsers(dest="db_command", required=True)
278
+ db_copy = db.add_parser("copy", help="copy all tables between database aliases (TD-19)")
279
+ db_copy.add_argument("--from", dest="source", default="default")
280
+ db_copy.add_argument("--to", dest="target", required=True)
281
+ dev = sub.add_parser("dev", help="developer tools").add_subparsers(dest="dev_command", required=True)
282
+ anon = dev.add_parser("anonymize", help="anonymize a mission log for test fixtures")
283
+ anon.add_argument("source", type=Path, help="mission .txt or .txt.zip")
284
+ anon.add_argument("target", type=Path, help="output .txt.zip")
285
+ bump = dev.add_parser(
286
+ "bump-templates",
287
+ help="after editing built-in templates/CSS/JS: add version lines, raise versions, rewrite the registry",
288
+ )
289
+ bump.add_argument("--check", action="store_true", help="change nothing; exit 1 if something would change")
290
+ changes = dev.add_parser(
291
+ "template-changes", help="template versions that changed since a release tag (release notes)"
292
+ )
293
+ changes.add_argument("old_tag", help="a git tag or commit, like v0.2.0")
294
+ bench = dev.add_parser("bench-ingest", help="time an import of a log folder, split by phase (throw-away data dir)")
295
+ bench.add_argument("source", type=Path, help="folder with mission logs (.txt / .txt.zip)")
296
+ bench.add_argument("--limit", type=int, metavar="N", help="only the first N missions")
297
+ bench.add_argument("--profile", type=Path, metavar="FILE", help="write a cProfile dump of the whole run")
298
+ bench.add_argument("--data-dir", type=Path, metavar="DIR", help="keep the resulting data dir here (for dump-db)")
299
+ bench.add_argument("--cpu", action="store_true", help="time process CPU instead of wall clock (noisy machines)")
300
+ bench.add_argument("--per-mission", action="store_true", help="level 2 after every mission, never batched")
301
+ dump = dev.add_parser(
302
+ "dump-db", help="write every table of a data dir's database as sorted JSON lines, to compare runs"
303
+ )
304
+ dump.add_argument("data_dir", type=Path, help="a data dir with il2ks.sqlite3")
305
+ dump.add_argument("target", type=Path, help="output file (.jsonl)")
306
+ assets = dev.add_parser(
307
+ "assets", help="inventory of every image the site uses or ships (design_doc/15, the designer brief)"
308
+ )
309
+ assets.add_argument("--check", action="store_true", help="exit 1 when the brief, the code and static/ disagree")
310
+ assets.add_argument("--write", action="store_true", help="rewrite the generated inventory in design_doc/15")
311
+ _add_translation_parsers(dev)
312
+ chk = dev.add_parser(
313
+ "check",
314
+ help="everything to run before committing (lint, types, guards, tests); see CLAUDE.md. Default tier: quick",
315
+ )
316
+ tier = chk.add_mutually_exclusive_group()
317
+ tier.add_argument("--fast", action="store_true", help="static checks + guards only (~15 s; the Stop hook)")
318
+ tier.add_argument("--quick", action="store_true", help="fast + unit tests (the default; before every commit)")
319
+ tier.add_argument("--full", action="store_true", help="quick + integration tests (before merging / finishing)")
320
+ chk.add_argument(
321
+ "--fix", action="store_true", help="first run the auto-fixers: ruff --fix/format, bump-templates, translations"
322
+ )
323
+ chk.add_argument("--no-tests", action="store_true", help="skip every test step (CI lint job)")
324
+ chk.add_argument("--postgres", action="store_true", help="also run the whole suite on Postgres (compose.dev.yaml)")
325
+ chk.add_argument("--e2e", action="store_true", help="also run the Playwright browser tests")
326
+ chk.add_argument("--only", action="append", default=[], metavar="STEP", help="just this step (repeatable)")
327
+ chk.add_argument("--list", action="store_true", dest="list_steps", help="list the steps of the chosen tier, exit")
328
+ chk.add_argument("--if-changed", action="store_true", help="skip when nothing changed since the last passing run")
329
+ chk.add_argument("--verbose", action="store_true", help="show the whole output of failed steps")
330
+ dev.add_parser(
331
+ "check-migrations",
332
+ help="released migrations unchanged (vs origin/main or main), exactly one leaf, no duplicate numbers",
333
+ )
334
+ serving_commands.add_parsers(sub)
335
+ return parser
336
+
337
+
338
+ def main(argv: Sequence[str] | None = None) -> int:
339
+ """Run the command. Ctrl+C / SIGTERM at any point (also while starting up) is a clean stop, not a traceback."""
340
+ try:
341
+ return _main(argv)
342
+ except KeyboardInterrupt:
343
+ return EXIT_OK
344
+
345
+
346
+ def _main(argv: Sequence[str] | None) -> int:
347
+ args = list(sys.argv[1:] if argv is None else argv)
348
+ if args and args[0] == "manage":
349
+ # Pass everything after "manage" to Django (migrate, createsuperuser, ...).
350
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "il2ks.settings")
351
+ # A first `il2ks manage migrate` on a fresh machine: SQLite can't create the folder it lives in. Any problem
352
+ # here is left for Django to report in its own words.
353
+ with contextlib.suppress(ConfigError, OSError):
354
+ load_config(create_server_uid=False).data_dir.mkdir(parents=True, exist_ok=True)
355
+ from django.core.management import execute_from_command_line
356
+
357
+ execute_from_command_line(["il2ks manage", *args[1:]])
358
+ return EXIT_OK
359
+
360
+ ns = _build_parser().parse_args(args)
361
+ command: str = ns.command
362
+ if command in serving_commands.COMMANDS:
363
+ hooks = serving_commands.Hooks(
364
+ django_setup=_django_setup,
365
+ migrate=migrate_if_needed,
366
+ setup_pending=_setup_pending,
367
+ contain_children=procutil.kill_children_when_we_die,
368
+ )
369
+ return serving_commands.dispatch(ns, hooks)
370
+ if command == "watch":
371
+ procutil.terminate_as_keyboard_interrupt() # `il2ks run` stops its children with SIGTERM / Ctrl+Break
372
+ if command == "dev" and ns.dev_command == "anonymize":
373
+ from il2ks.devtools.anonymize import anonymize_file
374
+
375
+ anonymize_file(ns.source, ns.target)
376
+ return EXIT_OK
377
+ if command == "dev" and ns.dev_command == "bailout-eval":
378
+ from il2ks.devtools.bailout_eval import run as run_bailout_eval
379
+
380
+ return run_bailout_eval(ns.directory, list_disagreements=ns.list)
381
+ if command == "dev" and ns.dev_command == "bench-ingest":
382
+ from il2ks.devtools.bench import bench_ingest
383
+
384
+ return bench_ingest(
385
+ ns.source, limit=ns.limit, profile=ns.profile, data_dir=ns.data_dir, cpu=ns.cpu, per_mission=ns.per_mission
386
+ )
387
+ if command == "dev" and ns.dev_command == "dump-db":
388
+ from il2ks.devtools.dbdump import dump_db
389
+
390
+ return dump_db(ns.data_dir, ns.target)
391
+ if command == "dev" and ns.dev_command == "bump-templates":
392
+ from il2ks.devtools.templates import bump_templates
393
+
394
+ return bump_templates(check=ns.check)
395
+ if command == "dev" and ns.dev_command == "assets":
396
+ _django_setup()
397
+ from il2ks.devtools.assets import run as run_assets
398
+
399
+ return run_assets(check=ns.check, write=ns.write)
400
+ if command == "dev" and ns.dev_command == "template-changes":
401
+ from il2ks.devtools.templates import template_changes
402
+
403
+ return template_changes(ns.old_tag)
404
+ if command == "dev" and ns.dev_command == "check":
405
+ from il2ks.devtools.check import Tier, check
406
+
407
+ chosen: Tier = "full" if ns.full else "fast" if ns.fast else "quick"
408
+ return check(
409
+ chosen,
410
+ fix=ns.fix,
411
+ no_tests=ns.no_tests,
412
+ postgres=ns.postgres,
413
+ e2e=ns.e2e,
414
+ only=ns.only,
415
+ if_changed=ns.if_changed,
416
+ verbose=ns.verbose,
417
+ list_only=ns.list_steps,
418
+ )
419
+ if command == "dev" and ns.dev_command == "check-migrations":
420
+ from il2ks.devtools import migrations as dev_migrations
421
+ from il2ks.devtools.check import repo_root
422
+
423
+ return dev_migrations.check(repo_root())
424
+ if command == "dev" and ns.dev_command == "translations":
425
+ return _translations_command(ns)
426
+ if command == "db" and ns.db_command == "copy":
427
+ _django_setup()
428
+ from il2ks.db.copy import copy_all
429
+
430
+ counts = copy_all(ns.source, ns.target)
431
+ for table, n in counts.items():
432
+ print(f"{table}: {n}")
433
+ return EXIT_OK
434
+ if command in {"ingest", "watch", "reprocess", "rebuild-aggregates"}:
435
+ return _writer_command(command, ns)
436
+ if command in OPS_COMMANDS:
437
+ from il2ks.ops import commands
438
+
439
+ handlers: dict[str, Callable[[argparse.Namespace], int]] = {
440
+ "setup": commands.cmd_setup,
441
+ "createadmin": commands.cmd_createadmin,
442
+ "doctor": commands.cmd_doctor,
443
+ "backup": commands.cmd_backup,
444
+ "restore": commands.cmd_restore,
445
+ }
446
+ return handlers[command](ns)
447
+ print(f"il2ks {command}: not implemented yet (planned: {PLANNED[command][0]})", file=sys.stderr)
448
+ return EXIT_USAGE
449
+
450
+
451
+ def _writer_command(command: str, ns: argparse.Namespace) -> int:
452
+ """Commands that write the DB: config, logging, writer lock, migrations, then the job."""
453
+ if ns.command == "reprocess" and not (ns.all or ns.mission or ns.since is not None or ns.until is not None):
454
+ ns.reprocess_parser.print_help(sys.stderr)
455
+ print(
456
+ "\nil2ks: choose what to reprocess: --all, --mission UID or --since/--until (nothing was reprocessed)",
457
+ file=sys.stderr,
458
+ )
459
+ return EXIT_USAGE
460
+ if ns.command == "reprocess" and ns.all and (ns.mission or ns.since is not None or ns.until is not None):
461
+ print("il2ks: --all cannot be combined with --mission, --since or --until", file=sys.stderr)
462
+ return EXIT_USAGE
463
+ if ns.command == "reprocess" and ns.since is not None and ns.until is not None and ns.since > ns.until:
464
+ print(f"il2ks: --since {ns.since} is after --until {ns.until}", file=sys.stderr)
465
+ return EXIT_USAGE
466
+ try:
467
+ cfg = load_config(ns.config)
468
+ except ConfigError as exc:
469
+ print(f"il2ks: configuration error: {exc}", file=sys.stderr)
470
+ return EXIT_USAGE
471
+ source: Path | None = getattr(ns, "source", None)
472
+ if command in {"ingest", "watch"} and source is None and cfg.logs.dir is None:
473
+ print("il2ks: logs.dir is not configured ([logs] dir in il2ks.toml, or IL2KS_LOGS_DIR)", file=sys.stderr)
474
+ return EXIT_USAGE
475
+ if source is not None and not source.exists():
476
+ print(f"il2ks: --from: {source} does not exist", file=sys.stderr)
477
+ return EXIT_USAGE
478
+ if command in {"ingest", "watch"} and source is None and cfg.logs.dir is not None and not cfg.logs.dir.is_dir():
479
+ print(f"il2ks: log folder {cfg.logs.dir} does not exist", file=sys.stderr)
480
+ return EXIT_USAGE
481
+
482
+ # settings.py reads the data dir from the environment; make it match the config before Django starts.
483
+ os.environ["IL2KS_DATA_DIR"] = str(cfg.data_dir)
484
+ cfg.data_dir.mkdir(parents=True, exist_ok=True)
485
+ from il2ks import logsetup
486
+
487
+ logsetup.configure_logging(
488
+ command, cfg.log_dir, cfg.log_level, server_uid=cfg.server_uid, keep_days=cfg.log_keep_days
489
+ )
490
+ _django_setup()
491
+
492
+ from il2ks.ingest.lock import LockBusyError
493
+
494
+ wait: float | None = getattr(ns, "wait", None)
495
+ try:
496
+ migrate_if_needed(cfg, command, wait) # FR-OPS-3, after a backup when the database has data (FR-OPS-6)
497
+ return _run_job(command, ns, cfg, source, wait)
498
+ except LockBusyError as exc:
499
+ print(f"il2ks {command}: {exc}", file=sys.stderr)
500
+ return EXIT_LOCKED
501
+ except KeyboardInterrupt:
502
+ log.info("stopped")
503
+ return EXIT_OK
504
+
505
+
506
+ def _run_job(command: str, ns: argparse.Namespace, cfg: Config, source: Path | None, wait: float | None) -> int:
507
+ from il2ks.ingest import reprocess as reprocess_mod
508
+ from il2ks.ingest import runner
509
+ from il2ks.ingest import watch as watch_mod
510
+
511
+ if command == "ingest":
512
+ opts = runner.IngestOptions(source=source, lock_wait=wait)
513
+ summary = runner.ingest_once(cfg, runner.default_pipeline(cfg), opts)
514
+ print(summary.describe())
515
+ return EXIT_FAILED if summary.failed else EXIT_OK
516
+ if command == "watch":
517
+ watch_mod.watch(cfg, runner.default_pipeline(cfg))
518
+ return EXIT_OK
519
+ if command == "reprocess":
520
+ result = reprocess_mod.reprocess(
521
+ cfg,
522
+ runner.default_pipeline(cfg, defer_ratings=True),
523
+ ns.mission,
524
+ since=ns.since,
525
+ until=ns.until,
526
+ workers=ns.workers,
527
+ lock_wait=wait,
528
+ )
529
+ print(result.describe())
530
+ return EXIT_FAILED if (result.failed or result.missing) else EXIT_OK
531
+ reprocess_mod.rebuild_all(cfg, reassign_tours=ns.retour, lock_wait=wait)
532
+ print("level-2 aggregates rebuilt" + (" (missions reassigned to tours)" if ns.retour else ""))
533
+ return EXIT_OK
534
+
535
+
536
+ if __name__ == "__main__":
537
+ raise SystemExit(main())