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.
- mergescout/__init__.py +1 -0
- mergescout/archive/__init__.py +0 -0
- mergescout/archive/__main__.py +30 -0
- mergescout/archive/convert.py +83 -0
- mergescout/archive/events.py +55 -0
- mergescout/archive/manifest.py +45 -0
- mergescout/archive/stream.py +83 -0
- mergescout/bench/__init__.py +1 -0
- mergescout/bench/__main__.py +135 -0
- mergescout/bench/ci.py +257 -0
- mergescout/bench/deploys.py +115 -0
- mergescout/bench/matched.py +22 -0
- mergescout/bench/prior.py +42 -0
- mergescout/bench/report.py +230 -0
- mergescout/bench/shared.py +112 -0
- mergescout/bench/stars.py +57 -0
- mergescout/cli.py +89 -0
- mergescout/config.py +35 -0
- mergescout/crawl/__init__.py +0 -0
- mergescout/crawl/__main__.py +97 -0
- mergescout/crawl/chain.py +34 -0
- mergescout/crawl/fetch.py +146 -0
- mergescout/crawl/outcomes.py +69 -0
- mergescout/crawl/queries.py +250 -0
- mergescout/crawl/runner.py +176 -0
- mergescout/crawl/seed.py +77 -0
- mergescout/crawl/state.py +169 -0
- mergescout/crawl/writer.py +87 -0
- mergescout/discover/__init__.py +0 -0
- mergescout/discover/__main__.py +58 -0
- mergescout/discover/sweep.py +286 -0
- mergescout/github/__init__.py +0 -0
- mergescout/github/graphql.py +202 -0
- mergescout/github/identities.py +35 -0
- mergescout/github/releases.py +137 -0
- mergescout/labels.py +20 -0
- mergescout/mcp_server.py +228 -0
- mergescout/measure/__init__.py +0 -0
- mergescout/measure/__main__.py +75 -0
- mergescout/measure/api_cost.py +91 -0
- mergescout/measure/count_repos.py +30 -0
- mergescout/measure/jsonl.py +14 -0
- mergescout/measure/report.py +56 -0
- mergescout/measure/sample.py +37 -0
- mergescout/owners.py +22 -0
- mergescout/setup_check.py +64 -0
- mergescout/signals/__init__.py +0 -0
- mergescout/signals/__main__.py +96 -0
- mergescout/signals/bump_ledger.py +258 -0
- mergescout/signals/bumps.py +104 -0
- mergescout/signals/check.py +141 -0
- mergescout/signals/community_bench.py +149 -0
- mergescout/signals/delivery_bench.py +103 -0
- mergescout/signals/deps_map.py +261 -0
- mergescout/signals/deps_trends.py +117 -0
- mergescout/signals/evidence.py +94 -0
- mergescout/signals/exploits.py +43 -0
- mergescout/signals/flow_bench.py +258 -0
- mergescout/signals/identity.py +73 -0
- mergescout/signals/issues.py +76 -0
- mergescout/signals/labels.py +79 -0
- mergescout/signals/osv.py +232 -0
- mergescout/signals/owners_watch.py +80 -0
- mergescout/signals/repo_stats.py +96 -0
- mergescout/signals/rules.py +70 -0
- mergescout/signals/silver.py +33 -0
- mergescout/signals/top_packages.py +26 -0
- mergescout/signals/triage.py +71 -0
- mergescout/signals/triage_math.py +120 -0
- mergescout/signals/walls.py +91 -0
- mergescout/snapshot/__init__.py +0 -0
- mergescout/snapshot/__main__.py +215 -0
- mergescout/snapshot/build.py +147 -0
- mergescout/snapshot/publish.py +70 -0
- mergescout/snapshot/schema.py +34 -0
- mergescout/snapshot/score.py +30 -0
- mergescout/store/__init__.py +0 -0
- mergescout/store/__main__.py +46 -0
- mergescout/store/sync.py +148 -0
- mergescout/timeutil.py +11 -0
- mergescout/tool/__init__.py +0 -0
- mergescout/tool/action_pack.py +62 -0
- mergescout/tool/live.py +201 -0
- mergescout/tool/queries.py +293 -0
- mergescout/tool/refs.py +31 -0
- mergescout/tool/store.py +129 -0
- mergescout/tool/tracking.py +83 -0
- stackvitals/__init__.py +2 -0
- stackvitals/__main__.py +4 -0
- stackvitals/ci/__init__.py +1 -0
- stackvitals/ci/action.py +574 -0
- stackvitals/ci/analyze.py +577 -0
- stackvitals/ci/api.py +218 -0
- stackvitals/ci/automation.py +212 -0
- stackvitals/ci/bench.py +195 -0
- stackvitals/ci/burnin.py +102 -0
- stackvitals/ci/cli.py +570 -0
- stackvitals/ci/collect.py +239 -0
- stackvitals/ci/cost.py +107 -0
- stackvitals/ci/coverage.py +155 -0
- stackvitals/ci/feedback.py +699 -0
- stackvitals/ci/fingerprint.py +220 -0
- stackvitals/ci/gitlab.py +121 -0
- stackvitals/ci/gitlab_action.py +125 -0
- stackvitals/ci/impact.py +125 -0
- stackvitals/ci/investigate.py +59 -0
- stackvitals/ci/kinds.py +52 -0
- stackvitals/ci/monitors.py +105 -0
- stackvitals/ci/owners.py +116 -0
- stackvitals/ci/quarantine_runtime.py +90 -0
- stackvitals/ci/report.py +435 -0
- stackvitals/ci/retry.py +67 -0
- stackvitals/ci/selfheal.py +102 -0
- stackvitals/ci/shards.py +120 -0
- stackvitals/ci/share.py +59 -0
- stackvitals/ci/store.py +93 -0
- stackvitals/ci/testreports.py +351 -0
- stackvitals/ci/tickets.py +106 -0
- stackvitals/ci/track_record.json +11 -0
- stackvitals/ci/triage.py +130 -0
- stackvitals/ci/triggers.py +147 -0
- stackvitals/ci/upload.py +85 -0
- stackvitals/ci/validate.py +91 -0
- stackvitals/cli.py +111 -0
- stackvitals/codeowners.py +69 -0
- stackvitals/community/__init__.py +0 -0
- stackvitals/community/action.py +140 -0
- stackvitals/community/affiliation.py +51 -0
- stackvitals/community/analyze.py +507 -0
- stackvitals/community/build.py +56 -0
- stackvitals/community/chaoss.py +70 -0
- stackvitals/community/cli.py +79 -0
- stackvitals/community/collect.py +240 -0
- stackvitals/community/context.py +54 -0
- stackvitals/community/report.py +250 -0
- stackvitals/connectors/__init__.py +244 -0
- stackvitals/connectors/ai_spend.py +190 -0
- stackvitals/connectors/base.py +82 -0
- stackvitals/connectors/check.py +686 -0
- stackvitals/connectors/copilot.py +104 -0
- stackvitals/connectors/discord.py +103 -0
- stackvitals/connectors/discourse.py +117 -0
- stackvitals/connectors/incident_io.py +106 -0
- stackvitals/connectors/jira.py +210 -0
- stackvitals/connectors/linear.py +140 -0
- stackvitals/connectors/pagerduty.py +96 -0
- stackvitals/connectors/slack.py +110 -0
- stackvitals/connectors/sonar.py +77 -0
- stackvitals/connectors/store.py +120 -0
- stackvitals/connectors/tracker.py +108 -0
- stackvitals/connectors/writeback.py +105 -0
- stackvitals/contribute/__init__.py +1 -0
- stackvitals/contribute/brief.py +297 -0
- stackvitals/contribute/cli.py +64 -0
- stackvitals/core/__init__.py +1 -0
- stackvitals/core/actors.py +103 -0
- stackvitals/core/agent_ledger.py +120 -0
- stackvitals/core/agreements.py +95 -0
- stackvitals/core/ai_outcomes.py +276 -0
- stackvitals/core/alerts.py +224 -0
- stackvitals/core/appdata.py +112 -0
- stackvitals/core/bucket.py +190 -0
- stackvitals/core/bugs.py +43 -0
- stackvitals/core/calendar.py +34 -0
- stackvitals/core/capex.py +57 -0
- stackvitals/core/change_risk.py +181 -0
- stackvitals/core/changes.py +133 -0
- stackvitals/core/chat.py +143 -0
- stackvitals/core/config.py +373 -0
- stackvitals/core/dataflow.py +168 -0
- stackvitals/core/decisions.py +275 -0
- stackvitals/core/delivery.py +954 -0
- stackvitals/core/delivery_peers.py +98 -0
- stackvitals/core/delivery_report.py +429 -0
- stackvitals/core/deliverycli.py +147 -0
- stackvitals/core/devlake_dora.sql +485 -0
- stackvitals/core/digest.py +333 -0
- stackvitals/core/explaincli.py +107 -0
- stackvitals/core/explore.py +296 -0
- stackvitals/core/export.py +342 -0
- stackvitals/core/exportcli.py +117 -0
- stackvitals/core/findings.py +350 -0
- stackvitals/core/health.py +94 -0
- stackvitals/core/identity.py +82 -0
- stackvitals/core/insights.py +365 -0
- stackvitals/core/insightscli.py +43 -0
- stackvitals/core/installdoc.py +69 -0
- stackvitals/core/investment.py +187 -0
- stackvitals/core/joins.py +43 -0
- stackvitals/core/llmsdoc.py +40 -0
- stackvitals/core/merge_queue.py +70 -0
- stackvitals/core/metrics.py +109 -0
- stackvitals/core/model.py +285 -0
- stackvitals/core/projectcli.py +66 -0
- stackvitals/core/propose.py +86 -0
- stackvitals/core/pulse.py +85 -0
- stackvitals/core/recorded.py +145 -0
- stackvitals/core/releases.py +39 -0
- stackvitals/core/requests.py +528 -0
- stackvitals/core/resync.py +35 -0
- stackvitals/core/schema.py +205 -0
- stackvitals/core/services.py +203 -0
- stackvitals/core/settings.py +96 -0
- stackvitals/core/sqlcli.py +111 -0
- stackvitals/core/stats.py +222 -0
- stackvitals/core/store.py +125 -0
- stackvitals/core/summary.py +104 -0
- stackvitals/core/time.py +29 -0
- stackvitals/core/tomledit.py +149 -0
- stackvitals/core/uicli.py +138 -0
- stackvitals/core/webapp.py +79 -0
- stackvitals/core/what_changed.py +76 -0
- stackvitals/core/workspace.py +194 -0
- stackvitals/deps/__init__.py +1 -0
- stackvitals/deps/action.py +299 -0
- stackvitals/deps/actions_vulns.py +89 -0
- stackvitals/deps/cli.py +77 -0
- stackvitals/deps/dossier.py +341 -0
- stackvitals/deps/evidence.py +44 -0
- stackvitals/deps/fixplan.py +40 -0
- stackvitals/deps/guarddog.py +32 -0
- stackvitals/deps/hygiene.py +47 -0
- stackvitals/deps/imports.py +113 -0
- stackvitals/deps/lockfiles.py +198 -0
- stackvitals/deps/lookalike.py +80 -0
- stackvitals/deps/reach.py +59 -0
- stackvitals/deps/release.py +110 -0
- stackvitals/deps/runtimes.py +161 -0
- stackvitals/deps/supply.py +454 -0
- stackvitals/deps/vulns.py +588 -0
- stackvitals/editors/__init__.py +40 -0
- stackvitals/editors/vscode/extension.js +101 -0
- stackvitals/editors/vscode/marks.js +92 -0
- stackvitals/editors/vscode/package.json +26 -0
- stackvitals/flow/__init__.py +1 -0
- stackvitals/flow/action.py +178 -0
- stackvitals/flow/analyze.py +418 -0
- stackvitals/flow/automate.py +72 -0
- stackvitals/flow/build.py +46 -0
- stackvitals/flow/cli.py +93 -0
- stackvitals/flow/code_hosts.py +146 -0
- stackvitals/flow/collect.py +451 -0
- stackvitals/flow/gitlab_mr.py +75 -0
- stackvitals/flow/knowledge.py +202 -0
- stackvitals/flow/peers.py +90 -0
- stackvitals/flow/preflight.py +210 -0
- stackvitals/flow/report.py +253 -0
- stackvitals/flow/rules.py +135 -0
- stackvitals/flow/store.py +108 -0
- stackvitals/flow/survival.py +84 -0
- stackvitals/flow/triage.py +62 -0
- stackvitals/mcp_server.py +670 -0
- stackvitals/portal/__init__.py +0 -0
- stackvitals/portal/action.py +205 -0
- stackvitals/portal/batch.py +80 -0
- stackvitals/portal/campaigns.py +104 -0
- stackvitals/portal/catalog.py +104 -0
- stackvitals/portal/checks.py +63 -0
- stackvitals/portal/cli.py +172 -0
- stackvitals/portal/descriptor.py +105 -0
- stackvitals/portal/export.py +43 -0
- stackvitals/portal/github.py +48 -0
- stackvitals/portal/golden.py +95 -0
- stackvitals/portal/history.py +30 -0
- stackvitals/portal/inventory.py +264 -0
- stackvitals/portal/owners.py +78 -0
- stackvitals/portal/pages.py +153 -0
- stackvitals/portal/rollup.py +32 -0
- stackvitals/portal/scorecard.py +366 -0
- stackvitals/security/__init__.py +2 -0
- stackvitals/security/probes.py +154 -0
- stackvitals/security/repo_checks.py +132 -0
- stackvitals/security/workflows.py +111 -0
- stackvitals/shell.py +107 -0
- stackvitals/site.py +51 -0
- stackvitals/web/app/LICENSES.md +428 -0
- stackvitals/web/app/boot.js +1 -0
- stackvitals/web/app/chunks/AddRepo-KRPZXWCY.js +1 -0
- stackvitals/web/app/chunks/SetupRunning-XWFQAKRZ.js +1 -0
- stackvitals/web/app/chunks/accuracy-data-OQTDXI7Y.js +1 -0
- stackvitals/web/app/chunks/agents-data-BAL6QAOF.js +11 -0
- stackvitals/web/app/chunks/chunk-24LFKFZ6.js +1 -0
- stackvitals/web/app/chunks/chunk-3G4SQNUC.js +1 -0
- stackvitals/web/app/chunks/chunk-3IXWEOIJ.js +154 -0
- stackvitals/web/app/chunks/chunk-3ROQFHPV.js +1 -0
- stackvitals/web/app/chunks/chunk-45PGAQRO.js +8 -0
- stackvitals/web/app/chunks/chunk-4H53OYLG.js +1 -0
- stackvitals/web/app/chunks/chunk-4LUI7O5P.js +1 -0
- stackvitals/web/app/chunks/chunk-4SQPQ5TB.js +1 -0
- stackvitals/web/app/chunks/chunk-4WKQTWUC.js +2 -0
- stackvitals/web/app/chunks/chunk-576YHDEK.js +8 -0
- stackvitals/web/app/chunks/chunk-6BDBXA6V.js +1 -0
- stackvitals/web/app/chunks/chunk-6RLVD2DT.js +1 -0
- stackvitals/web/app/chunks/chunk-763NH7PP.js +1 -0
- stackvitals/web/app/chunks/chunk-76WAVKUG.js +1 -0
- stackvitals/web/app/chunks/chunk-7II2BD5F.js +1 -0
- stackvitals/web/app/chunks/chunk-7TJCBOAY.js +1 -0
- stackvitals/web/app/chunks/chunk-ASYNLVOP.js +1 -0
- stackvitals/web/app/chunks/chunk-CTKDYZMG.js +1 -0
- stackvitals/web/app/chunks/chunk-CVB26NMC.js +1 -0
- stackvitals/web/app/chunks/chunk-G7NGB5UX.js +1 -0
- stackvitals/web/app/chunks/chunk-GPVLJRZD.js +1 -0
- stackvitals/web/app/chunks/chunk-GQYFN4PE.js +75 -0
- stackvitals/web/app/chunks/chunk-GT4EAKAO.js +2 -0
- stackvitals/web/app/chunks/chunk-IYMBFPCV.js +1 -0
- stackvitals/web/app/chunks/chunk-LAAGCPU3.js +1 -0
- stackvitals/web/app/chunks/chunk-MAPYGK3P.js +1 -0
- stackvitals/web/app/chunks/chunk-P5TXDD45.js +1 -0
- stackvitals/web/app/chunks/chunk-PABICGBL.js +1 -0
- stackvitals/web/app/chunks/chunk-RPXHAG6F.js +1 -0
- stackvitals/web/app/chunks/chunk-RQZPSJ2Y.js +1 -0
- stackvitals/web/app/chunks/chunk-S2ZBMEVA.js +1 -0
- stackvitals/web/app/chunks/chunk-S654BBEP.js +6 -0
- stackvitals/web/app/chunks/chunk-T7QVTNXF.js +1 -0
- stackvitals/web/app/chunks/chunk-THFUOCPC.js +273 -0
- stackvitals/web/app/chunks/chunk-TO4VVRZ7.js +1 -0
- stackvitals/web/app/chunks/chunk-VQ5YXO54.js +1 -0
- stackvitals/web/app/chunks/chunk-VWKSMFWJ.js +1 -0
- stackvitals/web/app/chunks/chunk-Y6LUXDM6.js +1 -0
- stackvitals/web/app/chunks/chunk-YTANGYYH.js +1 -0
- stackvitals/web/app/chunks/connect-KONYWYTT.js +1 -0
- stackvitals/web/app/chunks/connections-data-2DYS4UVW.js +32 -0
- stackvitals/web/app/chunks/logos-N5IINUAM.js +1 -0
- stackvitals/web/app/chunks/more-KMJMJQD3.js +10 -0
- stackvitals/web/app/chunks/more-PC6FVS3P.js +1 -0
- stackvitals/web/app/chunks/more-RCIVCUDE.js +2 -0
- stackvitals/web/app/chunks/more-ZKKRY6NO.css +1 -0
- stackvitals/web/app/chunks/sealbox-35YL4EUH.js +3 -0
- stackvitals/web/app/chunks/session-DYYTOE2K.js +1 -0
- stackvitals/web/app/chunks/settingsApply-2HRJEZYM.js +1 -0
- stackvitals/web/app/chunks/settingsReview-GX2FZQ4M.js +3 -0
- stackvitals/web/app/chunks/uPlot.esm-OIWT3AHJ.js +3 -0
- stackvitals/web/app/demo/ci/impact.json +1 -0
- stackvitals/web/app/demo/ci/quarantine.json +1 -0
- stackvitals/web/app/demo/ci/tickets.json +1 -0
- stackvitals/web/app/demo/mobile/alerts.json +1 -0
- stackvitals/web/app/demo/mobile/app.json +1 -0
- stackvitals/web/app/demo/mobile/community.json +1 -0
- stackvitals/web/app/demo/mobile/connectors.json +1 -0
- stackvitals/web/app/demo/mobile/data.json +1 -0
- stackvitals/web/app/demo/mobile/decisions.json +1 -0
- stackvitals/web/app/demo/mobile/delivery.json +1 -0
- stackvitals/web/app/demo/mobile/deps.json +1 -0
- stackvitals/web/app/demo/mobile/flow.json +1 -0
- stackvitals/web/app/demo/mobile/health.json +1 -0
- stackvitals/web/app/demo/mobile/insights.json +1 -0
- stackvitals/web/app/demo/mobile/pulse.json +1 -0
- stackvitals/web/app/demo/mobile/security.json +1 -0
- stackvitals/web/app/demo/mobile/summary/ci_jobs.jsonl +5 -0
- stackvitals/web/app/demo/mobile/summary/devlake_project_pr_metrics.jsonl +14 -0
- stackvitals/web/app/demo/mobile/summary/devlake_pull_requests.jsonl +14 -0
- stackvitals/web/app/demo/mobile/summary/manifest.json +1 -0
- stackvitals/web/app/demo/mobile/summary/prs.jsonl +19 -0
- stackvitals/web/app/demo/mobile/summary/tests.jsonl +1 -0
- stackvitals/web/app/demo/mobile/supply.json +1 -0
- stackvitals/web/app/demo/mobile/vulns.json +1 -0
- stackvitals/web/app/demo/payments-api/alerts.json +1 -0
- stackvitals/web/app/demo/payments-api/app.json +1 -0
- stackvitals/web/app/demo/payments-api/community.json +1 -0
- stackvitals/web/app/demo/payments-api/connectors.json +1 -0
- stackvitals/web/app/demo/payments-api/data.json +1 -0
- stackvitals/web/app/demo/payments-api/decisions.json +1 -0
- stackvitals/web/app/demo/payments-api/delivery.json +1 -0
- stackvitals/web/app/demo/payments-api/deps.json +1 -0
- stackvitals/web/app/demo/payments-api/flow.json +1 -0
- stackvitals/web/app/demo/payments-api/health.json +1 -0
- stackvitals/web/app/demo/payments-api/insights.json +1 -0
- stackvitals/web/app/demo/payments-api/pulse.json +1 -0
- stackvitals/web/app/demo/payments-api/security.json +1 -0
- stackvitals/web/app/demo/payments-api/summary/ci_jobs.jsonl +5 -0
- stackvitals/web/app/demo/payments-api/summary/devlake_project_pr_metrics.jsonl +14 -0
- stackvitals/web/app/demo/payments-api/summary/devlake_pull_requests.jsonl +14 -0
- stackvitals/web/app/demo/payments-api/summary/manifest.json +1 -0
- stackvitals/web/app/demo/payments-api/summary/prs.jsonl +22 -0
- stackvitals/web/app/demo/payments-api/summary/tests.jsonl +3 -0
- stackvitals/web/app/demo/payments-api/supply.json +1 -0
- stackvitals/web/app/demo/payments-api/vulns.json +1 -0
- stackvitals/web/app/demo/portal.json +1 -0
- stackvitals/web/app/demo/settings.json +1 -0
- stackvitals/web/app/demo/web-app/alerts.json +1 -0
- stackvitals/web/app/demo/web-app/app.json +1 -0
- stackvitals/web/app/demo/web-app/community.json +1 -0
- stackvitals/web/app/demo/web-app/connectors.json +1 -0
- stackvitals/web/app/demo/web-app/data.json +1 -0
- stackvitals/web/app/demo/web-app/decisions.json +1 -0
- stackvitals/web/app/demo/web-app/delivery.json +1 -0
- stackvitals/web/app/demo/web-app/deps.json +1 -0
- stackvitals/web/app/demo/web-app/flow.json +1 -0
- stackvitals/web/app/demo/web-app/health.json +1 -0
- stackvitals/web/app/demo/web-app/insights.json +1 -0
- stackvitals/web/app/demo/web-app/pulse.json +1 -0
- stackvitals/web/app/demo/web-app/security.json +1 -0
- stackvitals/web/app/demo/web-app/summary/ci_jobs.jsonl +5 -0
- stackvitals/web/app/demo/web-app/summary/devlake_project_pr_metrics.jsonl +14 -0
- stackvitals/web/app/demo/web-app/summary/devlake_pull_requests.jsonl +14 -0
- stackvitals/web/app/demo/web-app/summary/manifest.json +1 -0
- stackvitals/web/app/demo/web-app/summary/prs.jsonl +18 -0
- stackvitals/web/app/demo/web-app/summary/tests.jsonl +2 -0
- stackvitals/web/app/demo/web-app/supply.json +1 -0
- stackvitals/web/app/demo/web-app/vulns.json +1 -0
- stackvitals/web/app/fonts/plex-mono-400.woff2 +0 -0
- stackvitals/web/app/fonts/plex-sans-400.woff2 +0 -0
- stackvitals/web/app/fonts/plex-sans-600.woff2 +0 -0
- stackvitals/web/app/fonts/plex-sans-condensed-700.woff2 +0 -0
- stackvitals/web/app/help/accuracy.json +1 -0
- stackvitals/web/app/help/connectors.json +1 -0
- stackvitals/web/app/help/getting-started.json +1 -0
- stackvitals/web/app/help/index.json +1 -0
- stackvitals/web/app/help/modules.json +1 -0
- stackvitals/web/app/help/privacy-and-security.json +1 -0
- stackvitals/web/app/help/reference.json +1 -0
- stackvitals/web/app/main.css +1 -0
- stackvitals/web/app/main.js +1 -0
- stackvitals/web/app/pages/about.css +1 -0
- stackvitals/web/app/pages/about.js +1 -0
- stackvitals/web/app/pages/activity.css +1 -0
- stackvitals/web/app/pages/activity.js +1 -0
- stackvitals/web/app/pages/agents.css +1 -0
- stackvitals/web/app/pages/agents.js +2 -0
- stackvitals/web/app/pages/ci.css +1 -0
- stackvitals/web/app/pages/ci.js +1 -0
- stackvitals/web/app/pages/community.css +1 -0
- stackvitals/web/app/pages/community.js +1 -0
- stackvitals/web/app/pages/connections.css +1 -0
- stackvitals/web/app/pages/connections.js +6 -0
- stackvitals/web/app/pages/delivery.css +1 -0
- stackvitals/web/app/pages/delivery.js +45 -0
- stackvitals/web/app/pages/deps.css +1 -0
- stackvitals/web/app/pages/deps.js +7 -0
- stackvitals/web/app/pages/explore.css +1 -0
- stackvitals/web/app/pages/explore.js +7 -0
- stackvitals/web/app/pages/health.css +1 -0
- stackvitals/web/app/pages/health.js +1 -0
- stackvitals/web/app/pages/help.css +1 -0
- stackvitals/web/app/pages/help.js +6 -0
- stackvitals/web/app/pages/insights.css +1 -0
- stackvitals/web/app/pages/insights.js +1 -0
- stackvitals/web/app/pages/investment.css +1 -0
- stackvitals/web/app/pages/investment.js +3 -0
- stackvitals/web/app/pages/overview.css +1 -0
- stackvitals/web/app/pages/overview.js +1 -0
- stackvitals/web/app/pages/portal.css +1 -0
- stackvitals/web/app/pages/portal.js +51 -0
- stackvitals/web/app/pages/prs.css +1 -0
- stackvitals/web/app/pages/prs.js +4 -0
- stackvitals/web/app/pages/settings.css +1 -0
- stackvitals/web/app/pages/settings.js +1 -0
- stackvitals/web/app/pages/setup.css +1 -0
- stackvitals/web/app/pages/setup.js +2 -0
- stackvitals/web/app/pages/standing.css +1 -0
- stackvitals/web/app/pages/standing.js +1 -0
- stackvitals/web/app/pages/welcome-sheet.js +1 -0
- stackvitals/web/app/pages/welcome.js +1 -0
- stackvitals/web/app/settings-workflow.json +1 -0
- stackvitals/web/index.html +34 -0
- stackvitals/web/site/index.html +108 -0
- stackvitals/web/site/integrations.html +15 -0
- stackvitals/web/site/setup.html +15 -0
- stackvitals/web/site/site.css +68 -0
- stackvitals-0.1.0.dist-info/METADATA +134 -0
- stackvitals-0.1.0.dist-info/RECORD +465 -0
- stackvitals-0.1.0.dist-info/WHEEL +4 -0
- stackvitals-0.1.0.dist-info/entry_points.txt +3 -0
- stackvitals-0.1.0.dist-info/licenses/LICENSE +202 -0
- 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())
|