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
@@ -0,0 +1,244 @@
1
+ from stage.domain.company import Company
2
+ from stage.domain.coverage import (
3
+ CoverageClassification,
4
+ CoverageDisposition,
5
+ CoverageRow,
6
+ CoverageState,
7
+ UnregisteredCompany,
8
+ coverage_state,
9
+ )
10
+ from stage.domain.custom_board import (
11
+ KNOWN_FIELDS,
12
+ REQUIRED_FIELDS,
13
+ TITLE_FIELD,
14
+ CustomBoard,
15
+ )
16
+ from stage.domain.discovery import (
17
+ CandidateSkipped,
18
+ DiscoveryEvent,
19
+ DiscoveryFinished,
20
+ DiscoveryStarted,
21
+ PlatformCandidate,
22
+ PlatformProbed,
23
+ ProbeResult,
24
+ UrlResolved,
25
+ UrlUnrecognized,
26
+ )
27
+ from stage.domain.enums import (
28
+ UNKNOWN_TERM,
29
+ DegreeRequirement,
30
+ EmployerSize,
31
+ ExportFormat,
32
+ JobStatus,
33
+ Language,
34
+ LocationBucket,
35
+ Platform,
36
+ ProbeVerdict,
37
+ RemoteScope,
38
+ RoleCategory,
39
+ SourceOfRecord,
40
+ SyncOutcome,
41
+ )
42
+ from stage.domain.events import (
43
+ BucketPlan,
44
+ CompanyDeferred,
45
+ CompanyFailed,
46
+ CompanyFinished,
47
+ CompanyStarted,
48
+ CompanyUnchanged,
49
+ PlannedRequest,
50
+ RequestLogged,
51
+ SourceBlocked,
52
+ SourceCapped,
53
+ SourceFailed,
54
+ SourceFinished,
55
+ SourceFresh,
56
+ SourceResting,
57
+ SourceRotated,
58
+ SourceStarted,
59
+ SyncEvent,
60
+ SyncFinished,
61
+ SyncStarted,
62
+ UnroutableCompanies,
63
+ )
64
+ from stage.domain.filters import DEFAULT_LIMIT, DEFAULT_WINDOW_DAYS, JobFilters
65
+ from stage.domain.health import (
66
+ MIN_VOLUME_HISTORY,
67
+ STALE_AFTER_DAYS,
68
+ UNRECORDED_VOLUME,
69
+ VOLUME_DROP_RATIO,
70
+ IntegrityFinding,
71
+ IntegrityRepair,
72
+ VisitState,
73
+ VolumePoint,
74
+ VolumeSignal,
75
+ VolumeVerdict,
76
+ assess_volume,
77
+ classify_visit,
78
+ )
79
+ from stage.domain.ids import board_key, board_of, job_id
80
+ from stage.domain.job import Job
81
+ from stage.domain.matching import location_agrees, term_agrees
82
+ from stage.domain.priority import SOURCE_PRIORITY, source_rank
83
+ from stage.domain.quarantine import (
84
+ DEFAULT_QUARANTINE_LIMIT,
85
+ QuarantinedJob,
86
+ QuarantineFilters,
87
+ RejectionReason,
88
+ )
89
+ from stage.domain.rate_state import (
90
+ BLOCK_CAP_S,
91
+ BREAKER_THRESHOLD,
92
+ RateState,
93
+ block_duration,
94
+ blocked,
95
+ decay,
96
+ )
97
+ from stage.domain.retention import (
98
+ CLOSED_RETENTION_DAYS,
99
+ OPEN_RETENTION_DAYS,
100
+ PurgeResult,
101
+ Tombstone,
102
+ )
103
+ from stage.domain.rotation import Rotation, RotationMember, rotate
104
+ from stage.domain.signals import SourceSignals
105
+ from stage.domain.sync_run import SourceRunStats, SyncRun
106
+ from stage.domain.text import (
107
+ dump,
108
+ first_line,
109
+ graphemes,
110
+ json_safe,
111
+ public_https_url,
112
+ sanitize,
113
+ summary,
114
+ truncate,
115
+ web_url,
116
+ )
117
+ from stage.domain.validator import HttpValidator
118
+ from stage.domain.visits import (
119
+ REST_AFTER_FAILURES,
120
+ CompanyVisit,
121
+ DetailFetch,
122
+ SourceVisit,
123
+ is_resting,
124
+ rested_until,
125
+ )
126
+ from stage.domain.workday import WorkdayCrawl, WorkdayCrawlStep, WorkdayFacet
127
+
128
+ __all__ = [
129
+ "DegreeRequirement",
130
+ "BLOCK_CAP_S",
131
+ "BREAKER_THRESHOLD",
132
+ "BucketPlan",
133
+ "CLOSED_RETENTION_DAYS",
134
+ "DEFAULT_LIMIT",
135
+ "DEFAULT_QUARANTINE_LIMIT",
136
+ "DEFAULT_WINDOW_DAYS",
137
+ "UNKNOWN_TERM",
138
+ "CandidateSkipped",
139
+ "Company",
140
+ "CustomBoard",
141
+ "KNOWN_FIELDS",
142
+ "REQUIRED_FIELDS",
143
+ "TITLE_FIELD",
144
+ "CompanyVisit",
145
+ "DetailFetch",
146
+ "CompanyDeferred",
147
+ "CompanyFailed",
148
+ "CompanyFinished",
149
+ "CompanyStarted",
150
+ "CompanyUnchanged",
151
+ "DiscoveryEvent",
152
+ "DiscoveryFinished",
153
+ "DiscoveryStarted",
154
+ "EmployerSize",
155
+ "ExportFormat",
156
+ "HttpValidator",
157
+ "Job",
158
+ "JobFilters",
159
+ "JobStatus",
160
+ "Language",
161
+ "LocationBucket",
162
+ "Platform",
163
+ "PlatformCandidate",
164
+ "PlatformProbed",
165
+ "PlannedRequest",
166
+ "OPEN_RETENTION_DAYS",
167
+ "SOURCE_PRIORITY",
168
+ "PurgeResult",
169
+ "ProbeResult",
170
+ "ProbeVerdict",
171
+ "QuarantineFilters",
172
+ "QuarantinedJob",
173
+ "RateState",
174
+ "RejectionReason",
175
+ "RequestLogged",
176
+ "RemoteScope",
177
+ "RoleCategory",
178
+ "Rotation",
179
+ "RotationMember",
180
+ "SourceBlocked",
181
+ "SourceFailed",
182
+ "SourceFinished",
183
+ "SourceCapped",
184
+ "SourceFresh",
185
+ "SourceResting",
186
+ "SourceRotated",
187
+ "SourceOfRecord",
188
+ "SourceRunStats",
189
+ "REST_AFTER_FAILURES",
190
+ "SourceVisit",
191
+ "is_resting",
192
+ "rested_until",
193
+ "SourceSignals",
194
+ "SourceStarted",
195
+ "SyncEvent",
196
+ "SyncFinished",
197
+ "SyncOutcome",
198
+ "SyncRun",
199
+ "SyncStarted",
200
+ "Tombstone",
201
+ "UnroutableCompanies",
202
+ "UrlResolved",
203
+ "UrlUnrecognized",
204
+ "WorkdayFacet",
205
+ "WorkdayCrawl",
206
+ "WorkdayCrawlStep",
207
+ "block_duration",
208
+ "blocked",
209
+ "decay",
210
+ "rotate",
211
+ "MIN_VOLUME_HISTORY",
212
+ "STALE_AFTER_DAYS",
213
+ "UNRECORDED_VOLUME",
214
+ "VOLUME_DROP_RATIO",
215
+ "IntegrityFinding",
216
+ "IntegrityRepair",
217
+ "VisitState",
218
+ "VolumePoint",
219
+ "VolumeSignal",
220
+ "VolumeVerdict",
221
+ "assess_volume",
222
+ "board_key",
223
+ "board_of",
224
+ "classify_visit",
225
+ "CoverageRow",
226
+ "CoverageClassification",
227
+ "CoverageDisposition",
228
+ "CoverageState",
229
+ "UnregisteredCompany",
230
+ "coverage_state",
231
+ "dump",
232
+ "first_line",
233
+ "graphemes",
234
+ "job_id",
235
+ "json_safe",
236
+ "location_agrees",
237
+ "public_https_url",
238
+ "sanitize",
239
+ "source_rank",
240
+ "summary",
241
+ "term_agrees",
242
+ "truncate",
243
+ "web_url",
244
+ ]
@@ -0,0 +1,49 @@
1
+ from dataclasses import dataclass
2
+ from datetime import date
3
+
4
+ from stage.domain.custom_board import CustomBoard
5
+ from stage.domain.enums import Platform, SourceOfRecord
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class Company:
10
+ name: str
11
+ platform: Platform
12
+ slug: str
13
+ enabled: bool = True
14
+ rate_profile: str | None = None
15
+ last_verified: date | None = None
16
+ source_of_record: SourceOfRecord = SourceOfRecord.MANUAL
17
+ workday_tenant: str | None = None
18
+ workday_site: str | None = None
19
+ workday_dc: str | None = None
20
+ workday_facet: str | None = None
21
+ oracle_host: str | None = None
22
+ oracle_site: str | None = None
23
+ oracle_keyword: str | None = None
24
+ name_gate_exempt: bool = False
25
+ notes: str | None = None
26
+ recheck_after: date | None = None
27
+ paused_until: date | None = None
28
+ custom: CustomBoard | None = None
29
+
30
+ def due_for_recheck(self, today: date) -> bool:
31
+ return self.recheck_after is not None and self.recheck_after <= today
32
+
33
+ def pause_elapsed(self, today: date) -> bool:
34
+ return self.paused_until is not None and self.paused_until <= today
35
+
36
+ @property
37
+ def registry_key(self) -> str:
38
+ parts = [self.platform.value, self.slug]
39
+ parts.extend(
40
+ part
41
+ for part in (
42
+ self.workday_site,
43
+ self.workday_dc,
44
+ self.oracle_host,
45
+ self.oracle_site,
46
+ )
47
+ if part
48
+ )
49
+ return ":".join(parts)
@@ -0,0 +1,86 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime
3
+ from enum import StrEnum
4
+
5
+ from stage.domain.health import VisitState, classify_visit
6
+
7
+
8
+ class CoverageState(StrEnum):
9
+ PRODUCING = "producing"
10
+ EMPTY = "empty"
11
+ FAILING = "failing"
12
+ STALE = "stale"
13
+ NEVER_REACHED = "never-reached"
14
+ UNROUTABLE = "unroutable"
15
+
16
+
17
+ class CoverageDisposition(StrEnum):
18
+ FEED_ONLY = "feed-only"
19
+ UNAVAILABLE = "unavailable"
20
+ CUSTOM_JSON_CANDIDATE = "custom-json-candidate"
21
+ ADAPTER_CANDIDATE = "adapter-candidate"
22
+ DEFERRED = "deferred"
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class CoverageRow:
27
+ company: str
28
+ platform: str
29
+ board: str
30
+ state: CoverageState
31
+ postings: int
32
+ last_success_at: datetime | None
33
+ consecutive_failures: int
34
+ last_error: str
35
+
36
+ @property
37
+ def is_gap(self) -> bool:
38
+ return self.state is CoverageState.EMPTY
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class UnregisteredCompany:
43
+ company: str
44
+ sources: tuple[str, ...]
45
+ postings: int
46
+ quarantined: int = 0
47
+ posts_internships: bool = False
48
+
49
+
50
+ @dataclass(frozen=True, slots=True)
51
+ class CoverageClassification:
52
+ company: str
53
+ disposition: CoverageDisposition
54
+ note: str
55
+ checked_on: datetime
56
+ url: str | None = None
57
+
58
+
59
+ def coverage_state(
60
+ postings: int,
61
+ visited: bool,
62
+ last_success_at: datetime | None,
63
+ consecutive_failures: int,
64
+ now: datetime,
65
+ stale_after_days: int,
66
+ ) -> CoverageState:
67
+ if postings > 0:
68
+ return CoverageState.PRODUCING
69
+ if not visited:
70
+ return CoverageState.NEVER_REACHED
71
+ visit = classify_visit(last_success_at, consecutive_failures, now, stale_after_days)
72
+ if visit is VisitState.FAILING:
73
+ return CoverageState.FAILING
74
+ if visit is VisitState.STALE:
75
+ return CoverageState.STALE
76
+ return CoverageState.EMPTY
77
+
78
+
79
+ __all__ = [
80
+ "CoverageRow",
81
+ "CoverageClassification",
82
+ "CoverageDisposition",
83
+ "CoverageState",
84
+ "UnregisteredCompany",
85
+ "coverage_state",
86
+ ]
@@ -0,0 +1,92 @@
1
+ from collections.abc import Mapping
2
+ from dataclasses import dataclass, field
3
+ from types import MappingProxyType
4
+ from typing import Any
5
+
6
+ TITLE_FIELD = "title"
7
+ REQUIRED_FIELDS = (TITLE_FIELD,)
8
+ KNOWN_FIELDS = (
9
+ TITLE_FIELD,
10
+ "id",
11
+ "location",
12
+ "url",
13
+ "description",
14
+ "department",
15
+ "employment_type",
16
+ "category",
17
+ )
18
+
19
+ _EMPTY: Mapping[str, str] = MappingProxyType({})
20
+ _EMPTY_BODY: Mapping[str, Any] = MappingProxyType({})
21
+
22
+
23
+ @dataclass(frozen=True, slots=True)
24
+ class CustomBoard:
25
+ url: str
26
+ jobs_path: str = ""
27
+ fields: Mapping[str, str] = field(default_factory=lambda: _EMPTY)
28
+ url_template: str = ""
29
+ page_param: str = ""
30
+ page_size: int = 0
31
+ page_start: int = 0
32
+ page_step: int = 0
33
+ max_pages: int = 0
34
+ method: str = "GET"
35
+ fmt: str = "json"
36
+ extract: str = ""
37
+ row_selector: str = ""
38
+ row_filter: str = ""
39
+ item_tag: str = "item"
40
+ handshake_url: str = ""
41
+ token_pattern: str = ""
42
+ token_header: str = ""
43
+ token_prefix: str = ""
44
+ body: Mapping[str, Any] = field(default_factory=lambda: _EMPTY_BODY)
45
+ headers: Mapping[str, str] = field(default_factory=lambda: _EMPTY)
46
+ authoritative: bool = True
47
+
48
+ def mapped(self, name: str) -> str:
49
+ return self.fields.get(name, "")
50
+
51
+ @property
52
+ def embedded(self) -> bool:
53
+ return bool(self.extract)
54
+
55
+ @property
56
+ def rss(self) -> bool:
57
+ return self.fmt == "rss"
58
+
59
+ @property
60
+ def html(self) -> bool:
61
+ return self.fmt == "html"
62
+
63
+ @property
64
+ def sitemap(self) -> bool:
65
+ return self.fmt == "sitemap"
66
+
67
+ @property
68
+ def jsonld(self) -> bool:
69
+ return self.fmt == "jsonld"
70
+
71
+ @property
72
+ def handshakes(self) -> bool:
73
+ return bool(self.handshake_url and self.token_pattern and self.token_header)
74
+
75
+ @property
76
+ def posts(self) -> bool:
77
+ return self.method.upper() == "POST"
78
+
79
+ @property
80
+ def paginated(self) -> bool:
81
+ return bool(self.page_param) and self.page_size > 0
82
+
83
+ def page_budget(self, default: int, ceiling: int) -> int:
84
+ if not self.paginated:
85
+ return 1
86
+ return min(ceiling, self.max_pages or default)
87
+
88
+ def page_value(self, index: int) -> int:
89
+ return self.page_start + index * (self.page_step or self.page_size)
90
+
91
+
92
+ __all__ = ["KNOWN_FIELDS", "REQUIRED_FIELDS", "TITLE_FIELD", "CustomBoard"]
@@ -0,0 +1,94 @@
1
+ from dataclasses import dataclass
2
+
3
+ from stage.domain.enums import Platform, ProbeVerdict
4
+ from stage.domain.events import RequestLogged
5
+
6
+
7
+ @dataclass(frozen=True, slots=True)
8
+ class PlatformCandidate:
9
+ platform: Platform
10
+ slug: str
11
+ workday_tenant: str | None = None
12
+ workday_site: str | None = None
13
+ workday_dc: str | None = None
14
+ oracle_host: str | None = None
15
+ oracle_site: str | None = None
16
+ resolves_board: bool = True
17
+
18
+ @property
19
+ def label(self) -> str:
20
+ if not self.resolves_board:
21
+ return f"{self.platform.value}/{self.slug} (front end only — board unresolved)"
22
+ if self.workday_tenant is not None:
23
+ site = self.workday_site or "?"
24
+ return f"{self.platform.value}/{self.workday_tenant}/{site}@{self.workday_dc or '?'}"
25
+ if self.oracle_host is not None:
26
+ return f"{self.platform.value}/{self.oracle_host}/{self.oracle_site or '?'}"
27
+ return f"{self.platform.value}/{self.slug}"
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class ProbeResult:
32
+ company: str
33
+ candidate: PlatformCandidate
34
+ verdict: ProbeVerdict
35
+ url: str
36
+ board_name: str = ""
37
+ job_count: int | None = None
38
+ detail: str = ""
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class DiscoveryStarted:
43
+ companies: tuple[str, ...]
44
+ platforms: tuple[str, ...]
45
+ probes_planned: int
46
+
47
+
48
+ @dataclass(frozen=True, slots=True)
49
+ class CandidateSkipped:
50
+ company: str
51
+ slug: str
52
+ reason: str
53
+
54
+
55
+ @dataclass(frozen=True, slots=True)
56
+ class PlatformProbed:
57
+ result: ProbeResult
58
+
59
+
60
+ @dataclass(frozen=True, slots=True)
61
+ class UrlResolved:
62
+ url: str
63
+ candidate: PlatformCandidate
64
+ detail: str = ""
65
+
66
+
67
+ @dataclass(frozen=True, slots=True)
68
+ class UrlUnrecognized:
69
+ url: str
70
+ detail: str
71
+
72
+
73
+ @dataclass(frozen=True, slots=True)
74
+ class DiscoveryFinished:
75
+ matched: tuple[ProbeResult, ...]
76
+ unverified: tuple[ProbeResult, ...]
77
+ rejected: tuple[ProbeResult, ...]
78
+ missed: int
79
+ errors: int
80
+ requests: int
81
+ elapsed_ms: float
82
+ ceiling_hit: tuple[str, ...] = ()
83
+ non_json: tuple[tuple[str, int], ...] = ()
84
+
85
+
86
+ DiscoveryEvent = (
87
+ DiscoveryStarted
88
+ | CandidateSkipped
89
+ | PlatformProbed
90
+ | RequestLogged
91
+ | UrlResolved
92
+ | UrlUnrecognized
93
+ | DiscoveryFinished
94
+ )
stage/domain/enums.py ADDED
@@ -0,0 +1,114 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class JobStatus(StrEnum):
5
+ OPEN = "open"
6
+ CLOSED = "closed"
7
+
8
+
9
+ class Language(StrEnum):
10
+ EN = "en"
11
+ FR = "fr"
12
+ BILINGUAL = "bilingual"
13
+
14
+ UNKNOWN = "unknown"
15
+
16
+
17
+ class LocationBucket(StrEnum):
18
+ CANADA = "canada"
19
+ USA = "usa"
20
+ MONTREAL = "montreal"
21
+ INTERNATIONAL = "international"
22
+
23
+ UNKNOWN = "unknown"
24
+
25
+
26
+ class RemoteScope(StrEnum):
27
+ CANADA = "remote-canada"
28
+ US = "remote-us"
29
+ UNSPECIFIED = "remote-unspecified"
30
+
31
+
32
+ class RoleCategory(StrEnum):
33
+ SWE = "swe"
34
+ SECURITY = "security"
35
+ DATA = "data"
36
+ ML_AI = "ml-ai"
37
+ QUANT = "quant"
38
+ INFRA = "infra"
39
+ HARDWARE = "hardware"
40
+ EMBEDDED = "embedded"
41
+ GENERAL_CS = "general-cs"
42
+ UNKNOWN = "unknown"
43
+
44
+
45
+ class DegreeRequirement(StrEnum):
46
+ NONE = "none"
47
+ BACHELORS = "bachelors"
48
+ MASTERS = "masters"
49
+ PHD = "phd"
50
+ UNKNOWN = "unknown"
51
+
52
+
53
+ class Platform(StrEnum):
54
+ GREENHOUSE = "greenhouse"
55
+ LEVER = "lever"
56
+ ASHBY = "ashby"
57
+ SMARTRECRUITERS = "smartrecruiters"
58
+ RECRUITEE = "recruitee"
59
+ WORKABLE = "workable"
60
+ PERSONIO = "personio"
61
+ TEAMTAILOR = "teamtailor"
62
+ BREEZY = "breezy"
63
+ BAMBOOHR = "bamboohr"
64
+ JOBVITE = "jobvite"
65
+ JOIN = "join"
66
+ WORKDAY = "workday"
67
+ TALEO = "taleo"
68
+ ORACLE_CLOUD = "oracle_cloud"
69
+ SUCCESSFACTORS = "successfactors"
70
+ NJOYN = "njoyn"
71
+ ICIMS = "icims"
72
+ AVATURE = "avature"
73
+ PHENOM = "phenom"
74
+ EIGHTFOLD = "eightfold"
75
+ CORNERSTONE = "cornerstone"
76
+ COLLAGE = "collage"
77
+ CUSTOM_JSON = "custom_json"
78
+
79
+
80
+ class SourceOfRecord(StrEnum):
81
+ OPENJOBS = "openjobs"
82
+ DISCOVER = "discover"
83
+ MANUAL = "manual"
84
+
85
+
86
+ class ProbeVerdict(StrEnum):
87
+ MATCH = "match"
88
+ UNVERIFIED = "unverified"
89
+ EMPTY = "empty"
90
+ REJECTED = "rejected"
91
+ MISS = "miss"
92
+ ERROR = "error"
93
+
94
+
95
+ class EmployerSize(StrEnum):
96
+ STARTUP = "startup"
97
+ MID = "mid"
98
+ LARGE = "large"
99
+
100
+
101
+ class SyncOutcome(StrEnum):
102
+ SUCCESS = "success"
103
+ PARTIAL = "partial"
104
+ FAILURE = "failure"
105
+
106
+
107
+ class ExportFormat(StrEnum):
108
+ CSV = "csv"
109
+ JSON = "json"
110
+ MD = "md"
111
+ PDF = "pdf"
112
+
113
+
114
+ UNKNOWN_TERM = "unknown"