stage-cli 1.0.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.
- stage/__init__.py +1 -0
- stage/__main__.py +8 -0
- stage/banner.py +32 -0
- stage/bootstrap/__init__.py +0 -0
- stage/bootstrap/openjobs.py +392 -0
- stage/classify/__init__.py +29 -0
- stage/classify/eligibility.py +115 -0
- stage/classify/internship.py +64 -0
- stage/classify/role.py +91 -0
- stage/classify/scope.py +47 -0
- stage/cli/__init__.py +0 -0
- stage/cli/app.py +4 -0
- stage/cli/commands/__init__.py +8 -0
- stage/cli/commands/discovery.py +294 -0
- stage/cli/commands/insight.py +494 -0
- stage/cli/commands/pipeline.py +337 -0
- stage/cli/commands/postings.py +473 -0
- stage/cli/commands/schedule.py +171 -0
- stage/cli/housekeeping.py +64 -0
- stage/cli/logfile.py +56 -0
- stage/cli/notify.py +170 -0
- stage/cli/options.py +678 -0
- stage/cli/render.py +1398 -0
- stage/cli/runlock.py +74 -0
- stage/cli/schedule.py +702 -0
- stage/cli/schedule_state.py +363 -0
- stage/cli/selection.py +83 -0
- stage/cli/serialize.py +196 -0
- stage/companies.py +542 -0
- stage/data/companies/a.yaml +1289 -0
- stage/data/companies/b.yaml +900 -0
- stage/data/companies/c.yaml +1377 -0
- stage/data/companies/d.yaml +497 -0
- stage/data/companies/e.yaml +519 -0
- stage/data/companies/f.yaml +454 -0
- stage/data/companies/g.yaml +601 -0
- stage/data/companies/h.yaml +446 -0
- stage/data/companies/i.yaml +503 -0
- stage/data/companies/j.yaml +138 -0
- stage/data/companies/k.yaml +278 -0
- stage/data/companies/l.yaml +402 -0
- stage/data/companies/m.yaml +937 -0
- stage/data/companies/n.yaml +549 -0
- stage/data/companies/o.yaml +371 -0
- stage/data/companies/other.yaml +58 -0
- stage/data/companies/p.yaml +825 -0
- stage/data/companies/q.yaml +121 -0
- stage/data/companies/r.yaml +583 -0
- stage/data/companies/s.yaml +1140 -0
- stage/data/companies/t.yaml +817 -0
- stage/data/companies/u.yaml +196 -0
- stage/data/companies/v.yaml +325 -0
- stage/data/companies/w.yaml +353 -0
- stage/data/companies/x.yaml +67 -0
- stage/data/companies/y.yaml +36 -0
- stage/data/companies/z.yaml +146 -0
- stage/data/fonts/DejaVuSans.LICENSE.txt +99 -0
- stage/data/fonts/DejaVuSans.ttf +0 -0
- stage/data/lexicon/company_tokens.yaml +228 -0
- stage/data/lexicon/eligibility.yaml +455 -0
- stage/data/lexicon/inclusive_suffixes.yaml +37 -0
- stage/data/lexicon/internship.yaml +187 -0
- stage/data/lexicon/language.yaml +226 -0
- stage/data/lexicon/locations.yaml +1159 -0
- stage/data/lexicon/roles.yaml +2012 -0
- stage/data/lexicon/terms.yaml +76 -0
- stage/data/lexicon/workday_facets.yaml +27 -0
- stage/data/seed_companies.yaml +198 -0
- stage/dedup/__init__.py +19 -0
- stage/dedup/identity.py +113 -0
- stage/dedup/resolve.py +97 -0
- stage/domain/__init__.py +244 -0
- stage/domain/company.py +49 -0
- stage/domain/coverage.py +86 -0
- stage/domain/custom_board.py +92 -0
- stage/domain/discovery.py +94 -0
- stage/domain/enums.py +114 -0
- stage/domain/events.py +204 -0
- stage/domain/filters.py +27 -0
- stage/domain/health.py +169 -0
- stage/domain/ids.py +48 -0
- stage/domain/job.py +47 -0
- stage/domain/matching.py +15 -0
- stage/domain/priority.py +34 -0
- stage/domain/quarantine.py +39 -0
- stage/domain/rate_state.py +78 -0
- stage/domain/retention.py +20 -0
- stage/domain/rotation.py +46 -0
- stage/domain/signals.py +12 -0
- stage/domain/sync_run.py +35 -0
- stage/domain/text.py +113 -0
- stage/domain/validator.py +14 -0
- stage/domain/visits.py +60 -0
- stage/domain/workday.py +38 -0
- stage/http/__init__.py +58 -0
- stage/http/breaker.py +53 -0
- stage/http/cache.py +44 -0
- stage/http/client.py +725 -0
- stage/http/profiles.py +101 -0
- stage/lexicon.py +370 -0
- stage/normalize/__init__.py +16 -0
- stage/normalize/language.py +47 -0
- stage/normalize/location.py +271 -0
- stage/normalize/terms.py +153 -0
- stage/normalize/urls.py +122 -0
- stage/paths.py +86 -0
- stage/py.typed +0 -0
- stage/services/__init__.py +0 -0
- stage/services/canary.py +120 -0
- stage/services/coverage.py +231 -0
- stage/services/discover.py +747 -0
- stage/services/export.py +274 -0
- stage/services/health.py +237 -0
- stage/services/maintenance.py +225 -0
- stage/services/quarantine.py +20 -0
- stage/services/query.py +86 -0
- stage/services/sync.py +1257 -0
- stage/sources/__init__.py +82 -0
- stage/sources/_text.py +79 -0
- stage/sources/ashby.py +93 -0
- stage/sources/bamboohr.py +80 -0
- stage/sources/base.py +225 -0
- stage/sources/breezy.py +90 -0
- stage/sources/collage.py +60 -0
- stage/sources/community_feeds.py +142 -0
- stage/sources/curated_markdown.py +289 -0
- stage/sources/custom_json.py +610 -0
- stage/sources/espresso.py +154 -0
- stage/sources/feed.py +44 -0
- stage/sources/greenhouse.py +104 -0
- stage/sources/jobbank.py +147 -0
- stage/sources/jobvite.py +133 -0
- stage/sources/lever.py +76 -0
- stage/sources/oracle_cloud.py +187 -0
- stage/sources/platforms.py +609 -0
- stage/sources/quebec_emploi.py +146 -0
- stage/sources/recruitee.py +96 -0
- stage/sources/simplify.py +110 -0
- stage/sources/smartrecruiters.py +216 -0
- stage/sources/speedyapply.py +200 -0
- stage/sources/themuse.py +157 -0
- stage/sources/workable.py +83 -0
- stage/sources/workday.py +524 -0
- stage/sources/zshah.py +99 -0
- stage/storage/__init__.py +29 -0
- stage/storage/migrations/0001_initial.sql +239 -0
- stage/storage/migrations/__init__.py +135 -0
- stage/storage/repository.py +213 -0
- stage/storage/search.py +28 -0
- stage/storage/sqlite_repo.py +1586 -0
- stage/storage/writer.py +249 -0
- stage/tui/__init__.py +0 -0
- stage/tui/app.py +82 -0
- stage/tui/help.py +26 -0
- stage/tui/safe.py +21 -0
- stage/tui/screens/__init__.py +0 -0
- stage/tui/screens/boards.py +186 -0
- stage/tui/screens/postings.py +509 -0
- stage/tui/screens/review.py +209 -0
- stage/tui/screens/splash.py +37 -0
- stage/tui/screens/stats.py +124 -0
- stage/tui/screens/sync.py +194 -0
- stage/tui/state.py +160 -0
- stage/tui/theme.tcss +205 -0
- stage/tui/widgets/__init__.py +0 -0
- stage_cli-1.0.0.dist-info/METADATA +379 -0
- stage_cli-1.0.0.dist-info/RECORD +170 -0
- stage_cli-1.0.0.dist-info/WHEEL +4 -0
- stage_cli-1.0.0.dist-info/entry_points.txt +2 -0
- stage_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from stage.cli.options import (
|
|
6
|
+
WORD,
|
|
7
|
+
DatabaseOption,
|
|
8
|
+
InvalidOptionError,
|
|
9
|
+
JsonOption,
|
|
10
|
+
RegistryOption,
|
|
11
|
+
RepairOption,
|
|
12
|
+
StaleDaysOption,
|
|
13
|
+
_count,
|
|
14
|
+
_database,
|
|
15
|
+
_parse_enum,
|
|
16
|
+
_print_failure,
|
|
17
|
+
_require_enum,
|
|
18
|
+
app,
|
|
19
|
+
run_async,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.command(
|
|
24
|
+
help="Find registry gaps and unclassified companies seen in feeds",
|
|
25
|
+
rich_help_panel="Registry and maintenance",
|
|
26
|
+
)
|
|
27
|
+
def coverage(
|
|
28
|
+
unregistered: Annotated[
|
|
29
|
+
bool,
|
|
30
|
+
typer.Option(
|
|
31
|
+
"--unregistered",
|
|
32
|
+
help="Show feed companies not in the registry and not already reviewed",
|
|
33
|
+
),
|
|
34
|
+
] = False,
|
|
35
|
+
classified: Annotated[
|
|
36
|
+
bool,
|
|
37
|
+
typer.Option("--classified", help="Show researched feed-company classifications"),
|
|
38
|
+
] = False,
|
|
39
|
+
contradictions: Annotated[
|
|
40
|
+
bool,
|
|
41
|
+
typer.Option(
|
|
42
|
+
"--contradictions",
|
|
43
|
+
help="Show review verdicts the registry or the clock now disagrees with",
|
|
44
|
+
),
|
|
45
|
+
] = False,
|
|
46
|
+
show_all: Annotated[
|
|
47
|
+
bool,
|
|
48
|
+
typer.Option("--all", help="List every row instead of the first 30"),
|
|
49
|
+
] = False,
|
|
50
|
+
stale_days: StaleDaysOption = None,
|
|
51
|
+
as_json: JsonOption = False,
|
|
52
|
+
registry: RegistryOption = None,
|
|
53
|
+
db: DatabaseOption = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
from datetime import UTC, datetime
|
|
56
|
+
|
|
57
|
+
from stage.cli.render import ROW_PREVIEW, render_coverage, terminal
|
|
58
|
+
from stage.cli.serialize import coverage_to_json, emit
|
|
59
|
+
from stage.companies import RegistryError, load_companies
|
|
60
|
+
from stage.services.coverage import CoverageReport
|
|
61
|
+
from stage.services.coverage import coverage as coverage_service
|
|
62
|
+
from stage.storage import open_repository
|
|
63
|
+
|
|
64
|
+
console = terminal()
|
|
65
|
+
now = datetime.now(UTC)
|
|
66
|
+
|
|
67
|
+
async def run() -> CoverageReport:
|
|
68
|
+
companies = load_companies(registry)
|
|
69
|
+
async with open_repository(_database(db)) as repository:
|
|
70
|
+
if stale_days is None:
|
|
71
|
+
return await coverage_service(
|
|
72
|
+
repository,
|
|
73
|
+
companies,
|
|
74
|
+
now=now,
|
|
75
|
+
unregistered=unregistered,
|
|
76
|
+
)
|
|
77
|
+
return await coverage_service(
|
|
78
|
+
repository,
|
|
79
|
+
companies,
|
|
80
|
+
now=now,
|
|
81
|
+
unregistered=unregistered,
|
|
82
|
+
stale_after_days=stale_days,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
report = run_async(run())
|
|
87
|
+
except RegistryError as exc:
|
|
88
|
+
_print_failure(exc)
|
|
89
|
+
raise typer.Exit(code=2) from exc
|
|
90
|
+
|
|
91
|
+
if as_json:
|
|
92
|
+
emit(coverage_to_json(report))
|
|
93
|
+
return
|
|
94
|
+
render_coverage(
|
|
95
|
+
console,
|
|
96
|
+
report,
|
|
97
|
+
now,
|
|
98
|
+
include_classified=classified,
|
|
99
|
+
include_contradictions=contradictions,
|
|
100
|
+
limit=None if show_all else ROW_PREVIEW,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@app.command(
|
|
105
|
+
help="Record why a feed-seen employer is not directly synced",
|
|
106
|
+
rich_help_panel="Registry and maintenance",
|
|
107
|
+
)
|
|
108
|
+
def classify(
|
|
109
|
+
company: Annotated[
|
|
110
|
+
str,
|
|
111
|
+
typer.Argument(
|
|
112
|
+
metavar="NAME",
|
|
113
|
+
click_type=WORD,
|
|
114
|
+
help="Employer name from stage coverage --unregistered",
|
|
115
|
+
),
|
|
116
|
+
],
|
|
117
|
+
disposition: Annotated[
|
|
118
|
+
str | None,
|
|
119
|
+
typer.Option(
|
|
120
|
+
"--status",
|
|
121
|
+
metavar="STATUS",
|
|
122
|
+
help="Required unless --clear: feed-only, unavailable, custom-json-candidate, "
|
|
123
|
+
"adapter-candidate, or deferred",
|
|
124
|
+
),
|
|
125
|
+
] = None,
|
|
126
|
+
note: Annotated[
|
|
127
|
+
str | None,
|
|
128
|
+
typer.Option(
|
|
129
|
+
"--note", metavar="TEXT", help="Required unless --clear: evidence for this decision"
|
|
130
|
+
),
|
|
131
|
+
] = None,
|
|
132
|
+
url: Annotated[
|
|
133
|
+
str | None, typer.Option("--url", metavar="URL", help="Public careers or jobs endpoint")
|
|
134
|
+
] = None,
|
|
135
|
+
clear: Annotated[bool, typer.Option("--clear", help="Remove this classification")] = False,
|
|
136
|
+
db: DatabaseOption = None,
|
|
137
|
+
) -> None:
|
|
138
|
+
from datetime import UTC, datetime
|
|
139
|
+
|
|
140
|
+
from stage.cli.render import terminal
|
|
141
|
+
from stage.domain import CoverageClassification, CoverageDisposition
|
|
142
|
+
from stage.storage import open_repository
|
|
143
|
+
|
|
144
|
+
console = terminal()
|
|
145
|
+
if clear:
|
|
146
|
+
if disposition is not None or note is not None or url is not None:
|
|
147
|
+
console.print("[red]--clear cannot be combined with --status, --note, or --url.[/red]")
|
|
148
|
+
raise typer.Exit(code=2)
|
|
149
|
+
try:
|
|
150
|
+
|
|
151
|
+
async def clear_entry() -> bool:
|
|
152
|
+
async with open_repository(_database(db)) as repository:
|
|
153
|
+
return await repository.clear_coverage_classification(company)
|
|
154
|
+
|
|
155
|
+
removed = run_async(clear_entry())
|
|
156
|
+
except ValueError as exc:
|
|
157
|
+
_print_failure(exc)
|
|
158
|
+
raise typer.Exit(code=2) from exc
|
|
159
|
+
if not removed:
|
|
160
|
+
console.print("[yellow]No matching classification was recorded.[/yellow]")
|
|
161
|
+
raise typer.Exit(code=1)
|
|
162
|
+
console.print("Classification removed.")
|
|
163
|
+
return
|
|
164
|
+
if disposition is None or note is None:
|
|
165
|
+
options = ", ".join(item.value for item in CoverageDisposition)
|
|
166
|
+
console.print(
|
|
167
|
+
f"[red]Pass both --status and --note, or pass --clear.[/red]\n"
|
|
168
|
+
f"--status must be one of: {options}"
|
|
169
|
+
)
|
|
170
|
+
raise typer.Exit(code=2)
|
|
171
|
+
try:
|
|
172
|
+
entry = CoverageClassification(
|
|
173
|
+
company=company,
|
|
174
|
+
disposition=_require_enum(disposition, CoverageDisposition, "--status"),
|
|
175
|
+
note=note,
|
|
176
|
+
checked_on=datetime.now(UTC),
|
|
177
|
+
url=url,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
async def record_entry() -> bool:
|
|
181
|
+
async with open_repository(_database(db)) as repository:
|
|
182
|
+
return await repository.record_coverage_classification(entry)
|
|
183
|
+
|
|
184
|
+
replaced = run_async(record_entry())
|
|
185
|
+
except (InvalidOptionError, ValueError) as exc:
|
|
186
|
+
_print_failure(exc)
|
|
187
|
+
raise typer.Exit(code=2) from exc
|
|
188
|
+
console.print("Classification updated." if replaced else "Classification recorded.")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@app.command(
|
|
192
|
+
help="Check database integrity, source health, and staleness", rich_help_panel="Health"
|
|
193
|
+
)
|
|
194
|
+
def doctor(
|
|
195
|
+
show_all: Annotated[
|
|
196
|
+
bool,
|
|
197
|
+
typer.Option("--all", help="List every board and row instead of the first 10"),
|
|
198
|
+
] = False,
|
|
199
|
+
verbose: Annotated[
|
|
200
|
+
bool,
|
|
201
|
+
typer.Option("--verbose", help="Print each board error in full"),
|
|
202
|
+
] = False,
|
|
203
|
+
stale_days: StaleDaysOption = None,
|
|
204
|
+
as_json: JsonOption = False,
|
|
205
|
+
repair: RepairOption = False,
|
|
206
|
+
db: DatabaseOption = None,
|
|
207
|
+
registry: RegistryOption = None,
|
|
208
|
+
) -> None:
|
|
209
|
+
from datetime import UTC, datetime
|
|
210
|
+
|
|
211
|
+
from stage.cli.render import (
|
|
212
|
+
BOARD_PREVIEW,
|
|
213
|
+
failure,
|
|
214
|
+
render_doctor,
|
|
215
|
+
render_repairs,
|
|
216
|
+
terminal,
|
|
217
|
+
)
|
|
218
|
+
from stage.cli.serialize import emit, health_to_json
|
|
219
|
+
from stage.companies import RegistryError, load_companies
|
|
220
|
+
from stage.domain import IntegrityRepair
|
|
221
|
+
from stage.services.health import DoctorReport
|
|
222
|
+
from stage.services.health import doctor as run_doctor
|
|
223
|
+
from stage.services.maintenance import repair_integrity
|
|
224
|
+
from stage.storage import open_repository
|
|
225
|
+
|
|
226
|
+
console = terminal()
|
|
227
|
+
now = datetime.now(UTC)
|
|
228
|
+
try:
|
|
229
|
+
rows = load_companies(registry)
|
|
230
|
+
except RegistryError as exc:
|
|
231
|
+
console.print(failure(exc))
|
|
232
|
+
raise typer.Exit(code=2) from exc
|
|
233
|
+
|
|
234
|
+
async def run() -> tuple[DoctorReport, tuple[IntegrityRepair, ...]]:
|
|
235
|
+
async with open_repository(_database(db)) as repository:
|
|
236
|
+
repairs = await repair_integrity(repository) if repair else ()
|
|
237
|
+
if stale_days is None:
|
|
238
|
+
return await run_doctor(repository, now=now, companies=rows), repairs
|
|
239
|
+
return (
|
|
240
|
+
await run_doctor(repository, now=now, stale_after_days=stale_days, companies=rows),
|
|
241
|
+
repairs,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
report, repairs = run_async(run())
|
|
245
|
+
if as_json:
|
|
246
|
+
emit(health_to_json(report))
|
|
247
|
+
else:
|
|
248
|
+
render_repairs(console, repairs)
|
|
249
|
+
render_doctor(
|
|
250
|
+
console,
|
|
251
|
+
report,
|
|
252
|
+
now,
|
|
253
|
+
limit=None if show_all else BOARD_PREVIEW,
|
|
254
|
+
verbose=verbose,
|
|
255
|
+
)
|
|
256
|
+
if not report.is_healthy:
|
|
257
|
+
raise typer.Exit(code=1)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@app.command(help="Show sync history and database totals", rich_help_panel="Health")
|
|
261
|
+
def stats(
|
|
262
|
+
show_all: Annotated[
|
|
263
|
+
bool,
|
|
264
|
+
typer.Option("--all", help="List every breakdown row instead of the first 10"),
|
|
265
|
+
] = False,
|
|
266
|
+
runs: Annotated[
|
|
267
|
+
int,
|
|
268
|
+
typer.Option(
|
|
269
|
+
"--runs", metavar="N", click_type=_count(1, 200), help="How many recent syncs to show"
|
|
270
|
+
),
|
|
271
|
+
] = 10,
|
|
272
|
+
as_json: JsonOption = False,
|
|
273
|
+
db: DatabaseOption = None,
|
|
274
|
+
) -> None:
|
|
275
|
+
from datetime import UTC, datetime
|
|
276
|
+
|
|
277
|
+
from stage.cli.render import BOARD_PREVIEW, render_stats, terminal
|
|
278
|
+
from stage.cli.serialize import emit, stats_to_json
|
|
279
|
+
from stage.services.health import StatsReport, statistics
|
|
280
|
+
from stage.storage import open_repository
|
|
281
|
+
|
|
282
|
+
console = terminal()
|
|
283
|
+
|
|
284
|
+
async def run() -> StatsReport:
|
|
285
|
+
async with open_repository(_database(db)) as repository:
|
|
286
|
+
return await statistics(repository, history=max(1, runs))
|
|
287
|
+
|
|
288
|
+
report = run_async(run())
|
|
289
|
+
if as_json:
|
|
290
|
+
emit(stats_to_json(report))
|
|
291
|
+
else:
|
|
292
|
+
render_stats(
|
|
293
|
+
console,
|
|
294
|
+
report,
|
|
295
|
+
datetime.now(UTC),
|
|
296
|
+
limit=None if show_all else BOARD_PREVIEW,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@app.command(help="Inspect source health, rate limits, and HTTP caches", rich_help_panel="Health")
|
|
301
|
+
def sources(
|
|
302
|
+
verbose: Annotated[
|
|
303
|
+
bool,
|
|
304
|
+
typer.Option("--verbose", help="Print each error and reason in full"),
|
|
305
|
+
] = False,
|
|
306
|
+
reset_rate_limit: Annotated[
|
|
307
|
+
str | None,
|
|
308
|
+
typer.Option(
|
|
309
|
+
"--reset-rate-limit",
|
|
310
|
+
metavar="BUCKET",
|
|
311
|
+
help="Reset stored rate-limit state for one host bucket",
|
|
312
|
+
),
|
|
313
|
+
] = None,
|
|
314
|
+
reset_all_rate_limits: Annotated[
|
|
315
|
+
bool,
|
|
316
|
+
typer.Option(
|
|
317
|
+
"--reset-all",
|
|
318
|
+
help="Reset stored rate-limit state for every host bucket",
|
|
319
|
+
),
|
|
320
|
+
] = False,
|
|
321
|
+
boards: Annotated[
|
|
322
|
+
bool,
|
|
323
|
+
typer.Option("--boards", help="Include every failing or stale board"),
|
|
324
|
+
] = False,
|
|
325
|
+
clear_cache: Annotated[
|
|
326
|
+
str | None,
|
|
327
|
+
typer.Option(
|
|
328
|
+
"--clear-cache",
|
|
329
|
+
metavar="SOURCE",
|
|
330
|
+
help="Clear saved HTTP validators for one source to force a refetch",
|
|
331
|
+
),
|
|
332
|
+
] = None,
|
|
333
|
+
clear_cache_all: Annotated[
|
|
334
|
+
bool,
|
|
335
|
+
typer.Option("--clear-cache-all", help="Clear saved HTTP validators for every source"),
|
|
336
|
+
] = False,
|
|
337
|
+
stale_days: StaleDaysOption = None,
|
|
338
|
+
as_json: JsonOption = False,
|
|
339
|
+
db: DatabaseOption = None,
|
|
340
|
+
) -> None:
|
|
341
|
+
from datetime import UTC, datetime
|
|
342
|
+
|
|
343
|
+
from stage.cli.render import (
|
|
344
|
+
render_board_health,
|
|
345
|
+
render_rate_state,
|
|
346
|
+
render_source_health,
|
|
347
|
+
render_workday_crawl_progress,
|
|
348
|
+
terminal,
|
|
349
|
+
)
|
|
350
|
+
from stage.cli.serialize import emit, sources_to_json
|
|
351
|
+
from stage.services.health import DoctorReport
|
|
352
|
+
from stage.services.health import doctor as run_doctor
|
|
353
|
+
from stage.services.maintenance import RateStateView, rate_state
|
|
354
|
+
from stage.storage import open_repository
|
|
355
|
+
|
|
356
|
+
console = terminal()
|
|
357
|
+
now = datetime.now(UTC)
|
|
358
|
+
|
|
359
|
+
if reset_rate_limit is not None and reset_all_rate_limits:
|
|
360
|
+
console.print("[red]Pass either --reset-rate-limit BUCKET or --reset-all, not both.[/red]")
|
|
361
|
+
raise typer.Exit(code=2)
|
|
362
|
+
if clear_cache is not None and clear_cache_all:
|
|
363
|
+
console.print(
|
|
364
|
+
"[red]Pass either --clear-cache <source> or --clear-cache-all, not both.[/red]"
|
|
365
|
+
)
|
|
366
|
+
raise typer.Exit(code=2)
|
|
367
|
+
|
|
368
|
+
async def run() -> tuple[RateStateView, DoctorReport]:
|
|
369
|
+
async with open_repository(_database(db)) as repository:
|
|
370
|
+
view = await rate_state(
|
|
371
|
+
repository,
|
|
372
|
+
bucket=reset_rate_limit,
|
|
373
|
+
clear_all=reset_all_rate_limits,
|
|
374
|
+
clear_cache=clear_cache,
|
|
375
|
+
clear_cache_all=clear_cache_all,
|
|
376
|
+
)
|
|
377
|
+
report = (
|
|
378
|
+
await run_doctor(repository, now=now)
|
|
379
|
+
if stale_days is None
|
|
380
|
+
else await run_doctor(repository, now=now, stale_after_days=stale_days)
|
|
381
|
+
)
|
|
382
|
+
return view, report
|
|
383
|
+
|
|
384
|
+
view, report = run_async(run())
|
|
385
|
+
cleared, states = view.cleared, view.states
|
|
386
|
+
|
|
387
|
+
if view.validators_cleared or clear_cache is not None or clear_cache_all:
|
|
388
|
+
target = "every source" if clear_cache_all else f"source {clear_cache!r}"
|
|
389
|
+
console.print(f"Cleared {view.validators_cleared} saved HTTP validator(s) for {target}.")
|
|
390
|
+
console.print("[dim]The next sync will fully refetch those sources.[/dim]")
|
|
391
|
+
|
|
392
|
+
if reset_rate_limit is not None or reset_all_rate_limits:
|
|
393
|
+
target = (
|
|
394
|
+
"every host bucket" if reset_all_rate_limits else f"host bucket {reset_rate_limit!r}"
|
|
395
|
+
)
|
|
396
|
+
if cleared:
|
|
397
|
+
console.print(f"Reset rate-limit state for {target} ({cleared} row(s)).")
|
|
398
|
+
else:
|
|
399
|
+
console.print(f"[yellow]No stored rate state for {target}.[/yellow]")
|
|
400
|
+
|
|
401
|
+
if as_json:
|
|
402
|
+
emit(
|
|
403
|
+
sources_to_json(
|
|
404
|
+
report,
|
|
405
|
+
states,
|
|
406
|
+
include_boards=boards,
|
|
407
|
+
rate_states_cleared=cleared,
|
|
408
|
+
cache_validators_cleared=view.validators_cleared,
|
|
409
|
+
)
|
|
410
|
+
)
|
|
411
|
+
return
|
|
412
|
+
|
|
413
|
+
render_source_health(console, report.sources, report.stale_after_days)
|
|
414
|
+
console.print()
|
|
415
|
+
render_workday_crawl_progress(console, report.workday_crawls)
|
|
416
|
+
console.print()
|
|
417
|
+
render_rate_state(console, states, now, verbose=verbose)
|
|
418
|
+
if boards:
|
|
419
|
+
console.print()
|
|
420
|
+
render_board_health(console, report.sources, now, verbose=verbose)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@app.command(help="Inspect rejected postings and why they were rejected", rich_help_panel="Health")
|
|
424
|
+
def quarantine(
|
|
425
|
+
reason: Annotated[
|
|
426
|
+
str | None,
|
|
427
|
+
typer.Option(
|
|
428
|
+
"--reason",
|
|
429
|
+
metavar="REASON",
|
|
430
|
+
help=(
|
|
431
|
+
"Filter by rejection reason: out-of-scope-location, not-an-internship, "
|
|
432
|
+
"out-of-scope-degree, not-a-cs-role, unknown-cs-role"
|
|
433
|
+
),
|
|
434
|
+
),
|
|
435
|
+
] = None,
|
|
436
|
+
source: Annotated[
|
|
437
|
+
str | None,
|
|
438
|
+
typer.Option("--source", metavar="SOURCE", help="Filter by source adapter name"),
|
|
439
|
+
] = None,
|
|
440
|
+
company: Annotated[
|
|
441
|
+
str | None,
|
|
442
|
+
typer.Option("--company", metavar="NAME", help="Filter by exact employer name"),
|
|
443
|
+
] = None,
|
|
444
|
+
show_all: Annotated[
|
|
445
|
+
bool,
|
|
446
|
+
typer.Option("--all", help="Show every match instead of the first page"),
|
|
447
|
+
] = False,
|
|
448
|
+
limit: Annotated[
|
|
449
|
+
int,
|
|
450
|
+
typer.Option(
|
|
451
|
+
"--limit",
|
|
452
|
+
metavar="N",
|
|
453
|
+
click_type=_count(1, None),
|
|
454
|
+
help="Maximum number of rejected postings to show; --all shows every match",
|
|
455
|
+
),
|
|
456
|
+
] = 50,
|
|
457
|
+
as_json: JsonOption = False,
|
|
458
|
+
db: DatabaseOption = None,
|
|
459
|
+
) -> None:
|
|
460
|
+
from stage.cli.render import render_quarantine, terminal
|
|
461
|
+
from stage.cli.serialize import emit, quarantine_to_json
|
|
462
|
+
from stage.domain import QuarantineFilters, RejectionReason
|
|
463
|
+
from stage.services.quarantine import QuarantineListing, list_quarantined
|
|
464
|
+
from stage.storage import open_repository
|
|
465
|
+
|
|
466
|
+
console = terminal()
|
|
467
|
+
|
|
468
|
+
try:
|
|
469
|
+
filters = QuarantineFilters(
|
|
470
|
+
reason=_parse_enum(reason, RejectionReason, "--reason"),
|
|
471
|
+
source=source,
|
|
472
|
+
company=company,
|
|
473
|
+
limit=None if show_all else limit,
|
|
474
|
+
)
|
|
475
|
+
except InvalidOptionError as exc:
|
|
476
|
+
_print_failure(exc)
|
|
477
|
+
raise typer.Exit(code=2) from exc
|
|
478
|
+
|
|
479
|
+
async def run() -> QuarantineListing:
|
|
480
|
+
async with open_repository(_database(db)) as repository:
|
|
481
|
+
return await list_quarantined(repository, filters)
|
|
482
|
+
|
|
483
|
+
listing = run_async(run())
|
|
484
|
+
|
|
485
|
+
if as_json:
|
|
486
|
+
emit(quarantine_to_json(listing.entries))
|
|
487
|
+
return
|
|
488
|
+
|
|
489
|
+
render_quarantine(
|
|
490
|
+
console,
|
|
491
|
+
listing.entries,
|
|
492
|
+
total_matching=listing.total_matching,
|
|
493
|
+
reason_counts=listing.reason_counts,
|
|
494
|
+
)
|