stage-cli 1.1.1__tar.gz → 1.1.3__tar.gz

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 (294) hide show
  1. {stage_cli-1.1.1 → stage_cli-1.1.3}/.gitignore +0 -2
  2. {stage_cli-1.1.1 → stage_cli-1.1.3}/CHANGELOG.md +29 -0
  3. {stage_cli-1.1.1 → stage_cli-1.1.3}/PKG-INFO +8 -7
  4. {stage_cli-1.1.1 → stage_cli-1.1.3}/README.md +5 -5
  5. {stage_cli-1.1.1 → stage_cli-1.1.3}/SECURITY.md +1 -1
  6. {stage_cli-1.1.1 → stage_cli-1.1.3}/assets/ci-flow.svg +1 -1
  7. {stage_cli-1.1.1 → stage_cli-1.1.3}/pyproject.toml +2 -1
  8. stage_cli-1.1.3/src/stage/__init__.py +1 -0
  9. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/h.yaml +2 -0
  10. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/o.yaml +2 -3
  11. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/domain/__init__.py +2 -0
  12. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/domain/health.py +20 -6
  13. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/http/client.py +32 -3
  14. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/storage/sqlite_repo.py +5 -1
  15. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_health.py +45 -1
  16. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_http_client.py +43 -1
  17. {stage_cli-1.1.1 → stage_cli-1.1.3}/uv.lock +24 -24
  18. stage_cli-1.1.1/src/stage/__init__.py +0 -1
  19. {stage_cli-1.1.1 → stage_cli-1.1.3}/.gitattributes +0 -0
  20. {stage_cli-1.1.1 → stage_cli-1.1.3}/.python-version +0 -0
  21. {stage_cli-1.1.1 → stage_cli-1.1.3}/LICENSE +0 -0
  22. {stage_cli-1.1.1 → stage_cli-1.1.3}/assets/cli.png +0 -0
  23. {stage_cli-1.1.1 → stage_cli-1.1.3}/assets/tui.png +0 -0
  24. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/__main__.py +0 -0
  25. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/banner.py +0 -0
  26. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/bootstrap/__init__.py +0 -0
  27. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/bootstrap/openjobs.py +0 -0
  28. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/classify/__init__.py +0 -0
  29. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/classify/eligibility.py +0 -0
  30. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/classify/internship.py +0 -0
  31. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/classify/role.py +0 -0
  32. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/classify/scope.py +0 -0
  33. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/cli/__init__.py +0 -0
  34. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/cli/app.py +0 -0
  35. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/cli/commands/__init__.py +0 -0
  36. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/cli/commands/discovery.py +0 -0
  37. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/cli/commands/insight.py +0 -0
  38. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/cli/commands/pipeline.py +0 -0
  39. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/cli/commands/postings.py +0 -0
  40. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/cli/commands/schedule.py +0 -0
  41. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/cli/housekeeping.py +0 -0
  42. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/cli/logfile.py +0 -0
  43. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/cli/notify.py +0 -0
  44. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/cli/options.py +0 -0
  45. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/cli/render.py +0 -0
  46. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/cli/runlock.py +0 -0
  47. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/cli/schedule.py +0 -0
  48. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/cli/schedule_state.py +0 -0
  49. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/cli/selection.py +0 -0
  50. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/cli/serialize.py +0 -0
  51. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/companies.py +0 -0
  52. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/a.yaml +0 -0
  53. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/b.yaml +0 -0
  54. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/c.yaml +0 -0
  55. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/d.yaml +0 -0
  56. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/e.yaml +0 -0
  57. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/f.yaml +0 -0
  58. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/g.yaml +0 -0
  59. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/i.yaml +0 -0
  60. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/j.yaml +0 -0
  61. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/k.yaml +0 -0
  62. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/l.yaml +0 -0
  63. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/m.yaml +0 -0
  64. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/n.yaml +0 -0
  65. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/other.yaml +0 -0
  66. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/p.yaml +0 -0
  67. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/q.yaml +0 -0
  68. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/r.yaml +0 -0
  69. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/s.yaml +0 -0
  70. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/t.yaml +0 -0
  71. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/u.yaml +0 -0
  72. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/v.yaml +0 -0
  73. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/w.yaml +0 -0
  74. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/x.yaml +0 -0
  75. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/y.yaml +0 -0
  76. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/companies/z.yaml +0 -0
  77. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/fonts/DejaVuSans.LICENSE.txt +0 -0
  78. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/fonts/DejaVuSans.ttf +0 -0
  79. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/lexicon/company_tokens.yaml +0 -0
  80. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/lexicon/eligibility.yaml +0 -0
  81. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/lexicon/inclusive_suffixes.yaml +0 -0
  82. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/lexicon/internship.yaml +0 -0
  83. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/lexicon/language.yaml +0 -0
  84. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/lexicon/locations.yaml +0 -0
  85. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/lexicon/roles.yaml +0 -0
  86. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/lexicon/terms.yaml +0 -0
  87. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/lexicon/workday_facets.yaml +0 -0
  88. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/data/seed_companies.yaml +0 -0
  89. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/dedup/__init__.py +0 -0
  90. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/dedup/identity.py +0 -0
  91. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/dedup/resolve.py +0 -0
  92. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/domain/company.py +0 -0
  93. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/domain/coverage.py +0 -0
  94. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/domain/custom_board.py +0 -0
  95. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/domain/discovery.py +0 -0
  96. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/domain/enums.py +0 -0
  97. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/domain/events.py +0 -0
  98. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/domain/filters.py +0 -0
  99. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/domain/ids.py +0 -0
  100. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/domain/job.py +0 -0
  101. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/domain/matching.py +0 -0
  102. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/domain/priority.py +0 -0
  103. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/domain/quarantine.py +0 -0
  104. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/domain/rate_state.py +0 -0
  105. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/domain/retention.py +0 -0
  106. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/domain/rotation.py +0 -0
  107. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/domain/signals.py +0 -0
  108. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/domain/sync_run.py +0 -0
  109. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/domain/text.py +0 -0
  110. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/domain/validator.py +0 -0
  111. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/domain/visits.py +0 -0
  112. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/domain/workday.py +0 -0
  113. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/http/__init__.py +0 -0
  114. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/http/breaker.py +0 -0
  115. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/http/cache.py +0 -0
  116. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/http/profiles.py +0 -0
  117. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/lexicon.py +0 -0
  118. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/normalize/__init__.py +0 -0
  119. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/normalize/language.py +0 -0
  120. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/normalize/location.py +0 -0
  121. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/normalize/terms.py +0 -0
  122. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/normalize/urls.py +0 -0
  123. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/paths.py +0 -0
  124. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/py.typed +0 -0
  125. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/services/__init__.py +0 -0
  126. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/services/canary.py +0 -0
  127. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/services/coverage.py +0 -0
  128. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/services/discover.py +0 -0
  129. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/services/export.py +0 -0
  130. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/services/health.py +0 -0
  131. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/services/maintenance.py +0 -0
  132. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/services/quarantine.py +0 -0
  133. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/services/query.py +0 -0
  134. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/services/sync.py +0 -0
  135. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/__init__.py +0 -0
  136. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/_text.py +0 -0
  137. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/ashby.py +0 -0
  138. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/bamboohr.py +0 -0
  139. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/base.py +0 -0
  140. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/breezy.py +0 -0
  141. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/collage.py +0 -0
  142. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/community_feeds.py +0 -0
  143. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/curated_markdown.py +0 -0
  144. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/custom_json.py +0 -0
  145. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/espresso.py +0 -0
  146. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/feed.py +0 -0
  147. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/greenhouse.py +0 -0
  148. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/jobbank.py +0 -0
  149. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/jobvite.py +0 -0
  150. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/lever.py +0 -0
  151. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/oracle_cloud.py +0 -0
  152. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/platforms.py +0 -0
  153. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/quebec_emploi.py +0 -0
  154. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/recruitee.py +0 -0
  155. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/simplify.py +0 -0
  156. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/smartrecruiters.py +0 -0
  157. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/speedyapply.py +0 -0
  158. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/themuse.py +0 -0
  159. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/workable.py +0 -0
  160. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/workday.py +0 -0
  161. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/sources/zshah.py +0 -0
  162. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/storage/__init__.py +0 -0
  163. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/storage/migrations/0001_initial.sql +0 -0
  164. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/storage/migrations/__init__.py +0 -0
  165. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/storage/repository.py +0 -0
  166. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/storage/search.py +0 -0
  167. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/storage/writer.py +0 -0
  168. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/tui/__init__.py +0 -0
  169. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/tui/app.py +0 -0
  170. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/tui/help.py +0 -0
  171. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/tui/safe.py +0 -0
  172. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/tui/screens/__init__.py +0 -0
  173. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/tui/screens/boards.py +0 -0
  174. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/tui/screens/postings.py +0 -0
  175. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/tui/screens/review.py +0 -0
  176. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/tui/screens/splash.py +0 -0
  177. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/tui/screens/stats.py +0 -0
  178. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/tui/screens/sync.py +0 -0
  179. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/tui/state.py +0 -0
  180. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/tui/theme.tcss +0 -0
  181. {stage_cli-1.1.1 → stage_cli-1.1.3}/src/stage/tui/widgets/__init__.py +0 -0
  182. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/conftest.py +0 -0
  183. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/fixtures/bilingual_titles.json +0 -0
  184. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/fixtures/blackrock_search.html +0 -0
  185. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/fixtures/croesus_career.html +0 -0
  186. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/fixtures/disney_search.html +0 -0
  187. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/fixtures/ea_searchjobs.html +0 -0
  188. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/fixtures/espresso_search.html +0 -0
  189. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/fixtures/gildan_openpositions.html +0 -0
  190. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/fixtures/greenhouse_jobs.json +0 -0
  191. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/fixtures/jobbank_search.html +0 -0
  192. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/fixtures/jobvite_agscareer.html +0 -0
  193. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/fixtures/jobvite_resolver.html +0 -0
  194. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/fixtures/l3harris_search.html +0 -0
  195. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/fixtures/lightspeed_openings.html +0 -0
  196. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/fixtures/loblaw_jobs.html +0 -0
  197. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/fixtures/nbc_search.html +0 -0
  198. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/fixtures/parsons_search.html +0 -0
  199. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/fixtures/publicissapient_sitemap.html +0 -0
  200. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/fixtures/rentec_careers.html +0 -0
  201. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/fixtures/sega_vacancies.html +0 -0
  202. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/fixtures/sfrmk_search.html +0 -0
  203. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/fixtures/shopify_internships.html +0 -0
  204. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/fixtures/twosigma_openroles.html +0 -0
  205. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/golden/classification.json +0 -0
  206. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_adapter_salvage.py +0 -0
  207. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_adopt_unregistered.py +0 -0
  208. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_board_identity.py +0 -0
  209. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_board_rest.py +0 -0
  210. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_canary.py +0 -0
  211. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_canary_events.py +0 -0
  212. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_classify.py +0 -0
  213. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_cli_help.py +0 -0
  214. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_cli_options.py +0 -0
  215. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_collage_adapter.py +0 -0
  216. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_companies_registry.py +0 -0
  217. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_company_fold_key.py +0 -0
  218. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_coverage.py +0 -0
  219. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_curated_markdown.py +0 -0
  220. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_custom_json.py +0 -0
  221. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_custom_json_buckets.py +0 -0
  222. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_custom_json_formats.py +0 -0
  223. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_custom_json_pagination.py +0 -0
  224. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_dedup.py +0 -0
  225. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_degree_and_disciplines.py +0 -0
  226. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_detail_queue.py +0 -0
  227. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_discover.py +0 -0
  228. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_eligibility.py +0 -0
  229. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_export.py +0 -0
  230. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_feed_adapters.py +0 -0
  231. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_feeds.py +0 -0
  232. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_fetch_authority.py +0 -0
  233. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_fold_inclusive.py +0 -0
  234. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_golden.py +0 -0
  235. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_greenhouse_adapter.py +0 -0
  236. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_housekeeping.py +0 -0
  237. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_html_boards.py +0 -0
  238. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_html_text.py +0 -0
  239. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_http_akamai.py +0 -0
  240. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_http_conditional.py +0 -0
  241. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_http_resilience.py +0 -0
  242. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_integrity_repair.py +0 -0
  243. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_jobvite_adapter.py +0 -0
  244. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_language.py +0 -0
  245. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_layering.py +0 -0
  246. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_lever_adapter.py +0 -0
  247. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_lexicon.py +0 -0
  248. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_location.py +0 -0
  249. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_malformed_input.py +0 -0
  250. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_migrations.py +0 -0
  251. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_new_adapters.py +0 -0
  252. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_notify.py +0 -0
  253. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_openjobs_crossref.py +0 -0
  254. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_oracle_cloud_adapter.py +0 -0
  255. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_paths.py +0 -0
  256. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_permissions.py +0 -0
  257. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_platform_shapes.py +0 -0
  258. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_profiles.py +0 -0
  259. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_quarantine.py +0 -0
  260. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_quarantine_relabel.py +0 -0
  261. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_quebec_emploi.py +0 -0
  262. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_rate_state.py +0 -0
  263. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_recall_limits.py +0 -0
  264. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_recheck.py +0 -0
  265. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_registry_shards.py +0 -0
  266. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_render.py +0 -0
  267. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_request_log.py +0 -0
  268. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_rescreen.py +0 -0
  269. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_review_contradictions.py +0 -0
  270. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_rotation.py +0 -0
  271. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_run_budget.py +0 -0
  272. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_schedule.py +0 -0
  273. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_schedule_state.py +0 -0
  274. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_schema_baseline.py +0 -0
  275. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_search.py +0 -0
  276. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_serialize.py +0 -0
  277. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_show.py +0 -0
  278. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_signals_persist.py +0 -0
  279. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_smartrecruiters_adapter.py +0 -0
  280. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_speedyapply.py +0 -0
  281. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_startup_budget.py +0 -0
  282. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_sync_e2e.py +0 -0
  283. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_sync_shape.py +0 -0
  284. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_sync_step2.py +0 -0
  285. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_terms.py +0 -0
  286. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_tombstones.py +0 -0
  287. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_tui.py +0 -0
  288. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_unregistered_quarantine.py +0 -0
  289. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_untrusted_display.py +0 -0
  290. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_update_badges.py +0 -0
  291. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_verify_registry.py +0 -0
  292. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_workday_adapter.py +0 -0
  293. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_workday_urls.py +0 -0
  294. {stage_cli-1.1.1 → stage_cli-1.1.3}/tests/test_writer.py +0 -0
@@ -88,5 +88,3 @@ desktop.ini
88
88
  $RECYCLE.BIN/
89
89
  *.lnk
90
90
 
91
- # Local-only release runbook
92
- packaging/RELEASING.md
@@ -4,6 +4,33 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
5
  follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.1.3] - 2026-09-09
8
+
9
+ ### Fixed
10
+
11
+ - `doctor` said greenhouse stored nothing on a run that stored 138. A source
12
+ left out of a run writes a row of zeroes, and the volume check read those as
13
+ real while discarding every run that deferred a board. Greenhouse defers on
14
+ all of them. A run counts now only if the source ran, and a deferred run is
15
+ measured against other deferred runs.
16
+ - Haize Labs and Odin Dynamics are off. Both boards 404 with nowhere to move to.
17
+ The two internships they published were already quarantined as non-CS.
18
+ - The PyPI page showed no author. A name and an email in one metadata field get
19
+ folded into a single header, leaving the author line blank.
20
+
21
+ ## [1.1.2] - 2026-09-09
22
+
23
+ ### Fixed
24
+
25
+ - Job Bank has returned nothing since 31 August. Its server drops the connection
26
+ on any client offering TLS 1.3, so no request ever reached the board. That one
27
+ host is capped at TLS 1.2 now. Certificates are still checked and every other
28
+ board still gets TLS 1.3.
29
+ - A connection that died before the server answered was logged as `ConnectError:`
30
+ with nothing after the colon, which is why Job Bank looked fine for eight days.
31
+ The cause is carried through now:
32
+ `ConnectError: ConnectionResetError: [Errno 54] Connection reset by peer`.
33
+
7
34
  ## [1.1.1] - 2026-09-07
8
35
 
9
36
  ### Fixed
@@ -94,6 +121,8 @@ them in a SQLite database on your own machine.
94
121
 
95
122
  Python 3.12, 3.13 or 3.14 on macOS, Linux or Windows.
96
123
 
124
+ [1.1.3]: https://github.com/NicholasXydis/Stage/releases/tag/v1.1.3
125
+ [1.1.2]: https://github.com/NicholasXydis/Stage/releases/tag/v1.1.2
97
126
  [1.1.1]: https://github.com/NicholasXydis/Stage/releases/tag/v1.1.1
98
127
  [1.1.0]: https://github.com/NicholasXydis/Stage/releases/tag/v1.1.0
99
128
  [1.0.0]: https://github.com/NicholasXydis/Stage/releases/tag/v1.0.0
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: stage-cli
3
- Version: 1.1.1
3
+ Version: 1.1.3
4
4
  Summary: Aggregates CS internship postings (Canada/US, EN + FR) into a local SQLite database
5
5
  Project-URL: Homepage, https://github.com/NicholasXydis/Stage
6
6
  Project-URL: Repository, https://github.com/NicholasXydis/Stage
7
7
  Project-URL: Issues, https://github.com/NicholasXydis/Stage/issues
8
8
  Project-URL: Changelog, https://github.com/NicholasXydis/Stage/blob/main/CHANGELOG.md
9
- Author-email: Nicholas Xydis <NicholasXydis@outlook.com>
9
+ Author: Nicholas Xydis
10
+ Maintainer-email: Nicholas Xydis <NicholasXydis@outlook.com>
10
11
  License: MIT
11
12
  License-File: LICENSE
12
13
  Keywords: cli,co-op,internship,python,stage,student,tui
@@ -59,7 +60,7 @@ Free and open source. No account, no server, no telemetry.
59
60
 
60
61
  ![python](https://img.shields.io/static/v1?label=python&message=3.12%20%7C%203.13%20%7C%203.14&color=4a9eff&logo=python&logoColor=white)
61
62
 
62
- ![tests](https://img.shields.io/static/v1?label=tests&message=2%2C408&color=44bb00) ![coverage](https://img.shields.io/static/v1?label=coverage&message=89%25&color=44bb00) [![ci](https://img.shields.io/github/actions/workflow/status/NicholasXydis/Stage/ci.yml?branch=main&label=ci&logo=githubactions&logoColor=white)](https://github.com/NicholasXydis/Stage/actions/workflows/ci.yml)
63
+ ![tests](https://img.shields.io/static/v1?label=tests&message=2%2C420&color=44bb00) ![coverage](https://img.shields.io/static/v1?label=coverage&message=89%25&color=44bb00) [![ci](https://img.shields.io/github/actions/workflow/status/NicholasXydis/Stage/ci.yml?branch=main&label=ci&logo=githubactions&logoColor=white)](https://github.com/NicholasXydis/Stage/actions/workflows/ci.yml)
63
64
 
64
65
  <br>
65
66
 
@@ -90,7 +91,7 @@ Internships disappear fast. By the time you find the right posting, hundreds of
90
91
 
91
92
  Stage scans 1,450+ employer career boards and finds internships CS undergrads in Canada and the U.S. can actually apply to. Every rejected posting is logged with the exact reason it was filtered out.
92
93
 
93
- It has kept **3,577** postings out of **166,298** screened so far.
94
+ It has kept **4,024** postings out of **188,284** screened so far.
94
95
 
95
96
  ## A look at it
96
97
 
@@ -272,7 +273,7 @@ Stage/
272
273
  │ ├─ http/ rate posture, circuit breaker, validator cache
273
274
  │ ├─ storage/ SQLite repository, migrations, FTS
274
275
  │ └─ data/ packaged registry (a directory) and lexicons
275
- ├─ tests/ 2,408 tests, 89% branch coverage
276
+ ├─ tests/ 2,420 tests, 89% branch coverage
276
277
  └─ .github/workflows/ CI, CodeQL, scheduled canary, release
277
278
  ```
278
279
 
@@ -299,7 +300,7 @@ Stage/
299
300
  ▼ ▼
300
301
  ┌─────────────┐ ┌─────────────┐
301
302
  │ jobs │ │ quarantine │
302
- │ 5,495 │ │ 160,803 │
303
+ │ 6,219 │ │ 182,065 │
303
304
  └──────┬──────┘ └─────────────┘
304
305
  │ dedup collapses the same posting across sources
305
306
  ▼
@@ -318,7 +319,7 @@ Stage/
318
319
 
319
320
  | Workflow | File | Purpose |
320
321
  | --- | --- | --- |
321
- | CI | `.github/workflows/ci.yml` | Ruff, format, mypy strict, 2,408 tests, coverage floor, badge drift |
322
+ | CI | `.github/workflows/ci.yml` | Ruff, format, mypy strict, 2,420 tests, coverage floor, badge drift |
322
323
  | Supported Pythons | `.github/workflows/ci.yml` | The full suite again on 3.13 and 3.14 |
323
324
  | Wheel | `.github/workflows/ci.yml` | Builds and smoke-tests the wheel on Ubuntu, Windows and macOS |
324
325
  | CodeQL | `.github/workflows/codeql.yml` | Static analysis for Python and the workflows themselves |
@@ -20,7 +20,7 @@ Free and open source. No account, no server, no telemetry.
20
20
 
21
21
  ![python](https://img.shields.io/static/v1?label=python&message=3.12%20%7C%203.13%20%7C%203.14&color=4a9eff&logo=python&logoColor=white)
22
22
 
23
- ![tests](https://img.shields.io/static/v1?label=tests&message=2%2C408&color=44bb00) ![coverage](https://img.shields.io/static/v1?label=coverage&message=89%25&color=44bb00) [![ci](https://img.shields.io/github/actions/workflow/status/NicholasXydis/Stage/ci.yml?branch=main&label=ci&logo=githubactions&logoColor=white)](https://github.com/NicholasXydis/Stage/actions/workflows/ci.yml)
23
+ ![tests](https://img.shields.io/static/v1?label=tests&message=2%2C420&color=44bb00) ![coverage](https://img.shields.io/static/v1?label=coverage&message=89%25&color=44bb00) [![ci](https://img.shields.io/github/actions/workflow/status/NicholasXydis/Stage/ci.yml?branch=main&label=ci&logo=githubactions&logoColor=white)](https://github.com/NicholasXydis/Stage/actions/workflows/ci.yml)
24
24
 
25
25
  <br>
26
26
 
@@ -51,7 +51,7 @@ Internships disappear fast. By the time you find the right posting, hundreds of
51
51
 
52
52
  Stage scans 1,450+ employer career boards and finds internships CS undergrads in Canada and the U.S. can actually apply to. Every rejected posting is logged with the exact reason it was filtered out.
53
53
 
54
- It has kept **3,577** postings out of **166,298** screened so far.
54
+ It has kept **4,024** postings out of **188,284** screened so far.
55
55
 
56
56
  ## A look at it
57
57
 
@@ -233,7 +233,7 @@ Stage/
233
233
  │ ├─ http/ rate posture, circuit breaker, validator cache
234
234
  │ ├─ storage/ SQLite repository, migrations, FTS
235
235
  │ └─ data/ packaged registry (a directory) and lexicons
236
- ├─ tests/ 2,408 tests, 89% branch coverage
236
+ ├─ tests/ 2,420 tests, 89% branch coverage
237
237
  └─ .github/workflows/ CI, CodeQL, scheduled canary, release
238
238
  ```
239
239
 
@@ -260,7 +260,7 @@ Stage/
260
260
  ▼ ▼
261
261
  ┌─────────────┐ ┌─────────────┐
262
262
  │ jobs │ │ quarantine │
263
- │ 5,495 │ │ 160,803 │
263
+ │ 6,219 │ │ 182,065 │
264
264
  └──────┬──────┘ └─────────────┘
265
265
  │ dedup collapses the same posting across sources
266
266
  ▼
@@ -279,7 +279,7 @@ Stage/
279
279
 
280
280
  | Workflow | File | Purpose |
281
281
  | --- | --- | --- |
282
- | CI | `.github/workflows/ci.yml` | Ruff, format, mypy strict, 2,408 tests, coverage floor, badge drift |
282
+ | CI | `.github/workflows/ci.yml` | Ruff, format, mypy strict, 2,420 tests, coverage floor, badge drift |
283
283
  | Supported Pythons | `.github/workflows/ci.yml` | The full suite again on 3.13 and 3.14 |
284
284
  | Wheel | `.github/workflows/ci.yml` | Builds and smoke-tests the wheel on Ubuntu, Windows and macOS |
285
285
  | CodeQL | `.github/workflows/codeql.yml` | Static analysis for Python and the workflows themselves |
@@ -4,7 +4,7 @@
4
4
 
5
5
  | Version | Supported |
6
6
  | --- | --- |
7
- | 1.1.1 | ✅ |
7
+ | 1.1.3 | ✅ |
8
8
 
9
9
  Only the latest release receives security fixes.
10
10
 
@@ -22,7 +22,7 @@
22
22
 
23
23
  <rect x="240" y="42" width="180" height="58" rx="12" fill="#111C30" stroke="#3B82F6" stroke-width="2"/>
24
24
  <text x="330" y="66" fill="#F0F6FC" font-size="14">ruff, format, mypy</text>
25
- <text x="330" y="86" fill="#F0F6FC" font-size="14">2,408 tests, 89%</text>
25
+ <text x="330" y="86" fill="#F0F6FC" font-size="14">2,420 tests, 89%</text>
26
26
 
27
27
  <path d="M420 71H470" stroke="#3B82F6" stroke-width="3" marker-end="url(#arrow)"/>
28
28
 
@@ -5,7 +5,8 @@ description = "Aggregates CS internship postings (Canada/US, EN + FR) into a loc
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12,<3.15"
7
7
  license = { text = "MIT" }
8
- authors = [{ name = "Nicholas Xydis", email = "NicholasXydis@outlook.com" }]
8
+ authors = [{ name = "Nicholas Xydis" }]
9
+ maintainers = [{ name = "Nicholas Xydis", email = "NicholasXydis@outlook.com" }]
9
10
  keywords = [
10
11
  "internship",
11
12
  "stage",
@@ -0,0 +1 @@
1
+ __version__ = "1.1.3"
@@ -11,6 +11,8 @@
11
11
  slug: haizelabs
12
12
  source_of_record: discover
13
13
  last_verified: &id004 2026-08-14
14
+ enabled: false
15
+ notes: '2026-09-09: greenhouse/haizelabs 404s and their board page 500s; no ashby or lever board exists'
14
16
  - name: Halcyon
15
17
  platform: greenhouse
16
18
  slug: halcyon
@@ -43,9 +43,8 @@
43
43
  source_of_record: discover
44
44
  last_verified: &id004 2026-08-14
45
45
  name_gate_exempt: true
46
- notes: '2026-08-14: board token taken from this employer''s own application URL
47
- in a feed posting, not from slug guessing; ashby publishes no board name, so the
48
- name gate cannot confirm it and provenance is the evidence (12 job(s))'
46
+ enabled: false
47
+ notes: '2026-09-09: ashby/odin-dynamics 404s while the ashby API is healthy; the employer removed the board'
49
48
  - name: Off world studios
50
49
  platform: bamboohr
51
50
  slug: owi
@@ -65,6 +65,7 @@ from stage.domain.filters import DEFAULT_LIMIT, DEFAULT_WINDOW_DAYS, JobFilters
65
65
  from stage.domain.health import (
66
66
  MIN_VOLUME_HISTORY,
67
67
  STALE_AFTER_DAYS,
68
+ UNRECORDED_FETCH,
68
69
  UNRECORDED_VOLUME,
69
70
  VOLUME_DROP_RATIO,
70
71
  IntegrityFinding,
@@ -210,6 +211,7 @@ __all__ = [
210
211
  "rotate",
211
212
  "MIN_VOLUME_HISTORY",
212
213
  "STALE_AFTER_DAYS",
214
+ "UNRECORDED_FETCH",
213
215
  "UNRECORDED_VOLUME",
214
216
  "VOLUME_DROP_RATIO",
215
217
  "IntegrityFinding",
@@ -7,6 +7,7 @@ MIN_VOLUME_HISTORY = 3
7
7
  VOLUME_DROP_RATIO = 0.5
8
8
  STALE_AFTER_DAYS = 14
9
9
  UNRECORDED_VOLUME = -1
10
+ UNRECORDED_FETCH = -1
10
11
 
11
12
 
12
13
  class VolumeVerdict(StrEnum):
@@ -27,10 +28,21 @@ class VolumePoint:
27
28
  stored: int
28
29
  deferred: int = 0
29
30
  blocked: bool = False
31
+ fetched: int = UNRECORDED_FETCH
32
+ requests: int = UNRECORDED_FETCH
33
+ not_modified: int = UNRECORDED_FETCH
34
+
35
+ @property
36
+ def ran(self) -> bool:
37
+ return self.fetched != 0 or self.requests != 0 or self.not_modified != 0
38
+
39
+ @property
40
+ def rotated(self) -> bool:
41
+ return self.deferred > 0
30
42
 
31
43
  @property
32
44
  def is_evidence(self) -> bool:
33
- return self.stored > UNRECORDED_VOLUME and self.deferred == 0 and not self.blocked
45
+ return self.stored > UNRECORDED_VOLUME and not self.blocked and self.ran
34
46
 
35
47
 
36
48
  @dataclass(frozen=True, slots=True)
@@ -59,8 +71,9 @@ def assess_volume(source: str, history: list[VolumePoint]) -> VolumeSignal:
59
71
  detail=_unproven_reason(history),
60
72
  )
61
73
 
62
- latest = evidence[0].stored
63
- prior = [point.stored for point in evidence[1:]]
74
+ newest = evidence[0]
75
+ latest = newest.stored
76
+ prior = [point.stored for point in evidence[1:] if point.rotated == newest.rotated]
64
77
  if len(prior) < MIN_VOLUME_HISTORY:
65
78
  return VolumeSignal(
66
79
  source=source,
@@ -114,10 +127,10 @@ def assess_volume(source: str, history: list[VolumePoint]) -> VolumeSignal:
114
127
  def _unproven_reason(history: list[VolumePoint]) -> str:
115
128
  if not history:
116
129
  return "no runs on record"
117
- if any(point.blocked for point in history):
130
+ if not any(point.ran for point in history):
131
+ return "no run fetched this source, so none of them can show a drop"
132
+ if all(point.blocked for point in history if point.ran):
118
133
  return "every run was blocked, which is a throttle not a drop"
119
- if any(point.deferred for point in history):
120
- return "every run deferred members, so rotation explains it"
121
134
  return "no run has recorded a stored count yet"
122
135
 
123
136
 
@@ -157,6 +170,7 @@ class IntegrityFinding:
157
170
  __all__ = [
158
171
  "MIN_VOLUME_HISTORY",
159
172
  "STALE_AFTER_DAYS",
173
+ "UNRECORDED_FETCH",
160
174
  "UNRECORDED_VOLUME",
161
175
  "VOLUME_DROP_RATIO",
162
176
  "IntegrityFinding",
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
2
  import json
3
3
  import random
4
+ import ssl
4
5
  import time
5
6
  from collections.abc import Mapping
6
7
  from dataclasses import dataclass, field
@@ -38,6 +39,7 @@ RETRYABLE_STATUSES = frozenset({429, 500, 502, 503, 504})
38
39
  BLOCKING_STATUSES = frozenset({401, 403})
39
40
  BLOCKED_COOLDOWN_S = 1800.0
40
41
  DENIED_HOSTS_BEFORE_BLOCK = 2
42
+ TLS12_ONLY_HOSTS = frozenset({"www.jobbank.gc.ca"})
41
43
  MAX_REDIRECTS = 3
42
44
  ORIGIN_BOUND_HEADERS = frozenset(
43
45
  {"authorization", "cookie", "origin", "referer", "proxy-authorization", "x-csrf-token"}
@@ -229,6 +231,25 @@ class HostBudget:
229
231
  return state
230
232
 
231
233
 
234
+ def _tls12_context() -> ssl.SSLContext:
235
+ context = httpx.create_ssl_context()
236
+ context.maximum_version = ssl.TLSVersion.TLSv1_2
237
+ return context
238
+
239
+
240
+ def _describe(exc: BaseException) -> str:
241
+ detail = str(exc)
242
+ seen = {id(exc)}
243
+ cause = exc.__cause__ or exc.__context__
244
+ while not detail and cause is not None and id(cause) not in seen:
245
+ detail = str(cause)
246
+ seen.add(id(cause))
247
+ if detail:
248
+ return f"{type(exc).__name__}: {type(cause).__name__}: {detail}"
249
+ cause = cause.__cause__ or cause.__context__
250
+ return f"{type(exc).__name__}: {detail}"
251
+
252
+
232
253
  def _origin_of(url: httpx.URL) -> tuple[str, str, int | None]:
233
254
  return (url.scheme, (url.host or "").lower(), url.port)
234
255
 
@@ -269,11 +290,19 @@ class HttpClient:
269
290
  for bucket, state in self._rate_state.items()
270
291
  if state.is_blocked(self._now)
271
292
  }
293
+ mounts: dict[str, httpx.AsyncBaseTransport] | None = None
294
+ if transport is None:
295
+ capped = self._allowed_hosts & TLS12_ONLY_HOSTS
296
+ mounts = {
297
+ f"https://{host}": httpx.AsyncHTTPTransport(verify=_tls12_context())
298
+ for host in sorted(capped)
299
+ } or None
272
300
  self._client = httpx.AsyncClient(
273
301
  timeout=timeout or DEFAULT_TIMEOUT,
274
302
  headers={"User-Agent": USER_AGENT, "Accept": "application/json"},
275
303
  follow_redirects=False,
276
304
  transport=transport,
305
+ mounts=mounts or {},
277
306
  )
278
307
 
279
308
  async def __aenter__(self) -> Self:
@@ -556,7 +585,7 @@ class HttpClient:
556
585
  status=None,
557
586
  elapsed_ms=(time.perf_counter() - started) * 1000,
558
587
  attempt=attempt,
559
- error=f"{type(exc).__name__}: {exc}",
588
+ error=_describe(exc),
560
589
  )
561
590
  )
562
591
  raise
@@ -564,7 +593,7 @@ class HttpClient:
564
593
  elapsed = (time.perf_counter() - started) * 1000
565
594
  budget.metrics.failures += 1
566
595
  budget.breaker.record_failure()
567
- budget.last_error = f"{type(exc).__name__}: {exc}"
596
+ budget.last_error = _describe(exc)
568
597
  self._log.append(
569
598
  RequestRecord(
570
599
  method=method,
@@ -572,7 +601,7 @@ class HttpClient:
572
601
  status=None,
573
602
  elapsed_ms=elapsed,
574
603
  attempt=attempt,
575
- error=f"{type(exc).__name__}: {exc}",
604
+ error=_describe(exc),
576
605
  )
577
606
  )
578
607
  raise
@@ -1381,7 +1381,8 @@ class SqliteRepository:
1381
1381
 
1382
1382
  def volume_history(self, limit: int) -> Mapping[str, list[VolumePoint]]:
1383
1383
  rows = self._conn.execute(
1384
- "SELECT source, stored, deferred, blocked FROM sync_run_sources "
1384
+ "SELECT source, stored, deferred, blocked, fetched, requests, not_modified "
1385
+ "FROM sync_run_sources "
1385
1386
  "WHERE run_id IN (SELECT id FROM sync_runs ORDER BY id DESC LIMIT ?) "
1386
1387
  "ORDER BY run_id DESC",
1387
1388
  (limit,),
@@ -1393,6 +1394,9 @@ class SqliteRepository:
1393
1394
  stored=int(row["stored"]),
1394
1395
  deferred=int(row["deferred"]),
1395
1396
  blocked=bool(row["blocked"]),
1397
+ fetched=int(row["fetched"]),
1398
+ requests=int(row["requests"]),
1399
+ not_modified=int(row["not_modified"]),
1396
1400
  )
1397
1401
  )
1398
1402
  return history
@@ -58,6 +58,50 @@ def test_a_blocked_run_cannot_raise_a_volume_alert() -> None:
58
58
  assert signal.verdict is not VolumeVerdict.COLLAPSED, "a throttle is not drift"
59
59
 
60
60
 
61
+ def _idle(count: int) -> list[VolumePoint]:
62
+ return [VolumePoint(stored=0, fetched=0, requests=0, not_modified=0)] * count
63
+
64
+
65
+ def _ran(*stored: int) -> list[VolumePoint]:
66
+ return [VolumePoint(stored=value, fetched=value, requests=4) for value in stored]
67
+
68
+
69
+ def test_a_source_left_out_of_a_run_is_not_a_source_that_collapsed() -> None:
70
+ signal = assess_volume("greenhouse", _idle(1) + _ran(74, 74, 74, 74))
71
+ assert signal.verdict is not VolumeVerdict.COLLAPSED
72
+
73
+
74
+ def test_a_source_left_out_of_a_run_cannot_drag_the_baseline_down() -> None:
75
+ signal = assess_volume("greenhouse", _ran(138) + _idle(3) + _ran(231, 223, 219))
76
+ assert signal.verdict is VolumeVerdict.HEALTHY
77
+ assert signal.samples == 3
78
+
79
+
80
+ def test_a_source_that_never_ran_proves_nothing() -> None:
81
+ signal = assess_volume("ashby", _idle(5))
82
+ assert signal.verdict is VolumeVerdict.UNPROVEN
83
+ assert "fetched" in signal.detail
84
+
85
+
86
+ def test_a_source_that_fetched_rows_and_stored_none_still_collapses() -> None:
87
+ live = VolumePoint(stored=0, fetched=44, requests=1)
88
+ signal = assess_volume("quebec-emploi", [live] + _ran(40, 41, 40))
89
+ assert signal.verdict is VolumeVerdict.COLLAPSED
90
+
91
+
92
+ def test_a_source_blocked_on_every_run_is_a_throttle_not_a_drop() -> None:
93
+ history = [VolumePoint(stored=0, blocked=True, requests=4)] * 4
94
+ signal = assess_volume("workday", history)
95
+ assert signal.verdict is VolumeVerdict.UNPROVEN
96
+ assert "throttle" in signal.detail
97
+
98
+
99
+ def test_a_run_that_was_all_cache_hits_still_counts_as_having_run() -> None:
100
+ cached = VolumePoint(stored=0, fetched=0, requests=0, not_modified=12)
101
+ signal = assess_volume("lever", [cached] + _ran(40, 41, 40))
102
+ assert signal.verdict is VolumeVerdict.COLLAPSED
103
+
104
+
61
105
  def test_a_run_predating_the_stored_column_is_not_a_baseline_of_zero() -> None:
62
106
  history = [VolumePoint(stored=12)] + _points(*([UNRECORDED_VOLUME] * 6))
63
107
  signal = assess_volume("greenhouse", history)
@@ -143,7 +187,7 @@ async def test_doctor_names_the_source_whose_volume_collapsed(db_path: Path) ->
143
187
  started_at=NOW,
144
188
  finished_at=NOW,
145
189
  outcome=SyncOutcome.SUCCESS,
146
- sources=(SourceRunStats(source="lever", stored=stored),),
190
+ sources=(SourceRunStats(source="lever", stored=stored, requests=4),),
147
191
  )
148
192
  )
149
193
  report = await doctor(repository, now=NOW)
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import ssl
2
3
  import time
3
4
  from collections.abc import AsyncIterator
4
5
 
@@ -7,7 +8,7 @@ import pytest
7
8
  import respx
8
9
 
9
10
  from stage.http import HostBudgetExceededError, HttpClient, RatePosture
10
- from stage.http.client import USER_AGENT
11
+ from stage.http.client import USER_AGENT, _describe, _tls12_context
11
12
 
12
13
  ENDPOINT = "https://boards-api.greenhouse.io/v1/boards/acme/jobs"
13
14
 
@@ -328,3 +329,44 @@ async def test_a_plain_http_url_is_refused_before_it_is_sent() -> None:
328
329
  ) as client:
329
330
  with pytest.raises(RedirectNotAllowedError):
330
331
  await client.get_json("http://one.example.test/jobs")
332
+
333
+
334
+ def test_a_tls12_only_host_gets_a_capped_transport() -> None:
335
+ client = HttpClient(allowed_hosts=frozenset({"www.jobbank.gc.ca"}))
336
+ mounted = client._client._mounts
337
+ assert any(key.pattern.endswith("www.jobbank.gc.ca") for key in mounted)
338
+
339
+
340
+ def test_an_ordinary_host_keeps_the_default_transport() -> None:
341
+ client = HttpClient(allowed_hosts=frozenset({"boards-api.greenhouse.io"}))
342
+ assert not client._client._mounts
343
+
344
+
345
+ def test_the_capped_context_still_verifies_certificates() -> None:
346
+ context = _tls12_context()
347
+ assert context.maximum_version is ssl.TLSVersion.TLSv1_2
348
+ assert context.verify_mode is ssl.CERT_REQUIRED
349
+ assert context.check_hostname
350
+
351
+
352
+ def test_an_injected_transport_is_never_overridden() -> None:
353
+ transport = httpx.MockTransport(lambda _: httpx.Response(200))
354
+ client = HttpClient(allowed_hosts=frozenset({"www.jobbank.gc.ca"}), transport=transport)
355
+ assert not client._client._mounts
356
+
357
+
358
+ def test_an_empty_error_reports_its_underlying_cause() -> None:
359
+ try:
360
+ try:
361
+ raise ConnectionResetError(54, "Connection reset by peer")
362
+ except OSError as inner:
363
+ raise httpx.ConnectError("") from inner
364
+ except httpx.ConnectError as exc:
365
+ described = _describe(exc)
366
+ assert described == "ConnectError: ConnectionResetError: [Errno 54] Connection reset by peer"
367
+
368
+
369
+ def test_an_error_that_speaks_for_itself_is_unchanged() -> None:
370
+ assert _describe(httpx.ConnectError("all attempts failed")) == (
371
+ "ConnectError: all attempts failed"
372
+ )
@@ -531,11 +531,11 @@ wheels = [
531
531
 
532
532
  [[package]]
533
533
  name = "platformdirs"
534
- version = "4.11.5"
534
+ version = "4.11.7"
535
535
  source = { registry = "https://pypi.org/simple" }
536
- sdist = { url = "https://files.pythonhosted.org/packages/ea/06/cf1564dcc2e2261c8c8c6c05628dc8b418943bdae2a4e58640ceb2f770fa/platformdirs-4.11.5.tar.gz", hash = "sha256:e8b31f4f8bcbbedef91a6b57a706255e4f148d2a4e01648382a0a47342539173", size = 34823, upload-time = "2026-08-27T21:36:37.46Z" }
536
+ sdist = { url = "https://files.pythonhosted.org/packages/69/b7/802a56eca9f2fac455b8bab5375a2647b0f0e14a2cd63ef077de3c4a7658/platformdirs-4.11.7.tar.gz", hash = "sha256:4f41487eeeeeb07f3a6625e61d9bc0ae6809f92d3386dbd74392fbb76108104d", size = 35127, upload-time = "2026-09-01T13:35:10.502Z" }
537
537
  wheels = [
538
- { url = "https://files.pythonhosted.org/packages/c7/12/6f3fcd5067a9cbf4f8664b32957973498da8b083455203c8d9cab83a725c/platformdirs-4.11.5-py3-none-any.whl", hash = "sha256:89f8d42695853b89c7170bd49bc3dc593f98a71e695ede88e06a3b247bc4563b", size = 23900, upload-time = "2026-08-27T21:36:36.227Z" },
538
+ { url = "https://files.pythonhosted.org/packages/27/6e/80993e10a0482f630cef528635789233224f36b1ffd11592aa15d13ff9ce/platformdirs-4.11.7-py3-none-any.whl", hash = "sha256:8a02cb259042c79d1cd0450facc2fe6dc9d303ae7901afbe33bf8ea0b188cef6", size = 23938, upload-time = "2026-09-01T13:35:09.02Z" },
539
539
  ]
540
540
 
541
541
  [[package]]
@@ -775,27 +775,27 @@ wheels = [
775
775
 
776
776
  [[package]]
777
777
  name = "ruff"
778
- version = "0.16.5"
779
- source = { registry = "https://pypi.org/simple" }
780
- sdist = { url = "https://files.pythonhosted.org/packages/f3/85/c8e12473c93018f92d19dd988a294202e1c27426c47ec4de53ffb847b8d8/ruff-0.16.5.tar.gz", hash = "sha256:1b88500f9ffbcab3dedb0082c9f9492e91ec3d618aac1236a3e0189938f7040b", size = 4912003, upload-time = "2026-08-27T16:34:18.258Z" }
781
- wheels = [
782
- { url = "https://files.pythonhosted.org/packages/c6/b6/77c90a970fe2dae17a723acbd011043ea97c98d7deacccefdc4ba74ec512/ruff-0.16.5-py3-none-linux_armv6l.whl", hash = "sha256:12e5f673e774c35fbb62f288809c7653b73445f8ecec6b6063fd6ea3521aa14b", size = 10011941, upload-time = "2026-08-27T16:33:41.287Z" },
783
- { url = "https://files.pythonhosted.org/packages/4b/46/6cf67cf6411885a1d6f7f6d801682f155536a85176d10b605e2ceffed8bd/ruff-0.16.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:eda58a5802de40e7ed5b32b64e0b32539338cc6fcd2c78f61e3ad6a0d79f51c3", size = 10204049, upload-time = "2026-08-27T16:33:44.056Z" },
784
- { url = "https://files.pythonhosted.org/packages/46/fd/c8720ca7a090abf0c2fef4abe8a5ef6e5127ed15196d8886ff75a2b370e2/ruff-0.16.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5ae9a7b9a8875131f40f8fe967cc86abf899779efd663cb7ce3d572d01da7eb", size = 9809037, upload-time = "2026-08-27T16:33:46.257Z" },
785
- { url = "https://files.pythonhosted.org/packages/43/45/a684caacdedaca180f52bacccc40bf0789d2c5a7c75f25324853e9eaedb5/ruff-0.16.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b719b0a1f4d59710d283ab2965f621684a108a9e41da622e3b23f0326cd0025", size = 9964129, upload-time = "2026-08-27T16:33:48.352Z" },
786
- { url = "https://files.pythonhosted.org/packages/9e/f2/5d2bcdaca6b5b93d1b4dfc166cd2aebf7680143a1b38a28759df13a94d31/ruff-0.16.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2298f2780ed1be0c5cb1361e32ab7b1467f3cce7dabe101d2210a314f2fe42e9", size = 9821518, upload-time = "2026-08-27T16:33:50.57Z" },
787
- { url = "https://files.pythonhosted.org/packages/aa/ff/011cce29accf9257d5974145b733fc653a37985ed6825413a3987cefbfe0/ruff-0.16.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:258f29035a2dd021e7861e631b227a5b3f14e50c1184c9a6a122c5f4576154d7", size = 10534835, upload-time = "2026-08-27T16:33:52.522Z" },
788
- { url = "https://files.pythonhosted.org/packages/d7/5a/f0cf109bada9bba0e96c90c21c9f9251803f57225c32d293327a03c710d6/ruff-0.16.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9a4f0432966834019c74d1b7e5c51224305d7713f3d7faf3e7451f1a3be3cde", size = 11252550, upload-time = "2026-08-27T16:33:54.521Z" },
789
- { url = "https://files.pythonhosted.org/packages/63/4d/1d481aaea2046c6a7ed7c291f9004c669cce3c087b6b376ed5b08271e3fe/ruff-0.16.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5eb3a8c3d0ade9cea42b591fd530368e8798380e30e0a308b85a5cf718f09ea", size = 10777949, upload-time = "2026-08-27T16:33:56.88Z" },
790
- { url = "https://files.pythonhosted.org/packages/ee/34/ee245ca55f64443233034b3d02b03236b19242004281247c079390b7facd/ruff-0.16.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef0f69e191a13a3c9816f63163c88790cb12cd157bbbb384e9c44745702ab105", size = 10311656, upload-time = "2026-08-27T16:33:59.12Z" },
791
- { url = "https://files.pythonhosted.org/packages/a7/4d/c33a333e341c0a2b96c715b52d89a606f5a34cd4ac493cd9b8d0187186b8/ruff-0.16.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0eeab41fbea2c42f98dfb9822cdccda9d24ba38d49f6dc945b5c236d48f0ef29", size = 10532125, upload-time = "2026-08-27T16:34:01.166Z" },
792
- { url = "https://files.pythonhosted.org/packages/30/e1/a64cef78b40192497bb98a27a8aa8f2c98ee9ee15bc97f7712d94ef32937/ruff-0.16.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f0768e9df4300713fff30733c87575f68b6f1d8de41184e505b7fdd9c0c95eaf", size = 10097648, upload-time = "2026-08-27T16:34:03.16Z" },
793
- { url = "https://files.pythonhosted.org/packages/cc/4e/4cdc9ed3c3e109d2f71e62572a37457298d7bc7501ec3138babb7ed32bbd/ruff-0.16.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:95cc70cdc7aa80c338de356279d2adbeb2de0f520b9ecd8aba75b94e95e02f91", size = 9829344, upload-time = "2026-08-27T16:34:05.134Z" },
794
- { url = "https://files.pythonhosted.org/packages/39/4a/31ed35ce31729955fc583ee0d176d6e784c1290cb0b0a75cb2134c1ab72a/ruff-0.16.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d185c8398ded1bfd91c0c2cb258346307571eccc473a8490af8c3977399c384a", size = 10277117, upload-time = "2026-08-27T16:34:07.425Z" },
795
- { url = "https://files.pythonhosted.org/packages/a8/a0/60356d86687b4b666d593df213f4dc3041750d024cb7bf2cfa81cfd65c2e/ruff-0.16.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fb8e3a3c4c6a784150a7ced53b015f4b253fc2bf97a610886419ead64b4756ef", size = 10711653, upload-time = "2026-08-27T16:34:09.712Z" },
796
- { url = "https://files.pythonhosted.org/packages/ed/20/656d67f5b25ca9bda4e02b1de25867b2954e1d19e03648060f167ad0f4cc/ruff-0.16.5-py3-none-win32.whl", hash = "sha256:288b0a5f080492fe5635db849f9e2e84aa3cce7b7f0e955997d416c507c76a26", size = 10034250, upload-time = "2026-08-27T16:34:11.8Z" },
797
- { url = "https://files.pythonhosted.org/packages/5b/42/ee8e68a207b9127fcde6c3d7e197def432f346cb1af159e1fa14ca0d1cdc/ruff-0.16.5-py3-none-win_amd64.whl", hash = "sha256:ddc6385fb2137f616357ca03d6c74f4be987f80fed4008566b754f6032b8546f", size = 10516714, upload-time = "2026-08-27T16:34:13.963Z" },
798
- { url = "https://files.pythonhosted.org/packages/73/e3/7df5a396e445b9ba49ce9a9437439a4d80042c61c0ade199abf8d16de1ac/ruff-0.16.5-py3-none-win_arm64.whl", hash = "sha256:a64abe90968719b851bb7cedffaa8753fbdbdadab483089682db623f3edc587e", size = 10391564, upload-time = "2026-08-27T16:34:16.064Z" },
778
+ version = "0.16.6"
779
+ source = { registry = "https://pypi.org/simple" }
780
+ sdist = { url = "https://files.pythonhosted.org/packages/a4/7c/6adb35d70e7c027e308274557901c7e00fb3407750faf3620c184ae058cb/ruff-0.16.6.tar.gz", hash = "sha256:dcf8a73d2ff77e99dde91244b4da16feba7f14e6beeb4015dee7c5a909e99050", size = 4921251, upload-time = "2026-09-03T16:57:29.037Z" }
781
+ wheels = [
782
+ { url = "https://files.pythonhosted.org/packages/a4/28/9cc1b79639e284ec103f43c88c644db4eb58cbd0ea1ca11f1193435369ac/ruff-0.16.6-py3-none-linux_armv6l.whl", hash = "sha256:61c368c26bf8e973e5ab14a2772de587bc068ea3f9a277f673380749b4898fb8", size = 10015638, upload-time = "2026-09-03T16:56:40.986Z" },
783
+ { url = "https://files.pythonhosted.org/packages/71/11/627d342ef727ea7794edf74fe23d60a074b02c3acc2e9436684e782286ca/ruff-0.16.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ecf4f068e2e123e43a26e9db4e19524cc56563912404e83bbfca375757e45a32", size = 10220762, upload-time = "2026-09-03T16:56:44.681Z" },
784
+ { url = "https://files.pythonhosted.org/packages/43/d9/b75668ce41e4c8d073d18d6d08672ba6906ce45d5c06ea4fdb2e84ce3853/ruff-0.16.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:99b62ea33baf130f50368798d841f0d95527b6d817bf31817b65dd058f1d314c", size = 9835082, upload-time = "2026-09-03T16:56:47.142Z" },
785
+ { url = "https://files.pythonhosted.org/packages/99/97/123ab10b05cde889c107c20f5a9774955104b5552796a2a8584b089ae8eb/ruff-0.16.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fbf89013f2bb3f6835a6038ff658dc8a1b38c98dc8e724b964168ad4e881876", size = 9949304, upload-time = "2026-09-03T16:56:49.813Z" },
786
+ { url = "https://files.pythonhosted.org/packages/3e/58/a4a2c59dd2e5b85929c912d9cac3056eb9ee8c7e75e9b9fe3e109174966b/ruff-0.16.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56a67065e22efa6bc4d498299d3bb06c0c90aace8fac2068b5a12f9dc4d8d51d", size = 9840612, upload-time = "2026-09-03T16:56:52.368Z" },
787
+ { url = "https://files.pythonhosted.org/packages/61/6a/ff8c8626a786c4f49d48ced4a752dadbca65f5263005f9c2416578194694/ruff-0.16.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e25cc89174874b176a157e4428d66761c2c0c006654419bf384f967f361ff1b1", size = 10543465, upload-time = "2026-09-03T16:56:55.089Z" },
788
+ { url = "https://files.pythonhosted.org/packages/ad/bb/c47535923365f337b82e28192e4e9eef2176511007cfd99a62fc22df5dad/ruff-0.16.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700580ed5303723cb3c11c2f1d2a8913ce77b7ea86646dddb887f5417a9ba70", size = 11267576, upload-time = "2026-09-03T16:56:57.791Z" },
789
+ { url = "https://files.pythonhosted.org/packages/ba/50/e5119a5212b5cd63b51e1f4b25e7bd636a6668fc069a3160b108ad7e3c16/ruff-0.16.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15f1d0b6e165a6e56567befb6629f8209271311d990bae0f37e6d065035ef5f3", size = 10781993, upload-time = "2026-09-03T16:57:00.666Z" },
790
+ { url = "https://files.pythonhosted.org/packages/8b/98/083d8b4ef3c51a0d19db84367791cbe9f44e4b53343d19dfa83556e1cd9a/ruff-0.16.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d72c591a96986ee4268860e2b7235082129ca5e4cb9cbba653a4b57c11893757", size = 10317748, upload-time = "2026-09-03T16:57:03.428Z" },
791
+ { url = "https://files.pythonhosted.org/packages/9a/29/68f7ff2c5ad95f19f00627ac2de95644e25fe47371ea60b2db1fd952315e/ruff-0.16.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:65a006baa18f33324325814c864daef03541d51564b98c517610ea756ab7003e", size = 10540096, upload-time = "2026-09-03T16:57:06.182Z" },
792
+ { url = "https://files.pythonhosted.org/packages/c4/f9/79a8f6de85968641d68a7863aeec577551924ef066a990a48ff93167beab/ruff-0.16.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cd02a7bf1a21a8735228a3e8c95a9dc5cf86bd2a52194f4aaae2a5755b4de0f4", size = 10100494, upload-time = "2026-09-03T16:57:09.194Z" },
793
+ { url = "https://files.pythonhosted.org/packages/d9/e8/b81a22d9b90c00b892ccf2fa2ac36fa95de4c13ab85aea3e73795cfe4651/ruff-0.16.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:31b36f1e5ad85e0737f09d2be4e512e2e283583c14015da3b9dc07359ac0fc88", size = 9843663, upload-time = "2026-09-03T16:57:12.168Z" },
794
+ { url = "https://files.pythonhosted.org/packages/39/aa/54f516ec5e5a11c4afdceb1c454ebb054ffb96e4f4a1705580b4346abd35/ruff-0.16.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:61029b4ab4aa723fd3064fab96b1d814492596bf0c792679fffcbde1e1679953", size = 10282461, upload-time = "2026-09-03T16:57:15.077Z" },
795
+ { url = "https://files.pythonhosted.org/packages/52/0b/38d0aa8aa32372b96dc44f97b22e576c4147808271aab7b2cb1e353d4445/ruff-0.16.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9ac8998457832c2061709d900856b7ad271dace0cb41f346588d540162bfa718", size = 10728808, upload-time = "2026-09-03T16:57:17.797Z" },
796
+ { url = "https://files.pythonhosted.org/packages/5e/e5/9e274e24eeb027640ffc7442f21239f16d17f47acec15ae34f32e03a5c79/ruff-0.16.6-py3-none-win32.whl", hash = "sha256:0b87d9d16fcb63e8018423ca1d50b7260f15cb2da33e30db4baad4183a948c25", size = 10049212, upload-time = "2026-09-03T16:57:20.55Z" },
797
+ { url = "https://files.pythonhosted.org/packages/22/31/72472449414223ed1a2da236b992adbb1a2ae59e34794574810f60ce068e/ruff-0.16.6-py3-none-win_amd64.whl", hash = "sha256:10d21c51c3495d8eaea7b703a16592117ea6eb1d649e36335aa965ff1173eb39", size = 10556402, upload-time = "2026-09-03T16:57:23.501Z" },
798
+ { url = "https://files.pythonhosted.org/packages/fc/07/d781f8f8e1ac24bef9f3269cf62ffb1407ca24c3a8f12e5e22874f90528c/ruff-0.16.6-py3-none-win_arm64.whl", hash = "sha256:7a976c79b958f94e50a022a19f0f8c87387448020935ec14fc74331bd0a7f2c5", size = 10412850, upload-time = "2026-09-03T16:57:26.416Z" },
799
799
  ]
800
800
 
801
801
  [[package]]
@@ -1 +0,0 @@
1
- __version__ = "1.1.1"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes