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
stage/domain/__init__.py
ADDED
|
@@ -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
|
+
]
|
stage/domain/company.py
ADDED
|
@@ -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)
|
stage/domain/coverage.py
ADDED
|
@@ -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"
|