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.
Files changed (170) hide show
  1. stage/__init__.py +1 -0
  2. stage/__main__.py +8 -0
  3. stage/banner.py +32 -0
  4. stage/bootstrap/__init__.py +0 -0
  5. stage/bootstrap/openjobs.py +392 -0
  6. stage/classify/__init__.py +29 -0
  7. stage/classify/eligibility.py +115 -0
  8. stage/classify/internship.py +64 -0
  9. stage/classify/role.py +91 -0
  10. stage/classify/scope.py +47 -0
  11. stage/cli/__init__.py +0 -0
  12. stage/cli/app.py +4 -0
  13. stage/cli/commands/__init__.py +8 -0
  14. stage/cli/commands/discovery.py +294 -0
  15. stage/cli/commands/insight.py +494 -0
  16. stage/cli/commands/pipeline.py +337 -0
  17. stage/cli/commands/postings.py +473 -0
  18. stage/cli/commands/schedule.py +171 -0
  19. stage/cli/housekeeping.py +64 -0
  20. stage/cli/logfile.py +56 -0
  21. stage/cli/notify.py +170 -0
  22. stage/cli/options.py +678 -0
  23. stage/cli/render.py +1398 -0
  24. stage/cli/runlock.py +74 -0
  25. stage/cli/schedule.py +702 -0
  26. stage/cli/schedule_state.py +363 -0
  27. stage/cli/selection.py +83 -0
  28. stage/cli/serialize.py +196 -0
  29. stage/companies.py +542 -0
  30. stage/data/companies/a.yaml +1289 -0
  31. stage/data/companies/b.yaml +900 -0
  32. stage/data/companies/c.yaml +1377 -0
  33. stage/data/companies/d.yaml +497 -0
  34. stage/data/companies/e.yaml +519 -0
  35. stage/data/companies/f.yaml +454 -0
  36. stage/data/companies/g.yaml +601 -0
  37. stage/data/companies/h.yaml +446 -0
  38. stage/data/companies/i.yaml +503 -0
  39. stage/data/companies/j.yaml +138 -0
  40. stage/data/companies/k.yaml +278 -0
  41. stage/data/companies/l.yaml +402 -0
  42. stage/data/companies/m.yaml +937 -0
  43. stage/data/companies/n.yaml +549 -0
  44. stage/data/companies/o.yaml +371 -0
  45. stage/data/companies/other.yaml +58 -0
  46. stage/data/companies/p.yaml +825 -0
  47. stage/data/companies/q.yaml +121 -0
  48. stage/data/companies/r.yaml +583 -0
  49. stage/data/companies/s.yaml +1140 -0
  50. stage/data/companies/t.yaml +817 -0
  51. stage/data/companies/u.yaml +196 -0
  52. stage/data/companies/v.yaml +325 -0
  53. stage/data/companies/w.yaml +353 -0
  54. stage/data/companies/x.yaml +67 -0
  55. stage/data/companies/y.yaml +36 -0
  56. stage/data/companies/z.yaml +146 -0
  57. stage/data/fonts/DejaVuSans.LICENSE.txt +99 -0
  58. stage/data/fonts/DejaVuSans.ttf +0 -0
  59. stage/data/lexicon/company_tokens.yaml +228 -0
  60. stage/data/lexicon/eligibility.yaml +455 -0
  61. stage/data/lexicon/inclusive_suffixes.yaml +37 -0
  62. stage/data/lexicon/internship.yaml +187 -0
  63. stage/data/lexicon/language.yaml +226 -0
  64. stage/data/lexicon/locations.yaml +1159 -0
  65. stage/data/lexicon/roles.yaml +2012 -0
  66. stage/data/lexicon/terms.yaml +76 -0
  67. stage/data/lexicon/workday_facets.yaml +27 -0
  68. stage/data/seed_companies.yaml +198 -0
  69. stage/dedup/__init__.py +19 -0
  70. stage/dedup/identity.py +113 -0
  71. stage/dedup/resolve.py +97 -0
  72. stage/domain/__init__.py +244 -0
  73. stage/domain/company.py +49 -0
  74. stage/domain/coverage.py +86 -0
  75. stage/domain/custom_board.py +92 -0
  76. stage/domain/discovery.py +94 -0
  77. stage/domain/enums.py +114 -0
  78. stage/domain/events.py +204 -0
  79. stage/domain/filters.py +27 -0
  80. stage/domain/health.py +169 -0
  81. stage/domain/ids.py +48 -0
  82. stage/domain/job.py +47 -0
  83. stage/domain/matching.py +15 -0
  84. stage/domain/priority.py +34 -0
  85. stage/domain/quarantine.py +39 -0
  86. stage/domain/rate_state.py +78 -0
  87. stage/domain/retention.py +20 -0
  88. stage/domain/rotation.py +46 -0
  89. stage/domain/signals.py +12 -0
  90. stage/domain/sync_run.py +35 -0
  91. stage/domain/text.py +113 -0
  92. stage/domain/validator.py +14 -0
  93. stage/domain/visits.py +60 -0
  94. stage/domain/workday.py +38 -0
  95. stage/http/__init__.py +58 -0
  96. stage/http/breaker.py +53 -0
  97. stage/http/cache.py +44 -0
  98. stage/http/client.py +725 -0
  99. stage/http/profiles.py +101 -0
  100. stage/lexicon.py +370 -0
  101. stage/normalize/__init__.py +16 -0
  102. stage/normalize/language.py +47 -0
  103. stage/normalize/location.py +271 -0
  104. stage/normalize/terms.py +153 -0
  105. stage/normalize/urls.py +122 -0
  106. stage/paths.py +86 -0
  107. stage/py.typed +0 -0
  108. stage/services/__init__.py +0 -0
  109. stage/services/canary.py +120 -0
  110. stage/services/coverage.py +231 -0
  111. stage/services/discover.py +747 -0
  112. stage/services/export.py +274 -0
  113. stage/services/health.py +237 -0
  114. stage/services/maintenance.py +225 -0
  115. stage/services/quarantine.py +20 -0
  116. stage/services/query.py +86 -0
  117. stage/services/sync.py +1257 -0
  118. stage/sources/__init__.py +82 -0
  119. stage/sources/_text.py +79 -0
  120. stage/sources/ashby.py +93 -0
  121. stage/sources/bamboohr.py +80 -0
  122. stage/sources/base.py +225 -0
  123. stage/sources/breezy.py +90 -0
  124. stage/sources/collage.py +60 -0
  125. stage/sources/community_feeds.py +142 -0
  126. stage/sources/curated_markdown.py +289 -0
  127. stage/sources/custom_json.py +610 -0
  128. stage/sources/espresso.py +154 -0
  129. stage/sources/feed.py +44 -0
  130. stage/sources/greenhouse.py +104 -0
  131. stage/sources/jobbank.py +147 -0
  132. stage/sources/jobvite.py +133 -0
  133. stage/sources/lever.py +76 -0
  134. stage/sources/oracle_cloud.py +187 -0
  135. stage/sources/platforms.py +609 -0
  136. stage/sources/quebec_emploi.py +146 -0
  137. stage/sources/recruitee.py +96 -0
  138. stage/sources/simplify.py +110 -0
  139. stage/sources/smartrecruiters.py +216 -0
  140. stage/sources/speedyapply.py +200 -0
  141. stage/sources/themuse.py +157 -0
  142. stage/sources/workable.py +83 -0
  143. stage/sources/workday.py +524 -0
  144. stage/sources/zshah.py +99 -0
  145. stage/storage/__init__.py +29 -0
  146. stage/storage/migrations/0001_initial.sql +239 -0
  147. stage/storage/migrations/__init__.py +135 -0
  148. stage/storage/repository.py +213 -0
  149. stage/storage/search.py +28 -0
  150. stage/storage/sqlite_repo.py +1586 -0
  151. stage/storage/writer.py +249 -0
  152. stage/tui/__init__.py +0 -0
  153. stage/tui/app.py +82 -0
  154. stage/tui/help.py +26 -0
  155. stage/tui/safe.py +21 -0
  156. stage/tui/screens/__init__.py +0 -0
  157. stage/tui/screens/boards.py +186 -0
  158. stage/tui/screens/postings.py +509 -0
  159. stage/tui/screens/review.py +209 -0
  160. stage/tui/screens/splash.py +37 -0
  161. stage/tui/screens/stats.py +124 -0
  162. stage/tui/screens/sync.py +194 -0
  163. stage/tui/state.py +160 -0
  164. stage/tui/theme.tcss +205 -0
  165. stage/tui/widgets/__init__.py +0 -0
  166. stage_cli-1.0.0.dist-info/METADATA +379 -0
  167. stage_cli-1.0.0.dist-info/RECORD +170 -0
  168. stage_cli-1.0.0.dist-info/WHEEL +4 -0
  169. stage_cli-1.0.0.dist-info/entry_points.txt +2 -0
  170. stage_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
stage/classify/role.py ADDED
@@ -0,0 +1,91 @@
1
+ from dataclasses import dataclass
2
+ from functools import lru_cache
3
+
4
+ from stage.domain import RoleCategory
5
+ from stage.lexicon import fold, role_lexicon, source_role_categories
6
+
7
+ GENERAL_CS = RoleCategory.GENERAL_CS.value
8
+ SWE = RoleCategory.SWE.value
9
+ TECHNOLOGY_NOT_DISCIPLINE = frozenset({"gpu", "fpga", "soc", "asic", "rtos", "cuda"})
10
+ OUTRANKED_BY_A_LEADING_QUALIFIER = (SWE,)
11
+
12
+
13
+ @lru_cache(maxsize=1)
14
+ def _by_first_token() -> dict[str, tuple[tuple[str, str], ...]]:
15
+ index: dict[str, list[tuple[str, str]]] = {}
16
+ for category, phrases in role_lexicon().items():
17
+ for phrase in phrases:
18
+ index.setdefault(phrase.split(" ", 1)[0], []).append((category, phrase))
19
+ return {token: tuple(entries) for token, entries in index.items()}
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class RoleVerdict:
24
+ role: RoleCategory = RoleCategory.UNKNOWN
25
+ matched: tuple[str, ...] = ()
26
+ ambiguous: bool = False
27
+
28
+
29
+ def _leading_specialist(folded: str, hits: dict[str, list[str]]) -> str | None:
30
+ outranked = [category for category in OUTRANKED_BY_A_LEADING_QUALIFIER if category in hits]
31
+ if not outranked or len(hits) < 2:
32
+ return None
33
+ if len(outranked) > 1:
34
+ return min(outranked, key=lambda c: min(folded.find(p) for p in hits[c]))
35
+ generic = min(folded.find(phrase) for category in outranked for phrase in hits[category])
36
+ ahead = {
37
+ category: min(
38
+ folded.find(phrase) for phrase in phrases if phrase not in TECHNOLOGY_NOT_DISCIPLINE
39
+ )
40
+ for category, phrases in hits.items()
41
+ if category not in outranked
42
+ and any(phrase not in TECHNOLOGY_NOT_DISCIPLINE for phrase in phrases)
43
+ }
44
+ ahead = {category: at for category, at in ahead.items() if 0 <= at < generic}
45
+ if len(ahead) != 1:
46
+ return None
47
+ return next(iter(ahead))
48
+
49
+
50
+ def classify_role(title: str, description: str = "", source_category: str = "") -> RoleVerdict:
51
+ index = _by_first_token()
52
+ declared = source_role_categories().get(fold(source_category)) if source_category else None
53
+ for text in (title, description):
54
+ folded = fold(text)
55
+ if not folded:
56
+ continue
57
+ padded = f" {folded} "
58
+ hits: dict[str, list[str]] = {}
59
+ for token in set(folded.split()):
60
+ for category, phrase in index.get(token, ()):
61
+ if f" {phrase} " in padded:
62
+ hits.setdefault(category, []).append(phrase)
63
+ if not hits:
64
+ continue
65
+ if len(hits) > 1:
66
+ hits.pop(GENERAL_CS, None)
67
+ hits = {
68
+ category: sorted(set(found), key=len, reverse=True) for category, found in hits.items()
69
+ }
70
+
71
+ leading = _leading_specialist(folded, hits) if text is title else None
72
+ if leading is not None:
73
+ matched = tuple(sorted({phrase for phrases in hits.values() for phrase in phrases}))
74
+ return RoleVerdict(role=RoleCategory(leading), matched=matched)
75
+
76
+ best = max(len(phrases[0]) for phrases in hits.values())
77
+ winners = [category for category, phrases in hits.items() if len(phrases[0]) == best]
78
+ matched = tuple(sorted({phrase for phrases in hits.values() for phrase in phrases}))
79
+ if len(winners) != 1:
80
+ winner = min(
81
+ winners,
82
+ key=lambda category: (
83
+ min(folded.find(phrase) for phrase in hits[category] if len(phrase) == best),
84
+ category,
85
+ ),
86
+ )
87
+ return RoleVerdict(role=RoleCategory(winner), matched=matched)
88
+ return RoleVerdict(role=RoleCategory(winners[0]), matched=matched)
89
+ if declared is not None:
90
+ return RoleVerdict(role=RoleCategory(declared), matched=(fold(source_category),))
91
+ return RoleVerdict()
@@ -0,0 +1,47 @@
1
+ from dataclasses import dataclass
2
+
3
+ from stage.domain import Job, LocationBucket, QuarantinedJob, RejectionReason
4
+
5
+
6
+ @dataclass(frozen=True, slots=True)
7
+ class Rejection:
8
+ reason: RejectionReason
9
+ matched_phrase: str
10
+
11
+
12
+ def screen_is_internship(job: Job) -> Rejection | None:
13
+ from stage.classify.internship import screen_internship
14
+
15
+ verdict = screen_internship(job.title_raw, job.signals.employment_type, job.signals.category)
16
+ if verdict.is_internship:
17
+ return None
18
+ return Rejection(
19
+ reason=RejectionReason.NOT_AN_INTERNSHIP,
20
+ matched_phrase=verdict.disqualified_by or "no internship marker in title",
21
+ )
22
+
23
+
24
+ def screen_location(job: Job) -> Rejection | None:
25
+ if job.location is not LocationBucket.INTERNATIONAL:
26
+ return None
27
+ return Rejection(
28
+ reason=RejectionReason.OUT_OF_SCOPE_LOCATION,
29
+ matched_phrase=job.location_raw or "outside Canada and the United States",
30
+ )
31
+
32
+
33
+ def to_quarantined(job: Job, rejection: Rejection) -> QuarantinedJob:
34
+ return QuarantinedJob(
35
+ id=job.id,
36
+ source=job.source,
37
+ company=job.company,
38
+ title_raw=job.title_raw,
39
+ reason=rejection.reason,
40
+ first_seen=job.first_seen,
41
+ last_seen=job.last_seen,
42
+ apply_url_raw=job.apply_url_raw,
43
+ location_raw=job.location_raw,
44
+ location=job.location,
45
+ remote_scope=job.remote_scope,
46
+ matched_phrase=rejection.matched_phrase,
47
+ )
stage/cli/__init__.py ADDED
File without changes
stage/cli/app.py ADDED
@@ -0,0 +1,4 @@
1
+ from stage.cli import commands
2
+ from stage.cli.options import app, main, run_async, show_help
3
+
4
+ __all__ = ["app", "commands", "main", "run_async", "show_help"]
@@ -0,0 +1,8 @@
1
+ import importlib
2
+
3
+ _ORDER = ("postings", "pipeline", "schedule", "insight", "discovery")
4
+
5
+ for _name in _ORDER:
6
+ importlib.import_module(f"{__name__}.{_name}")
7
+
8
+ __all__ = list(_ORDER)
@@ -0,0 +1,294 @@
1
+ from collections.abc import AsyncIterator
2
+ from pathlib import Path
3
+ from typing import Annotated, Any
4
+
5
+ import typer
6
+
7
+ from stage.cli.options import (
8
+ WORD,
9
+ DatabaseOption,
10
+ InvalidOptionError,
11
+ RegistryOption,
12
+ _adopt_unregistered,
13
+ _count,
14
+ _lock_path,
15
+ _parse_enum,
16
+ _print_failure,
17
+ _require_enum,
18
+ app,
19
+ run_async,
20
+ )
21
+
22
+
23
+ @app.command(
24
+ help="Find the job-board platform a company uses", rich_help_panel="Registry and maintenance"
25
+ )
26
+ def discover(
27
+ companies: Annotated[
28
+ list[str] | None,
29
+ typer.Argument(metavar="NAME...", click_type=WORD, help="Company names to probe"),
30
+ ] = None,
31
+ url: Annotated[
32
+ str | None,
33
+ typer.Option(
34
+ "--url",
35
+ metavar="URL",
36
+ help="Extract the platform from a careers URL without fetching that URL",
37
+ ),
38
+ ] = None,
39
+ name: Annotated[
40
+ str | None,
41
+ typer.Option("--name", metavar="NAME", help="Company name to show with --url"),
42
+ ] = None,
43
+ platform: Annotated[
44
+ list[str] | None,
45
+ typer.Option(
46
+ "--platform", metavar="PLATFORM", help="Probe only these platform adapters; repeatable"
47
+ ),
48
+ ] = None,
49
+ exclude: Annotated[
50
+ list[str] | None,
51
+ typer.Option(
52
+ "--exclude",
53
+ metavar="NAME",
54
+ help="Skip these platform adapters while probing; repeatable",
55
+ ),
56
+ ] = None,
57
+ only: Annotated[
58
+ list[str] | None,
59
+ typer.Option(
60
+ "--company",
61
+ metavar="NAME",
62
+ help="With --verify, recheck only these registry companies; repeatable",
63
+ ),
64
+ ] = None,
65
+ expect_size: Annotated[
66
+ str | None,
67
+ typer.Option(
68
+ "--expect-size",
69
+ metavar="SIZE",
70
+ help="Optional employer-size hint: startup, mid, large",
71
+ ),
72
+ ] = None,
73
+ request_log: Annotated[
74
+ Path | None,
75
+ typer.Option(
76
+ "--request-log",
77
+ metavar="FILE",
78
+ help="Write outbound HTTP requests to this JSON Lines file",
79
+ ),
80
+ ] = None,
81
+ scheduled_progress: Annotated[
82
+ Path | None,
83
+ typer.Option("--scheduled-progress", hidden=True),
84
+ ] = None,
85
+ verify: Annotated[
86
+ bool,
87
+ typer.Option(
88
+ "--verify",
89
+ help="Recheck existing registry companies",
90
+ ),
91
+ ] = False,
92
+ unregistered: Annotated[
93
+ bool,
94
+ typer.Option(
95
+ "--unregistered",
96
+ help="Probe companies found in feeds but missing from the registry",
97
+ ),
98
+ ] = False,
99
+ direct_only: Annotated[
100
+ bool,
101
+ typer.Option(
102
+ "--direct-only",
103
+ help="With --unregistered, verify only ATS links already present in feed applications",
104
+ ),
105
+ ] = False,
106
+ adopt_unnamed: Annotated[
107
+ bool,
108
+ typer.Option(
109
+ "--adopt-unnamed",
110
+ help="With --direct-only, also adopt boards whose platform publishes no board name",
111
+ ),
112
+ ] = False,
113
+ limit: Annotated[
114
+ int,
115
+ typer.Option(
116
+ "--limit",
117
+ metavar="N",
118
+ click_type=_count(1, 2000),
119
+ help="Maximum unregistered company names to probe",
120
+ ),
121
+ ] = 40,
122
+ show_all: Annotated[
123
+ bool,
124
+ typer.Option("--all", help="List every result instead of the first few"),
125
+ ] = False,
126
+ db: DatabaseOption = None,
127
+ apply: Annotated[
128
+ bool,
129
+ typer.Option(
130
+ "--apply",
131
+ help="Write results to the registry; requires --verify or --unregistered",
132
+ ),
133
+ ] = False,
134
+ registry: RegistryOption = None,
135
+ ) -> None:
136
+ from contextlib import ExitStack
137
+
138
+ from stage.cli.logfile import open_request_log
139
+ from stage.cli.render import failure, plain, render_discovery, terminal
140
+ from stage.cli.runlock import AnotherRunInProgressError, single_run
141
+ from stage.cli.schedule_state import ScheduleStateWriter
142
+ from stage.companies import RegistryError
143
+ from stage.companies import load_companies as load_registry
144
+ from stage.domain import DiscoveryEvent, DiscoveryFinished, EmployerSize, Platform
145
+ from stage.services.discover import NoMatchingCompanyError
146
+
147
+ console = terminal()
148
+ scheduled_state = (
149
+ ScheduleStateWriter.open(scheduled_progress, "discover")
150
+ if scheduled_progress is not None
151
+ else None
152
+ )
153
+ progress = scheduled_state.discovery_event if scheduled_state is not None else None
154
+
155
+ if url is not None and companies:
156
+ console.print("[red]Pass either company names or --url, not both.[/red]")
157
+ raise typer.Exit(code=2)
158
+ if verify and unregistered:
159
+ console.print("[red]Pass either --verify or --unregistered, not both.[/red]")
160
+ raise typer.Exit(code=2)
161
+ if direct_only and not unregistered:
162
+ console.print("[red]--direct-only requires --unregistered.[/red]")
163
+ raise typer.Exit(code=2)
164
+ if adopt_unnamed and not direct_only:
165
+ console.print(
166
+ "[red]--adopt-unnamed requires --direct-only[/red] — the token must come from an "
167
+ "apply URL."
168
+ )
169
+ raise typer.Exit(code=2)
170
+ if apply and not (verify or unregistered):
171
+ console.print("[red]--apply only means something with --verify or --unregistered.[/red]")
172
+ raise typer.Exit(code=2)
173
+ if platform and exclude:
174
+ console.print("[red]Pass either --platform or --exclude, not both.[/red]")
175
+ raise typer.Exit(code=2)
176
+ if not verify and not unregistered and url is None and not companies:
177
+ console.print(
178
+ "[red]Nothing to discover.[/red] Pass a company name, a careers page with "
179
+ "[bold]--url[/bold], or [bold]--verify[/bold] to re-probe the registry."
180
+ )
181
+ raise typer.Exit(code=2)
182
+
183
+ try:
184
+ size = _parse_enum(expect_size, EmployerSize, "--expect-size")
185
+ platforms = (
186
+ [_require_enum(value, Platform, "--platform") for value in platform]
187
+ if platform
188
+ else None
189
+ )
190
+ excluded = (
191
+ [_require_enum(value, Platform, "--exclude") for value in exclude] if exclude else None
192
+ )
193
+ except InvalidOptionError as exc:
194
+ _print_failure(exc)
195
+ raise typer.Exit(code=2) from exc
196
+
197
+ async def run() -> object:
198
+ from datetime import UTC, datetime
199
+
200
+ from stage.services.discover import probe_companies, resolve_careers_url
201
+
202
+ today = datetime.now(UTC).date()
203
+ with ExitStack() as stack:
204
+ stream = (
205
+ stack.enter_context(open_request_log(request_log))
206
+ if request_log is not None
207
+ else None
208
+ )
209
+ if verify:
210
+ from stage.companies import load_companies, update_registry
211
+ from stage.services.discover import apply_verification, verify_registry
212
+
213
+ rows = checked if checked is not None else load_companies(registry)
214
+ outcome = await render_discovery(
215
+ console,
216
+ verify_registry(rows, platforms=platforms, excluded=excluded, only=only),
217
+ verified_on=today,
218
+ request_log=stream,
219
+ collect=True,
220
+ progress=progress,
221
+ )
222
+ if apply and isinstance(outcome, DiscoveryFinished):
223
+
224
+ def update(
225
+ existing: tuple[Any, ...],
226
+ ) -> tuple[tuple[Any, ...], tuple[int, int]]:
227
+ updated, ok, off = apply_verification(existing, outcome, today)
228
+ return updated, (ok, off)
229
+
230
+ target, (ok, off) = update_registry(update, registry)
231
+ console.print(
232
+ plain(
233
+ f"\napplied — {ok} row(s) verified, {off} disabled, written to {target}"
234
+ )
235
+ )
236
+ return isinstance(outcome, DiscoveryFinished) and bool(outcome.matched)
237
+ if url is not None:
238
+
239
+ async def once() -> AsyncIterator[DiscoveryEvent]:
240
+ yield resolve_careers_url(url)
241
+
242
+ return await render_discovery(console, once(), display_name=name, progress=progress)
243
+
244
+ if unregistered:
245
+ adopted = await _adopt_unregistered(
246
+ console,
247
+ registry=registry,
248
+ db=db,
249
+ platforms=platforms,
250
+ excluded=excluded,
251
+ size=size,
252
+ limit=limit,
253
+ direct_only=direct_only,
254
+ adopt_unnamed=adopt_unnamed,
255
+ apply_rows=apply,
256
+ today=today,
257
+ stream=stream,
258
+ progress=progress,
259
+ show_all=show_all,
260
+ )
261
+ if scheduled_state is not None:
262
+ scheduled_state.heartbeat()
263
+ return adopted
264
+
265
+ events = probe_companies(
266
+ companies or [], platforms=platforms, excluded=excluded, size=size
267
+ )
268
+ return await render_discovery(
269
+ console, events, verified_on=today, request_log=stream, progress=progress
270
+ )
271
+
272
+ checked = None
273
+ if verify:
274
+ try:
275
+ checked = load_registry(registry)
276
+ except RegistryError as exc:
277
+ console.print(failure(exc))
278
+ raise typer.Exit(code=2) from exc
279
+
280
+ try:
281
+ if url is not None:
282
+ resolved = run_async(run())
283
+ else:
284
+ with single_run("discover", _lock_path(db)):
285
+ resolved = run_async(run())
286
+ except AnotherRunInProgressError as exc:
287
+ if scheduled_state is not None:
288
+ scheduled_state.blocked(str(exc))
289
+ console.print(failure(exc))
290
+ raise typer.Exit(code=2) from exc
291
+ except (RegistryError, NoMatchingCompanyError) as exc:
292
+ console.print(failure(exc))
293
+ raise typer.Exit(code=2) from exc
294
+ raise typer.Exit(code=0 if resolved or unregistered else 1)