stackvitals 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 (465) hide show
  1. mergescout/__init__.py +1 -0
  2. mergescout/archive/__init__.py +0 -0
  3. mergescout/archive/__main__.py +30 -0
  4. mergescout/archive/convert.py +83 -0
  5. mergescout/archive/events.py +55 -0
  6. mergescout/archive/manifest.py +45 -0
  7. mergescout/archive/stream.py +83 -0
  8. mergescout/bench/__init__.py +1 -0
  9. mergescout/bench/__main__.py +135 -0
  10. mergescout/bench/ci.py +257 -0
  11. mergescout/bench/deploys.py +115 -0
  12. mergescout/bench/matched.py +22 -0
  13. mergescout/bench/prior.py +42 -0
  14. mergescout/bench/report.py +230 -0
  15. mergescout/bench/shared.py +112 -0
  16. mergescout/bench/stars.py +57 -0
  17. mergescout/cli.py +89 -0
  18. mergescout/config.py +35 -0
  19. mergescout/crawl/__init__.py +0 -0
  20. mergescout/crawl/__main__.py +97 -0
  21. mergescout/crawl/chain.py +34 -0
  22. mergescout/crawl/fetch.py +146 -0
  23. mergescout/crawl/outcomes.py +69 -0
  24. mergescout/crawl/queries.py +250 -0
  25. mergescout/crawl/runner.py +176 -0
  26. mergescout/crawl/seed.py +77 -0
  27. mergescout/crawl/state.py +169 -0
  28. mergescout/crawl/writer.py +87 -0
  29. mergescout/discover/__init__.py +0 -0
  30. mergescout/discover/__main__.py +58 -0
  31. mergescout/discover/sweep.py +286 -0
  32. mergescout/github/__init__.py +0 -0
  33. mergescout/github/graphql.py +202 -0
  34. mergescout/github/identities.py +35 -0
  35. mergescout/github/releases.py +137 -0
  36. mergescout/labels.py +20 -0
  37. mergescout/mcp_server.py +228 -0
  38. mergescout/measure/__init__.py +0 -0
  39. mergescout/measure/__main__.py +75 -0
  40. mergescout/measure/api_cost.py +91 -0
  41. mergescout/measure/count_repos.py +30 -0
  42. mergescout/measure/jsonl.py +14 -0
  43. mergescout/measure/report.py +56 -0
  44. mergescout/measure/sample.py +37 -0
  45. mergescout/owners.py +22 -0
  46. mergescout/setup_check.py +64 -0
  47. mergescout/signals/__init__.py +0 -0
  48. mergescout/signals/__main__.py +96 -0
  49. mergescout/signals/bump_ledger.py +258 -0
  50. mergescout/signals/bumps.py +104 -0
  51. mergescout/signals/check.py +141 -0
  52. mergescout/signals/community_bench.py +149 -0
  53. mergescout/signals/delivery_bench.py +103 -0
  54. mergescout/signals/deps_map.py +261 -0
  55. mergescout/signals/deps_trends.py +117 -0
  56. mergescout/signals/evidence.py +94 -0
  57. mergescout/signals/exploits.py +43 -0
  58. mergescout/signals/flow_bench.py +258 -0
  59. mergescout/signals/identity.py +73 -0
  60. mergescout/signals/issues.py +76 -0
  61. mergescout/signals/labels.py +79 -0
  62. mergescout/signals/osv.py +232 -0
  63. mergescout/signals/owners_watch.py +80 -0
  64. mergescout/signals/repo_stats.py +96 -0
  65. mergescout/signals/rules.py +70 -0
  66. mergescout/signals/silver.py +33 -0
  67. mergescout/signals/top_packages.py +26 -0
  68. mergescout/signals/triage.py +71 -0
  69. mergescout/signals/triage_math.py +120 -0
  70. mergescout/signals/walls.py +91 -0
  71. mergescout/snapshot/__init__.py +0 -0
  72. mergescout/snapshot/__main__.py +215 -0
  73. mergescout/snapshot/build.py +147 -0
  74. mergescout/snapshot/publish.py +70 -0
  75. mergescout/snapshot/schema.py +34 -0
  76. mergescout/snapshot/score.py +30 -0
  77. mergescout/store/__init__.py +0 -0
  78. mergescout/store/__main__.py +46 -0
  79. mergescout/store/sync.py +148 -0
  80. mergescout/timeutil.py +11 -0
  81. mergescout/tool/__init__.py +0 -0
  82. mergescout/tool/action_pack.py +62 -0
  83. mergescout/tool/live.py +201 -0
  84. mergescout/tool/queries.py +293 -0
  85. mergescout/tool/refs.py +31 -0
  86. mergescout/tool/store.py +129 -0
  87. mergescout/tool/tracking.py +83 -0
  88. stackvitals/__init__.py +2 -0
  89. stackvitals/__main__.py +4 -0
  90. stackvitals/ci/__init__.py +1 -0
  91. stackvitals/ci/action.py +574 -0
  92. stackvitals/ci/analyze.py +577 -0
  93. stackvitals/ci/api.py +218 -0
  94. stackvitals/ci/automation.py +212 -0
  95. stackvitals/ci/bench.py +195 -0
  96. stackvitals/ci/burnin.py +102 -0
  97. stackvitals/ci/cli.py +570 -0
  98. stackvitals/ci/collect.py +239 -0
  99. stackvitals/ci/cost.py +107 -0
  100. stackvitals/ci/coverage.py +155 -0
  101. stackvitals/ci/feedback.py +699 -0
  102. stackvitals/ci/fingerprint.py +220 -0
  103. stackvitals/ci/gitlab.py +121 -0
  104. stackvitals/ci/gitlab_action.py +125 -0
  105. stackvitals/ci/impact.py +125 -0
  106. stackvitals/ci/investigate.py +59 -0
  107. stackvitals/ci/kinds.py +52 -0
  108. stackvitals/ci/monitors.py +105 -0
  109. stackvitals/ci/owners.py +116 -0
  110. stackvitals/ci/quarantine_runtime.py +90 -0
  111. stackvitals/ci/report.py +435 -0
  112. stackvitals/ci/retry.py +67 -0
  113. stackvitals/ci/selfheal.py +102 -0
  114. stackvitals/ci/shards.py +120 -0
  115. stackvitals/ci/share.py +59 -0
  116. stackvitals/ci/store.py +93 -0
  117. stackvitals/ci/testreports.py +351 -0
  118. stackvitals/ci/tickets.py +106 -0
  119. stackvitals/ci/track_record.json +11 -0
  120. stackvitals/ci/triage.py +130 -0
  121. stackvitals/ci/triggers.py +147 -0
  122. stackvitals/ci/upload.py +85 -0
  123. stackvitals/ci/validate.py +91 -0
  124. stackvitals/cli.py +111 -0
  125. stackvitals/codeowners.py +69 -0
  126. stackvitals/community/__init__.py +0 -0
  127. stackvitals/community/action.py +140 -0
  128. stackvitals/community/affiliation.py +51 -0
  129. stackvitals/community/analyze.py +507 -0
  130. stackvitals/community/build.py +56 -0
  131. stackvitals/community/chaoss.py +70 -0
  132. stackvitals/community/cli.py +79 -0
  133. stackvitals/community/collect.py +240 -0
  134. stackvitals/community/context.py +54 -0
  135. stackvitals/community/report.py +250 -0
  136. stackvitals/connectors/__init__.py +244 -0
  137. stackvitals/connectors/ai_spend.py +190 -0
  138. stackvitals/connectors/base.py +82 -0
  139. stackvitals/connectors/check.py +686 -0
  140. stackvitals/connectors/copilot.py +104 -0
  141. stackvitals/connectors/discord.py +103 -0
  142. stackvitals/connectors/discourse.py +117 -0
  143. stackvitals/connectors/incident_io.py +106 -0
  144. stackvitals/connectors/jira.py +210 -0
  145. stackvitals/connectors/linear.py +140 -0
  146. stackvitals/connectors/pagerduty.py +96 -0
  147. stackvitals/connectors/slack.py +110 -0
  148. stackvitals/connectors/sonar.py +77 -0
  149. stackvitals/connectors/store.py +120 -0
  150. stackvitals/connectors/tracker.py +108 -0
  151. stackvitals/connectors/writeback.py +105 -0
  152. stackvitals/contribute/__init__.py +1 -0
  153. stackvitals/contribute/brief.py +297 -0
  154. stackvitals/contribute/cli.py +64 -0
  155. stackvitals/core/__init__.py +1 -0
  156. stackvitals/core/actors.py +103 -0
  157. stackvitals/core/agent_ledger.py +120 -0
  158. stackvitals/core/agreements.py +95 -0
  159. stackvitals/core/ai_outcomes.py +276 -0
  160. stackvitals/core/alerts.py +224 -0
  161. stackvitals/core/appdata.py +112 -0
  162. stackvitals/core/bucket.py +190 -0
  163. stackvitals/core/bugs.py +43 -0
  164. stackvitals/core/calendar.py +34 -0
  165. stackvitals/core/capex.py +57 -0
  166. stackvitals/core/change_risk.py +181 -0
  167. stackvitals/core/changes.py +133 -0
  168. stackvitals/core/chat.py +143 -0
  169. stackvitals/core/config.py +373 -0
  170. stackvitals/core/dataflow.py +168 -0
  171. stackvitals/core/decisions.py +275 -0
  172. stackvitals/core/delivery.py +954 -0
  173. stackvitals/core/delivery_peers.py +98 -0
  174. stackvitals/core/delivery_report.py +429 -0
  175. stackvitals/core/deliverycli.py +147 -0
  176. stackvitals/core/devlake_dora.sql +485 -0
  177. stackvitals/core/digest.py +333 -0
  178. stackvitals/core/explaincli.py +107 -0
  179. stackvitals/core/explore.py +296 -0
  180. stackvitals/core/export.py +342 -0
  181. stackvitals/core/exportcli.py +117 -0
  182. stackvitals/core/findings.py +350 -0
  183. stackvitals/core/health.py +94 -0
  184. stackvitals/core/identity.py +82 -0
  185. stackvitals/core/insights.py +365 -0
  186. stackvitals/core/insightscli.py +43 -0
  187. stackvitals/core/installdoc.py +69 -0
  188. stackvitals/core/investment.py +187 -0
  189. stackvitals/core/joins.py +43 -0
  190. stackvitals/core/llmsdoc.py +40 -0
  191. stackvitals/core/merge_queue.py +70 -0
  192. stackvitals/core/metrics.py +109 -0
  193. stackvitals/core/model.py +285 -0
  194. stackvitals/core/projectcli.py +66 -0
  195. stackvitals/core/propose.py +86 -0
  196. stackvitals/core/pulse.py +85 -0
  197. stackvitals/core/recorded.py +145 -0
  198. stackvitals/core/releases.py +39 -0
  199. stackvitals/core/requests.py +528 -0
  200. stackvitals/core/resync.py +35 -0
  201. stackvitals/core/schema.py +205 -0
  202. stackvitals/core/services.py +203 -0
  203. stackvitals/core/settings.py +96 -0
  204. stackvitals/core/sqlcli.py +111 -0
  205. stackvitals/core/stats.py +222 -0
  206. stackvitals/core/store.py +125 -0
  207. stackvitals/core/summary.py +104 -0
  208. stackvitals/core/time.py +29 -0
  209. stackvitals/core/tomledit.py +149 -0
  210. stackvitals/core/uicli.py +138 -0
  211. stackvitals/core/webapp.py +79 -0
  212. stackvitals/core/what_changed.py +76 -0
  213. stackvitals/core/workspace.py +194 -0
  214. stackvitals/deps/__init__.py +1 -0
  215. stackvitals/deps/action.py +299 -0
  216. stackvitals/deps/actions_vulns.py +89 -0
  217. stackvitals/deps/cli.py +77 -0
  218. stackvitals/deps/dossier.py +341 -0
  219. stackvitals/deps/evidence.py +44 -0
  220. stackvitals/deps/fixplan.py +40 -0
  221. stackvitals/deps/guarddog.py +32 -0
  222. stackvitals/deps/hygiene.py +47 -0
  223. stackvitals/deps/imports.py +113 -0
  224. stackvitals/deps/lockfiles.py +198 -0
  225. stackvitals/deps/lookalike.py +80 -0
  226. stackvitals/deps/reach.py +59 -0
  227. stackvitals/deps/release.py +110 -0
  228. stackvitals/deps/runtimes.py +161 -0
  229. stackvitals/deps/supply.py +454 -0
  230. stackvitals/deps/vulns.py +588 -0
  231. stackvitals/editors/__init__.py +40 -0
  232. stackvitals/editors/vscode/extension.js +101 -0
  233. stackvitals/editors/vscode/marks.js +92 -0
  234. stackvitals/editors/vscode/package.json +26 -0
  235. stackvitals/flow/__init__.py +1 -0
  236. stackvitals/flow/action.py +178 -0
  237. stackvitals/flow/analyze.py +418 -0
  238. stackvitals/flow/automate.py +72 -0
  239. stackvitals/flow/build.py +46 -0
  240. stackvitals/flow/cli.py +93 -0
  241. stackvitals/flow/code_hosts.py +146 -0
  242. stackvitals/flow/collect.py +451 -0
  243. stackvitals/flow/gitlab_mr.py +75 -0
  244. stackvitals/flow/knowledge.py +202 -0
  245. stackvitals/flow/peers.py +90 -0
  246. stackvitals/flow/preflight.py +210 -0
  247. stackvitals/flow/report.py +253 -0
  248. stackvitals/flow/rules.py +135 -0
  249. stackvitals/flow/store.py +108 -0
  250. stackvitals/flow/survival.py +84 -0
  251. stackvitals/flow/triage.py +62 -0
  252. stackvitals/mcp_server.py +670 -0
  253. stackvitals/portal/__init__.py +0 -0
  254. stackvitals/portal/action.py +205 -0
  255. stackvitals/portal/batch.py +80 -0
  256. stackvitals/portal/campaigns.py +104 -0
  257. stackvitals/portal/catalog.py +104 -0
  258. stackvitals/portal/checks.py +63 -0
  259. stackvitals/portal/cli.py +172 -0
  260. stackvitals/portal/descriptor.py +105 -0
  261. stackvitals/portal/export.py +43 -0
  262. stackvitals/portal/github.py +48 -0
  263. stackvitals/portal/golden.py +95 -0
  264. stackvitals/portal/history.py +30 -0
  265. stackvitals/portal/inventory.py +264 -0
  266. stackvitals/portal/owners.py +78 -0
  267. stackvitals/portal/pages.py +153 -0
  268. stackvitals/portal/rollup.py +32 -0
  269. stackvitals/portal/scorecard.py +366 -0
  270. stackvitals/security/__init__.py +2 -0
  271. stackvitals/security/probes.py +154 -0
  272. stackvitals/security/repo_checks.py +132 -0
  273. stackvitals/security/workflows.py +111 -0
  274. stackvitals/shell.py +107 -0
  275. stackvitals/site.py +51 -0
  276. stackvitals/web/app/LICENSES.md +428 -0
  277. stackvitals/web/app/boot.js +1 -0
  278. stackvitals/web/app/chunks/AddRepo-KRPZXWCY.js +1 -0
  279. stackvitals/web/app/chunks/SetupRunning-XWFQAKRZ.js +1 -0
  280. stackvitals/web/app/chunks/accuracy-data-OQTDXI7Y.js +1 -0
  281. stackvitals/web/app/chunks/agents-data-BAL6QAOF.js +11 -0
  282. stackvitals/web/app/chunks/chunk-24LFKFZ6.js +1 -0
  283. stackvitals/web/app/chunks/chunk-3G4SQNUC.js +1 -0
  284. stackvitals/web/app/chunks/chunk-3IXWEOIJ.js +154 -0
  285. stackvitals/web/app/chunks/chunk-3ROQFHPV.js +1 -0
  286. stackvitals/web/app/chunks/chunk-45PGAQRO.js +8 -0
  287. stackvitals/web/app/chunks/chunk-4H53OYLG.js +1 -0
  288. stackvitals/web/app/chunks/chunk-4LUI7O5P.js +1 -0
  289. stackvitals/web/app/chunks/chunk-4SQPQ5TB.js +1 -0
  290. stackvitals/web/app/chunks/chunk-4WKQTWUC.js +2 -0
  291. stackvitals/web/app/chunks/chunk-576YHDEK.js +8 -0
  292. stackvitals/web/app/chunks/chunk-6BDBXA6V.js +1 -0
  293. stackvitals/web/app/chunks/chunk-6RLVD2DT.js +1 -0
  294. stackvitals/web/app/chunks/chunk-763NH7PP.js +1 -0
  295. stackvitals/web/app/chunks/chunk-76WAVKUG.js +1 -0
  296. stackvitals/web/app/chunks/chunk-7II2BD5F.js +1 -0
  297. stackvitals/web/app/chunks/chunk-7TJCBOAY.js +1 -0
  298. stackvitals/web/app/chunks/chunk-ASYNLVOP.js +1 -0
  299. stackvitals/web/app/chunks/chunk-CTKDYZMG.js +1 -0
  300. stackvitals/web/app/chunks/chunk-CVB26NMC.js +1 -0
  301. stackvitals/web/app/chunks/chunk-G7NGB5UX.js +1 -0
  302. stackvitals/web/app/chunks/chunk-GPVLJRZD.js +1 -0
  303. stackvitals/web/app/chunks/chunk-GQYFN4PE.js +75 -0
  304. stackvitals/web/app/chunks/chunk-GT4EAKAO.js +2 -0
  305. stackvitals/web/app/chunks/chunk-IYMBFPCV.js +1 -0
  306. stackvitals/web/app/chunks/chunk-LAAGCPU3.js +1 -0
  307. stackvitals/web/app/chunks/chunk-MAPYGK3P.js +1 -0
  308. stackvitals/web/app/chunks/chunk-P5TXDD45.js +1 -0
  309. stackvitals/web/app/chunks/chunk-PABICGBL.js +1 -0
  310. stackvitals/web/app/chunks/chunk-RPXHAG6F.js +1 -0
  311. stackvitals/web/app/chunks/chunk-RQZPSJ2Y.js +1 -0
  312. stackvitals/web/app/chunks/chunk-S2ZBMEVA.js +1 -0
  313. stackvitals/web/app/chunks/chunk-S654BBEP.js +6 -0
  314. stackvitals/web/app/chunks/chunk-T7QVTNXF.js +1 -0
  315. stackvitals/web/app/chunks/chunk-THFUOCPC.js +273 -0
  316. stackvitals/web/app/chunks/chunk-TO4VVRZ7.js +1 -0
  317. stackvitals/web/app/chunks/chunk-VQ5YXO54.js +1 -0
  318. stackvitals/web/app/chunks/chunk-VWKSMFWJ.js +1 -0
  319. stackvitals/web/app/chunks/chunk-Y6LUXDM6.js +1 -0
  320. stackvitals/web/app/chunks/chunk-YTANGYYH.js +1 -0
  321. stackvitals/web/app/chunks/connect-KONYWYTT.js +1 -0
  322. stackvitals/web/app/chunks/connections-data-2DYS4UVW.js +32 -0
  323. stackvitals/web/app/chunks/logos-N5IINUAM.js +1 -0
  324. stackvitals/web/app/chunks/more-KMJMJQD3.js +10 -0
  325. stackvitals/web/app/chunks/more-PC6FVS3P.js +1 -0
  326. stackvitals/web/app/chunks/more-RCIVCUDE.js +2 -0
  327. stackvitals/web/app/chunks/more-ZKKRY6NO.css +1 -0
  328. stackvitals/web/app/chunks/sealbox-35YL4EUH.js +3 -0
  329. stackvitals/web/app/chunks/session-DYYTOE2K.js +1 -0
  330. stackvitals/web/app/chunks/settingsApply-2HRJEZYM.js +1 -0
  331. stackvitals/web/app/chunks/settingsReview-GX2FZQ4M.js +3 -0
  332. stackvitals/web/app/chunks/uPlot.esm-OIWT3AHJ.js +3 -0
  333. stackvitals/web/app/demo/ci/impact.json +1 -0
  334. stackvitals/web/app/demo/ci/quarantine.json +1 -0
  335. stackvitals/web/app/demo/ci/tickets.json +1 -0
  336. stackvitals/web/app/demo/mobile/alerts.json +1 -0
  337. stackvitals/web/app/demo/mobile/app.json +1 -0
  338. stackvitals/web/app/demo/mobile/community.json +1 -0
  339. stackvitals/web/app/demo/mobile/connectors.json +1 -0
  340. stackvitals/web/app/demo/mobile/data.json +1 -0
  341. stackvitals/web/app/demo/mobile/decisions.json +1 -0
  342. stackvitals/web/app/demo/mobile/delivery.json +1 -0
  343. stackvitals/web/app/demo/mobile/deps.json +1 -0
  344. stackvitals/web/app/demo/mobile/flow.json +1 -0
  345. stackvitals/web/app/demo/mobile/health.json +1 -0
  346. stackvitals/web/app/demo/mobile/insights.json +1 -0
  347. stackvitals/web/app/demo/mobile/pulse.json +1 -0
  348. stackvitals/web/app/demo/mobile/security.json +1 -0
  349. stackvitals/web/app/demo/mobile/summary/ci_jobs.jsonl +5 -0
  350. stackvitals/web/app/demo/mobile/summary/devlake_project_pr_metrics.jsonl +14 -0
  351. stackvitals/web/app/demo/mobile/summary/devlake_pull_requests.jsonl +14 -0
  352. stackvitals/web/app/demo/mobile/summary/manifest.json +1 -0
  353. stackvitals/web/app/demo/mobile/summary/prs.jsonl +19 -0
  354. stackvitals/web/app/demo/mobile/summary/tests.jsonl +1 -0
  355. stackvitals/web/app/demo/mobile/supply.json +1 -0
  356. stackvitals/web/app/demo/mobile/vulns.json +1 -0
  357. stackvitals/web/app/demo/payments-api/alerts.json +1 -0
  358. stackvitals/web/app/demo/payments-api/app.json +1 -0
  359. stackvitals/web/app/demo/payments-api/community.json +1 -0
  360. stackvitals/web/app/demo/payments-api/connectors.json +1 -0
  361. stackvitals/web/app/demo/payments-api/data.json +1 -0
  362. stackvitals/web/app/demo/payments-api/decisions.json +1 -0
  363. stackvitals/web/app/demo/payments-api/delivery.json +1 -0
  364. stackvitals/web/app/demo/payments-api/deps.json +1 -0
  365. stackvitals/web/app/demo/payments-api/flow.json +1 -0
  366. stackvitals/web/app/demo/payments-api/health.json +1 -0
  367. stackvitals/web/app/demo/payments-api/insights.json +1 -0
  368. stackvitals/web/app/demo/payments-api/pulse.json +1 -0
  369. stackvitals/web/app/demo/payments-api/security.json +1 -0
  370. stackvitals/web/app/demo/payments-api/summary/ci_jobs.jsonl +5 -0
  371. stackvitals/web/app/demo/payments-api/summary/devlake_project_pr_metrics.jsonl +14 -0
  372. stackvitals/web/app/demo/payments-api/summary/devlake_pull_requests.jsonl +14 -0
  373. stackvitals/web/app/demo/payments-api/summary/manifest.json +1 -0
  374. stackvitals/web/app/demo/payments-api/summary/prs.jsonl +22 -0
  375. stackvitals/web/app/demo/payments-api/summary/tests.jsonl +3 -0
  376. stackvitals/web/app/demo/payments-api/supply.json +1 -0
  377. stackvitals/web/app/demo/payments-api/vulns.json +1 -0
  378. stackvitals/web/app/demo/portal.json +1 -0
  379. stackvitals/web/app/demo/settings.json +1 -0
  380. stackvitals/web/app/demo/web-app/alerts.json +1 -0
  381. stackvitals/web/app/demo/web-app/app.json +1 -0
  382. stackvitals/web/app/demo/web-app/community.json +1 -0
  383. stackvitals/web/app/demo/web-app/connectors.json +1 -0
  384. stackvitals/web/app/demo/web-app/data.json +1 -0
  385. stackvitals/web/app/demo/web-app/decisions.json +1 -0
  386. stackvitals/web/app/demo/web-app/delivery.json +1 -0
  387. stackvitals/web/app/demo/web-app/deps.json +1 -0
  388. stackvitals/web/app/demo/web-app/flow.json +1 -0
  389. stackvitals/web/app/demo/web-app/health.json +1 -0
  390. stackvitals/web/app/demo/web-app/insights.json +1 -0
  391. stackvitals/web/app/demo/web-app/pulse.json +1 -0
  392. stackvitals/web/app/demo/web-app/security.json +1 -0
  393. stackvitals/web/app/demo/web-app/summary/ci_jobs.jsonl +5 -0
  394. stackvitals/web/app/demo/web-app/summary/devlake_project_pr_metrics.jsonl +14 -0
  395. stackvitals/web/app/demo/web-app/summary/devlake_pull_requests.jsonl +14 -0
  396. stackvitals/web/app/demo/web-app/summary/manifest.json +1 -0
  397. stackvitals/web/app/demo/web-app/summary/prs.jsonl +18 -0
  398. stackvitals/web/app/demo/web-app/summary/tests.jsonl +2 -0
  399. stackvitals/web/app/demo/web-app/supply.json +1 -0
  400. stackvitals/web/app/demo/web-app/vulns.json +1 -0
  401. stackvitals/web/app/fonts/plex-mono-400.woff2 +0 -0
  402. stackvitals/web/app/fonts/plex-sans-400.woff2 +0 -0
  403. stackvitals/web/app/fonts/plex-sans-600.woff2 +0 -0
  404. stackvitals/web/app/fonts/plex-sans-condensed-700.woff2 +0 -0
  405. stackvitals/web/app/help/accuracy.json +1 -0
  406. stackvitals/web/app/help/connectors.json +1 -0
  407. stackvitals/web/app/help/getting-started.json +1 -0
  408. stackvitals/web/app/help/index.json +1 -0
  409. stackvitals/web/app/help/modules.json +1 -0
  410. stackvitals/web/app/help/privacy-and-security.json +1 -0
  411. stackvitals/web/app/help/reference.json +1 -0
  412. stackvitals/web/app/main.css +1 -0
  413. stackvitals/web/app/main.js +1 -0
  414. stackvitals/web/app/pages/about.css +1 -0
  415. stackvitals/web/app/pages/about.js +1 -0
  416. stackvitals/web/app/pages/activity.css +1 -0
  417. stackvitals/web/app/pages/activity.js +1 -0
  418. stackvitals/web/app/pages/agents.css +1 -0
  419. stackvitals/web/app/pages/agents.js +2 -0
  420. stackvitals/web/app/pages/ci.css +1 -0
  421. stackvitals/web/app/pages/ci.js +1 -0
  422. stackvitals/web/app/pages/community.css +1 -0
  423. stackvitals/web/app/pages/community.js +1 -0
  424. stackvitals/web/app/pages/connections.css +1 -0
  425. stackvitals/web/app/pages/connections.js +6 -0
  426. stackvitals/web/app/pages/delivery.css +1 -0
  427. stackvitals/web/app/pages/delivery.js +45 -0
  428. stackvitals/web/app/pages/deps.css +1 -0
  429. stackvitals/web/app/pages/deps.js +7 -0
  430. stackvitals/web/app/pages/explore.css +1 -0
  431. stackvitals/web/app/pages/explore.js +7 -0
  432. stackvitals/web/app/pages/health.css +1 -0
  433. stackvitals/web/app/pages/health.js +1 -0
  434. stackvitals/web/app/pages/help.css +1 -0
  435. stackvitals/web/app/pages/help.js +6 -0
  436. stackvitals/web/app/pages/insights.css +1 -0
  437. stackvitals/web/app/pages/insights.js +1 -0
  438. stackvitals/web/app/pages/investment.css +1 -0
  439. stackvitals/web/app/pages/investment.js +3 -0
  440. stackvitals/web/app/pages/overview.css +1 -0
  441. stackvitals/web/app/pages/overview.js +1 -0
  442. stackvitals/web/app/pages/portal.css +1 -0
  443. stackvitals/web/app/pages/portal.js +51 -0
  444. stackvitals/web/app/pages/prs.css +1 -0
  445. stackvitals/web/app/pages/prs.js +4 -0
  446. stackvitals/web/app/pages/settings.css +1 -0
  447. stackvitals/web/app/pages/settings.js +1 -0
  448. stackvitals/web/app/pages/setup.css +1 -0
  449. stackvitals/web/app/pages/setup.js +2 -0
  450. stackvitals/web/app/pages/standing.css +1 -0
  451. stackvitals/web/app/pages/standing.js +1 -0
  452. stackvitals/web/app/pages/welcome-sheet.js +1 -0
  453. stackvitals/web/app/pages/welcome.js +1 -0
  454. stackvitals/web/app/settings-workflow.json +1 -0
  455. stackvitals/web/index.html +34 -0
  456. stackvitals/web/site/index.html +108 -0
  457. stackvitals/web/site/integrations.html +15 -0
  458. stackvitals/web/site/setup.html +15 -0
  459. stackvitals/web/site/site.css +68 -0
  460. stackvitals-0.1.0.dist-info/METADATA +134 -0
  461. stackvitals-0.1.0.dist-info/RECORD +465 -0
  462. stackvitals-0.1.0.dist-info/WHEEL +4 -0
  463. stackvitals-0.1.0.dist-info/entry_points.txt +3 -0
  464. stackvitals-0.1.0.dist-info/licenses/LICENSE +202 -0
  465. stackvitals-0.1.0.dist-info/licenses/NOTICE +16 -0
mergescout/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,30 @@
1
+ import argparse
2
+ import sys
3
+
4
+ from mergescout.archive.convert import convert_hour
5
+ from mergescout.archive.manifest import Manifest
6
+ from mergescout.config import load_config
7
+
8
+
9
+ def main(argv: list[str] | None = None) -> int:
10
+ parser = argparse.ArgumentParser(prog="python -m mergescout.archive",
11
+ description="Convert GH Archive hours into slim Parquet files.")
12
+ parser.add_argument("hours", nargs="+", help="UTC hour keys like 2026-10-01-15")
13
+ args = parser.parse_args(argv)
14
+ cfg = load_config()
15
+ manifest = Manifest(cfg.data_dir / "manifest.sqlite")
16
+ failed = 0
17
+ for key in args.hours:
18
+ try:
19
+ r = convert_hour(key, cfg.data_dir, manifest)
20
+ print(f"{r.hour_key}: {r.status} in={r.events_in} kept={r.events_kept} "
21
+ f"bad={r.bad_lines} bytes={r.bytes_out}")
22
+ except Exception as exc:
23
+ failed += 1
24
+ print(f"{key}: failed ({exc})", file=sys.stderr)
25
+ manifest.close()
26
+ return 1 if failed else 0
27
+
28
+
29
+ if __name__ == "__main__":
30
+ raise SystemExit(main())
@@ -0,0 +1,83 @@
1
+ import hashlib
2
+ import os
3
+ from collections.abc import Callable, Iterable
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ import pyarrow as pa
8
+ import pyarrow.parquet as pq
9
+
10
+ from mergescout.archive.events import SCHEMA, slim_event
11
+ from mergescout.archive.manifest import Manifest
12
+ from mergescout.archive.stream import fetch_chunks, hour_url, iter_events, normalize_hour_key, parse_hour_key
13
+
14
+
15
+ @dataclass
16
+ class HourResult:
17
+ hour_key: str
18
+ status: str
19
+ events_in: int = 0
20
+ events_kept: int = 0
21
+ bad_lines: int = 0
22
+ bytes_out: int = 0
23
+ path: Path | None = None
24
+
25
+
26
+ def hour_path(data_dir: Path, key: str) -> Path:
27
+ day, hour = parse_hour_key(key)
28
+ return data_dir / "bronze" / f"date={day}" / f"hour={hour}.parquet"
29
+
30
+
31
+ def write_parquet(rows: Iterable[dict], out_path: Path, batch_size: int = 50_000) -> int:
32
+ # Write to a .part file first so a crash never leaves a half file that looks finished.
33
+ part = out_path.with_name(out_path.name + ".part")
34
+ written = 0
35
+ try:
36
+ with pq.ParquetWriter(part, SCHEMA, compression="zstd") as writer:
37
+ batch = []
38
+ for row in rows:
39
+ batch.append(row)
40
+ if len(batch) >= batch_size:
41
+ writer.write_table(pa.Table.from_pylist(batch, schema=SCHEMA))
42
+ written += len(batch)
43
+ batch = []
44
+ if batch:
45
+ writer.write_table(pa.Table.from_pylist(batch, schema=SCHEMA))
46
+ written += len(batch)
47
+ os.replace(part, out_path)
48
+ finally:
49
+ if part.exists():
50
+ part.unlink()
51
+ return written
52
+
53
+
54
+ def _sha256(path: Path) -> str:
55
+ digest = hashlib.sha256()
56
+ with path.open("rb") as f:
57
+ for block in iter(lambda: f.read(1 << 20), b""):
58
+ digest.update(block)
59
+ return digest.hexdigest()
60
+
61
+
62
+ def convert_hour(
63
+ key: str,
64
+ data_dir: Path,
65
+ manifest: Manifest,
66
+ chunks_factory: Callable[[str], Iterable[bytes]] = fetch_chunks,
67
+ ) -> HourResult:
68
+ key = normalize_hour_key(key)
69
+ if manifest.is_done(key):
70
+ return HourResult(key, "skipped")
71
+ out = hour_path(data_dir, key)
72
+ out.parent.mkdir(parents=True, exist_ok=True)
73
+ stats = {"events_in": 0, "bad_lines": 0}
74
+ try:
75
+ events = iter_events(chunks_factory(hour_url(key)), stats)
76
+ kept = write_parquet((row for row in map(slim_event, events) if row is not None), out)
77
+ except Exception as exc:
78
+ manifest.mark_failed(key, f"{type(exc).__name__}: {exc}")
79
+ raise
80
+ size = out.stat().st_size
81
+ manifest.mark_done(key, events_in=stats["events_in"], events_kept=kept,
82
+ bad_lines=stats["bad_lines"], bytes_out=size, sha256=_sha256(out))
83
+ return HourResult(key, "done", stats["events_in"], kept, stats["bad_lines"], size, out)
@@ -0,0 +1,55 @@
1
+ import pyarrow as pa
2
+
3
+ from mergescout.timeutil import parse_time
4
+
5
+ KEPT_TYPES = frozenset({
6
+ "IssuesEvent", "IssueCommentEvent", "PullRequestEvent", "PullRequestReviewEvent", "PushEvent",
7
+ })
8
+
9
+ SCHEMA = pa.schema([
10
+ ("event_id", pa.string()),
11
+ ("type", pa.string()),
12
+ ("action", pa.string()),
13
+ ("created_at", pa.timestamp("us", tz="UTC")),
14
+ ("repo_id", pa.int64()),
15
+ ("repo_name", pa.string()),
16
+ ("actor_login", pa.string()),
17
+ ("number", pa.int64()),
18
+ ("is_pr", pa.bool_()),
19
+ ("issue_state", pa.string()),
20
+ ("assignee_count", pa.int32()),
21
+ ("label_names", pa.list_(pa.string())),
22
+ ("body_len", pa.int32()),
23
+ ("ref", pa.string()),
24
+ ])
25
+
26
+
27
+ def slim_event(event: dict) -> dict | None:
28
+ etype = event.get("type")
29
+ if etype not in KEPT_TYPES:
30
+ return None
31
+ payload = event.get("payload") or {}
32
+ repo = event.get("repo") or {}
33
+ actor = event.get("actor") or {}
34
+ issue = payload.get("issue") or {}
35
+ pr = payload.get("pull_request") or {}
36
+ comment = payload.get("comment") or {}
37
+ labels = issue.get("labels")
38
+ assignees = issue.get("assignees")
39
+ body = comment.get("body") if etype == "IssueCommentEvent" else issue.get("body")
40
+ return {
41
+ "event_id": str(event.get("id")),
42
+ "type": etype,
43
+ "action": payload.get("action"),
44
+ "created_at": parse_time(event.get("created_at")),
45
+ "repo_id": repo.get("id"),
46
+ "repo_name": repo.get("name"),
47
+ "actor_login": actor.get("login"),
48
+ "number": issue.get("number") or pr.get("number") or payload.get("number"),
49
+ "is_pr": etype in ("PullRequestEvent", "PullRequestReviewEvent") or "pull_request" in issue,
50
+ "issue_state": issue.get("state"),
51
+ "assignee_count": len(assignees) if assignees is not None else None,
52
+ "label_names": [lb["name"] for lb in labels if lb.get("name")] if labels is not None else None,
53
+ "body_len": len(body) if body is not None else None,
54
+ "ref": payload.get("ref") if etype == "PushEvent" else None,
55
+ }
@@ -0,0 +1,45 @@
1
+ import sqlite3
2
+ from datetime import datetime, timezone
3
+ from pathlib import Path
4
+
5
+
6
+ def _now() -> str:
7
+ return datetime.now(timezone.utc).isoformat(timespec="seconds")
8
+
9
+
10
+ class Manifest:
11
+ def __init__(self, path: Path):
12
+ path.parent.mkdir(parents=True, exist_ok=True)
13
+ self.db = sqlite3.connect(path)
14
+ self.db.row_factory = sqlite3.Row
15
+ with self.db:
16
+ self.db.execute(
17
+ "CREATE TABLE IF NOT EXISTS archive_hours ("
18
+ "hour_key TEXT PRIMARY KEY, status TEXT NOT NULL, events_in INTEGER, events_kept INTEGER, "
19
+ "bad_lines INTEGER, bytes_out INTEGER, sha256 TEXT, error TEXT, updated_at TEXT NOT NULL)"
20
+ )
21
+
22
+ def get(self, key: str) -> dict | None:
23
+ row = self.db.execute("SELECT * FROM archive_hours WHERE hour_key = ?", (key,)).fetchone()
24
+ return dict(row) if row else None
25
+
26
+ def is_done(self, key: str) -> bool:
27
+ row = self.get(key)
28
+ return bool(row and row["status"] == "done")
29
+
30
+ def mark_done(self, key, *, events_in, events_kept, bad_lines, bytes_out, sha256) -> None:
31
+ with self.db:
32
+ self.db.execute(
33
+ "INSERT OR REPLACE INTO archive_hours VALUES (?, 'done', ?, ?, ?, ?, ?, NULL, ?)",
34
+ (key, events_in, events_kept, bad_lines, bytes_out, sha256, _now()),
35
+ )
36
+
37
+ def mark_failed(self, key: str, error: str) -> None:
38
+ with self.db:
39
+ self.db.execute(
40
+ "INSERT OR REPLACE INTO archive_hours VALUES (?, 'failed', NULL, NULL, NULL, NULL, NULL, ?, ?)",
41
+ (key, error[:500], _now()),
42
+ )
43
+
44
+ def close(self) -> None:
45
+ self.db.close()
@@ -0,0 +1,83 @@
1
+ import zlib
2
+ from collections.abc import Iterable, Iterator
3
+ from datetime import date
4
+
5
+ import httpx
6
+ import orjson
7
+
8
+ ARCHIVE_BASE = "https://data.gharchive.org"
9
+
10
+
11
+ class ArchiveStreamError(Exception):
12
+ pass
13
+
14
+
15
+ def normalize_hour_key(key: str) -> str:
16
+ parts = key.split("-")
17
+ if len(parts) != 4:
18
+ raise ValueError(f"Hour key must look like 2026-10-01-15, got {key!r}")
19
+ try:
20
+ day = date(int(parts[0]), int(parts[1]), int(parts[2]))
21
+ hour = int(parts[3])
22
+ except ValueError as exc:
23
+ raise ValueError(f"Hour key must look like 2026-10-01-15, got {key!r}") from exc
24
+ if not 0 <= hour <= 23:
25
+ raise ValueError(f"Hour must be 0-23, got {hour}")
26
+ return f"{day.isoformat()}-{hour}"
27
+
28
+
29
+ def parse_hour_key(key: str) -> tuple[str, int]:
30
+ norm = normalize_hour_key(key)
31
+ day, hour = norm.rsplit("-", 1)
32
+ return day, int(hour)
33
+
34
+
35
+ def hour_url(key: str) -> str:
36
+ return f"{ARCHIVE_BASE}/{normalize_hour_key(key)}.json.gz"
37
+
38
+
39
+ def fetch_chunks(url: str, client: httpx.Client | None = None, chunk_size: int = 1 << 20) -> Iterator[bytes]:
40
+ own = client is None
41
+ client = client or httpx.Client(timeout=httpx.Timeout(60.0, connect=15.0), follow_redirects=True)
42
+ try:
43
+ with client.stream("GET", url) as resp:
44
+ resp.raise_for_status()
45
+ # Raw bytes, so httpx never tries to unzip for us.
46
+ yield from resp.iter_raw(chunk_size)
47
+ finally:
48
+ if own:
49
+ client.close()
50
+
51
+
52
+ def iter_lines(chunks: Iterable[bytes]) -> Iterator[bytes]:
53
+ decomp = zlib.decompressobj(zlib.MAX_WBITS | 16)
54
+ buffer = b""
55
+ try:
56
+ for chunk in chunks:
57
+ data = decomp.decompress(chunk)
58
+ # A file can hold several gzip parts back to back.
59
+ while decomp.unused_data:
60
+ leftover = decomp.unused_data
61
+ decomp = zlib.decompressobj(zlib.MAX_WBITS | 16)
62
+ data += decomp.decompress(leftover)
63
+ buffer += data
64
+ *lines, buffer = buffer.split(b"\n")
65
+ yield from (line for line in lines if line)
66
+ buffer += decomp.flush()
67
+ except zlib.error as exc:
68
+ raise ArchiveStreamError(f"Archive file is corrupt: {exc}") from exc
69
+ if not decomp.eof:
70
+ raise ArchiveStreamError("Archive file ended early (download cut off).")
71
+ if buffer.strip():
72
+ yield buffer
73
+
74
+
75
+ def iter_events(chunks: Iterable[bytes], stats: dict) -> Iterator[dict]:
76
+ for line in iter_lines(chunks):
77
+ try:
78
+ event = orjson.loads(line)
79
+ except orjson.JSONDecodeError:
80
+ stats["bad_lines"] += 1
81
+ continue
82
+ stats["events_in"] += 1
83
+ yield event
@@ -0,0 +1 @@
1
+ """Daily samplers that turn the crawled repo list into open-source benchmarks."""
@@ -0,0 +1,135 @@
1
+ import argparse
2
+ import gzip
3
+ import json
4
+ import os
5
+ import re
6
+ import sys
7
+ from datetime import datetime, timedelta, timezone
8
+ from pathlib import Path
9
+
10
+ import pyarrow.parquet as pq
11
+
12
+ from mergescout.bench.ci import TokenPool, aggregate, fetch_all, todays_share
13
+ from mergescout.bench.deploys import aggregate_deploys, repo_deploys
14
+ from mergescout.bench.stars import aggregate_stars, read_stars
15
+ from mergescout.config import load_config
16
+ from mergescout.github.releases import Releases
17
+ from mergescout.store.sync import STATE_TAG, safe_extract
18
+
19
+ LATEST_TAG = "bench-latest"
20
+ DAILY = re.compile(r"^(?P<kind>[a-z]+)-(\d{4}-\d{2}-\d{2})\.ndjson\.gz$")
21
+
22
+
23
+ def week_tag(day: datetime) -> str:
24
+ # One release per ISO week keeps every release small.
25
+ return f"bench-{day:%G-W%V}"
26
+
27
+
28
+ def load_repos(basics: Path, now: datetime, active_days: int = 30, min_stars: int = 100) -> list[dict]:
29
+ """Repos from the discovery sweep that pushed recently: the only ones with push runs to read."""
30
+ since, best = now - timedelta(days=active_days), {}
31
+ for path in sorted(basics.rglob("*.parquet")):
32
+ for r in pq.read_table(path, columns=["name", "stars", "language", "pushed_at"]).to_pylist():
33
+ if not r["name"] or (r["stars"] or 0) < min_stars or not r["pushed_at"] or r["pushed_at"] < since:
34
+ continue
35
+ key = r["name"].lower()
36
+ if key not in best or (r["stars"] or 0) > best[key]["stars"]:
37
+ best[key] = {"name": r["name"], "language": r["language"], "stars": r["stars"]}
38
+ return sorted(best.values(), key=lambda r: r["name"].lower())
39
+
40
+
41
+ def _read(path: Path) -> list[dict]:
42
+ with gzip.open(path, "rt", encoding="utf-8") as f:
43
+ return [json.loads(line) for line in f if line.strip()]
44
+
45
+
46
+ def publish(data_dir: Path, releases, records: list[dict], now: datetime, days: int = 35, log=print, kind: str = "ci",
47
+ build=aggregate) -> dict:
48
+ """Store today's sample privately, then rebuild the benchmarks from the last five weeks of samples of that kind."""
49
+ folder = data_dir / "bench"
50
+ folder.mkdir(parents=True, exist_ok=True)
51
+ name = f"{kind}-{now:%Y-%m-%d}.ndjson.gz"
52
+ today = folder / name
53
+ # A second run on the same day adds to that day's file instead of replacing it.
54
+ release = releases.get(week_tag(now))
55
+ earlier = next((a for a in releases.assets(release) if a["name"] == name), None) if release else None
56
+ merged = {}
57
+ if earlier:
58
+ releases.download_asset(earlier, today)
59
+ merged = {r["repo"].lower(): r for r in _read(today)}
60
+ merged.update({r["repo"].lower(): r for r in records})
61
+ records = list(merged.values())
62
+ with gzip.open(today, "wt", encoding="utf-8") as f:
63
+ f.writelines(json.dumps(r) + "\n" for r in records)
64
+ releases.upload(week_tag(now), today)
65
+ first = (now - timedelta(days=days)).strftime("%Y-%m-%d")
66
+ every = list(records)
67
+ for tag in sorted({week_tag(now - timedelta(days=d)) for d in range(days + 1)}):
68
+ release = releases.get(tag)
69
+ for asset in releases.assets(release) if release else []:
70
+ m = DAILY.match(asset["name"])
71
+ if not m or m["kind"] != kind or asset["name"] == name or m[2] < first:
72
+ continue
73
+ local = folder / asset["name"]
74
+ if not local.exists():
75
+ releases.download_asset(asset, local)
76
+ every += _read(local)
77
+ agg = build(every, now)
78
+ out = folder / f"{kind}-benchmarks.json"
79
+ out.write_text(json.dumps(agg, indent=1, sort_keys=True), encoding="utf-8")
80
+ releases.upload(LATEST_TAG, out)
81
+ log(f"benchmarks: {agg['repos']} repos, {len(agg['groups'])} groups with enough repos")
82
+ return agg
83
+
84
+
85
+ def main(argv: list[str] | None = None) -> int:
86
+ parser = argparse.ArgumentParser(prog="python -m mergescout.bench",
87
+ description="Sample CI statistics of crawled repos into open benchmarks.")
88
+ sub = parser.add_subparsers(dest="cmd", required=True)
89
+ ci = sub.add_parser("ci", help="Read today's share of the active repos' push runs and rebuild the benchmarks")
90
+ stars = sub.add_parser("stars", help="Read today's share of the active repos' weekly star history and rebuild its benchmarks")
91
+ deploys = sub.add_parser("deploys", help="Read today's share of the active repos' deployments and releases (2 requests "
92
+ "each) and rebuild the shipping-frequency benchmarks")
93
+ for p in (ci, stars, deploys):
94
+ p.add_argument("--workers", type=int, default=10)
95
+ p.add_argument("--max-wait", type=int, default=3600, help="seconds to wait in total for hourly limits to reset")
96
+ p.add_argument("--limit", type=int, default=0, help="read at most this many repos (0: all of today's share)")
97
+ parser.add_argument("--repo", default=os.environ.get("MS_STORE_REPO") or os.environ.get("GITHUB_REPOSITORY"),
98
+ help="owner/name of the repo whose releases hold the data")
99
+ args = parser.parse_args(argv)
100
+ store_token = os.environ.get("MS_RELEASE_TOKEN")
101
+ tokens = [os.environ.get(k) for k in ("GITHUB_TOKEN", "MS_APP_TOKEN", "MS_ACTIONS_TOKEN")]
102
+ if not store_token or not args.repo or not any(tokens):
103
+ print("Set MS_RELEASE_TOKEN, MS_STORE_REPO (or GITHUB_REPOSITORY) and GITHUB_TOKEN.", file=sys.stderr)
104
+ return 1
105
+ log = lambda msg: print(msg, flush=True) # noqa: E731
106
+ now, data_dir = datetime.now(timezone.utc), load_config().data_dir
107
+ releases = Releases(store_token, args.repo)
108
+ basics = data_dir / "discover" / "basics"
109
+ if not basics.exists():
110
+ # Only the sweep results are needed, never the crawl state.
111
+ bundle = data_dir / "outbox" / "discover.tar"
112
+ if not releases.download(STATE_TAG, "discover.tar", bundle):
113
+ print("No sweep results in the releases yet.", file=sys.stderr)
114
+ return 1
115
+ safe_extract(bundle, data_dir)
116
+ bundle.unlink()
117
+ repos = load_repos(basics, now)
118
+ share = todays_share([r["name"] for r in repos], now)
119
+ picked = set(share[:args.limit] if args.limit else share)
120
+ todo = [r for r in repos if r["name"] in picked]
121
+ log(f"{len(repos)} active repos; today's share: {len(todo)}")
122
+ pool = TokenPool(tokens, max_wait=args.max_wait)
123
+ readers = {"ci": (None, aggregate, "push runs"), "stars": (read_stars, aggregate_stars, "star history"),
124
+ "deploys": (repo_deploys, aggregate_deploys, "deployment and release history")}
125
+ read, build, what = readers[args.cmd]
126
+ got = fetch_all(pool, todo, now, workers=args.workers, log=log, read=read)
127
+ # The requests are counted so each sampler is seen to stay inside the day's REST budget.
128
+ log(f"{len(got['records'])} repos with {what}, {got['requests']} requests "
129
+ f"({got['requests'] / max(len(todo), 1):.2f} a repo), {'complete' if got['complete'] else 'stopped at the hourly limits'}")
130
+ publish(data_dir, releases, got["records"], now, log=log, kind=args.cmd, build=build)
131
+ return 0
132
+
133
+
134
+ if __name__ == "__main__":
135
+ raise SystemExit(main())