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,187 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from datetime import UTC, date, datetime
|
|
3
|
+
from typing import Any, ClassVar
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
|
6
|
+
|
|
7
|
+
from stage.domain import Company, Job, Platform, board_key, job_id
|
|
8
|
+
from stage.http import HttpClient
|
|
9
|
+
from stage.sources import register
|
|
10
|
+
from stage.sources._text import collapse_whitespace, strip_html
|
|
11
|
+
from stage.sources.base import (
|
|
12
|
+
FetchResult,
|
|
13
|
+
NullableStr,
|
|
14
|
+
PayloadValidationError,
|
|
15
|
+
capture_payload,
|
|
16
|
+
malformed_note,
|
|
17
|
+
validate_rows,
|
|
18
|
+
)
|
|
19
|
+
from stage.sources.platforms import oracle_target
|
|
20
|
+
|
|
21
|
+
PATH = "/hcmRestApi/resources/latest/recruitingCEJobRequisitions"
|
|
22
|
+
PAGE_SIZE = 100
|
|
23
|
+
MAX_PAGES = 16
|
|
24
|
+
KEYWORD = "internship"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OraclePosting(BaseModel):
|
|
28
|
+
model_config = ConfigDict(extra="ignore")
|
|
29
|
+
|
|
30
|
+
Id: str
|
|
31
|
+
Title: str
|
|
32
|
+
PostedDate: date | None = None
|
|
33
|
+
PrimaryLocation: NullableStr = ""
|
|
34
|
+
PrimaryLocationCountry: NullableStr = ""
|
|
35
|
+
ShortDescriptionStr: NullableStr = ""
|
|
36
|
+
ExternalQualificationsStr: NullableStr = ""
|
|
37
|
+
ExternalResponsibilitiesStr: NullableStr = ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class OraclePage(BaseModel):
|
|
41
|
+
model_config = ConfigDict(extra="ignore")
|
|
42
|
+
|
|
43
|
+
TotalJobsCount: int = Field(ge=0)
|
|
44
|
+
requisitionList: list[Any] = Field(default_factory=list)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@register
|
|
48
|
+
class OracleCloudAdapter:
|
|
49
|
+
name: ClassVar[str] = "oracle_cloud"
|
|
50
|
+
platform: ClassVar[Platform] = Platform.ORACLE_CLOUD
|
|
51
|
+
rate_profile: ClassVar[str] = "paginated"
|
|
52
|
+
hosts: ClassVar[frozenset[str]] = frozenset()
|
|
53
|
+
bucket_key: ClassVar[str] = "oracle_cloud"
|
|
54
|
+
detail_budget: ClassVar[int] = 0
|
|
55
|
+
rotation_slice: ClassVar[int] = 0
|
|
56
|
+
max_requests_per_company: ClassVar[int] = MAX_PAGES
|
|
57
|
+
|
|
58
|
+
def host_for(self, company: Company) -> str:
|
|
59
|
+
host, _ = oracle_target(company.oracle_host or "", company.oracle_site or "")
|
|
60
|
+
return host
|
|
61
|
+
|
|
62
|
+
def hosts_for(self, companies: Sequence[Company]) -> frozenset[str]:
|
|
63
|
+
return frozenset(self.host_for(company) for company in companies)
|
|
64
|
+
|
|
65
|
+
def board_key(self, company: Company) -> str:
|
|
66
|
+
return board_key(self.name, self._board(company))
|
|
67
|
+
|
|
68
|
+
def url_for(self, company: Company) -> str:
|
|
69
|
+
return f"https://{self.host_for(company)}{PATH}"
|
|
70
|
+
|
|
71
|
+
def params_for(self, company: Company, offset: int) -> dict[str, str]:
|
|
72
|
+
_, site = oracle_target(company.oracle_host or "", company.oracle_site or "")
|
|
73
|
+
keyword = KEYWORD if company.oracle_keyword is None else company.oracle_keyword.strip()
|
|
74
|
+
narrowed = f",keyword={keyword}" if keyword else ""
|
|
75
|
+
return {
|
|
76
|
+
"onlyData": "true",
|
|
77
|
+
"expand": "requisitionList",
|
|
78
|
+
"finder": (f"findReqs;siteNumber={site}{narrowed},offset={offset},limit={PAGE_SIZE}"),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
def plan(self, company: Company) -> tuple[str, ...]:
|
|
82
|
+
return (self.url_for(company),)
|
|
83
|
+
|
|
84
|
+
async def fetch(
|
|
85
|
+
self,
|
|
86
|
+
company: Company,
|
|
87
|
+
client: HttpClient,
|
|
88
|
+
now: datetime,
|
|
89
|
+
facets: object = None,
|
|
90
|
+
details: Sequence[str] = (),
|
|
91
|
+
) -> FetchResult:
|
|
92
|
+
postings: list[OraclePosting] = []
|
|
93
|
+
malformed = 0
|
|
94
|
+
truncated = False
|
|
95
|
+
premature_end = False
|
|
96
|
+
|
|
97
|
+
for page in range(MAX_PAGES):
|
|
98
|
+
response = await client.get_json(
|
|
99
|
+
self.url_for(company), params=self.params_for(company, page * PAGE_SIZE)
|
|
100
|
+
)
|
|
101
|
+
if response.not_modified:
|
|
102
|
+
if page == 0:
|
|
103
|
+
return FetchResult(not_modified=True)
|
|
104
|
+
return FetchResult(
|
|
105
|
+
jobs=tuple(self._to_job(company, row, now) for row in postings),
|
|
106
|
+
authoritative=False,
|
|
107
|
+
degraded="a later page answered 304, so the walk ended early",
|
|
108
|
+
)
|
|
109
|
+
rows, dropped, total = self._validate(company, response.payload)
|
|
110
|
+
malformed += dropped
|
|
111
|
+
if not rows and not dropped:
|
|
112
|
+
premature_end = len(postings) + malformed < total
|
|
113
|
+
break
|
|
114
|
+
postings.extend(rows)
|
|
115
|
+
if len(postings) + malformed >= total:
|
|
116
|
+
break
|
|
117
|
+
else:
|
|
118
|
+
truncated = True
|
|
119
|
+
|
|
120
|
+
notes = []
|
|
121
|
+
if truncated:
|
|
122
|
+
notes.append(f"stopped at the {MAX_PAGES}-page cap")
|
|
123
|
+
if premature_end:
|
|
124
|
+
notes.append("received an empty page before the reported total")
|
|
125
|
+
if malformed:
|
|
126
|
+
notes.append(malformed_note(malformed))
|
|
127
|
+
return FetchResult(
|
|
128
|
+
jobs=tuple(self._to_job(company, row, now) for row in postings),
|
|
129
|
+
authoritative=not (truncated or premature_end or malformed),
|
|
130
|
+
degraded="; ".join(notes),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def _validate(self, company: Company, payload: Any) -> tuple[list[OraclePosting], int, int]:
|
|
134
|
+
try:
|
|
135
|
+
parents = payload["items"] if isinstance(payload, dict) else None
|
|
136
|
+
if not isinstance(parents, list) or len(parents) != 1:
|
|
137
|
+
raise ValueError("items must contain exactly one search result")
|
|
138
|
+
page = OraclePage.model_validate(parents[0])
|
|
139
|
+
except (KeyError, TypeError, ValidationError, ValueError) as exc:
|
|
140
|
+
captured = capture_payload(self.name, company.slug, payload)
|
|
141
|
+
raise PayloadValidationError(
|
|
142
|
+
f"oracle_cloud/{company.slug}: invalid recruiting search response; raw payload "
|
|
143
|
+
f"captured at {captured}"
|
|
144
|
+
) from exc
|
|
145
|
+
rows, dropped = validate_rows(
|
|
146
|
+
OraclePosting, page.requisitionList, source=self.name, slug=company.slug
|
|
147
|
+
)
|
|
148
|
+
return rows, dropped, page.TotalJobsCount
|
|
149
|
+
|
|
150
|
+
def _to_job(self, company: Company, row: OraclePosting, now: datetime) -> Job:
|
|
151
|
+
host, site = oracle_target(company.oracle_host or "", company.oracle_site or "")
|
|
152
|
+
title = collapse_whitespace(row.Title)
|
|
153
|
+
description = collapse_whitespace(
|
|
154
|
+
" ".join(
|
|
155
|
+
strip_html(text)
|
|
156
|
+
for text in (
|
|
157
|
+
row.ShortDescriptionStr,
|
|
158
|
+
row.ExternalQualificationsStr,
|
|
159
|
+
row.ExternalResponsibilitiesStr,
|
|
160
|
+
)
|
|
161
|
+
if text
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
location = collapse_whitespace(
|
|
165
|
+
", ".join(part for part in (row.PrimaryLocation, row.PrimaryLocationCountry) if part)
|
|
166
|
+
)
|
|
167
|
+
posted = (
|
|
168
|
+
datetime.combine(row.PostedDate, datetime.min.time(), tzinfo=UTC)
|
|
169
|
+
if row.PostedDate is not None
|
|
170
|
+
else None
|
|
171
|
+
)
|
|
172
|
+
return Job(
|
|
173
|
+
id=job_id(self.name, self._board(company), row.Id),
|
|
174
|
+
source=self.name,
|
|
175
|
+
company=company.name,
|
|
176
|
+
title_raw=title,
|
|
177
|
+
title_normalized=title.lower(),
|
|
178
|
+
apply_url_raw=f"https://{host}/hcmUI/CandidateExperience/en/sites/{site}/job/{row.Id}",
|
|
179
|
+
description=description,
|
|
180
|
+
location_raw=location,
|
|
181
|
+
first_seen=now,
|
|
182
|
+
last_seen=now,
|
|
183
|
+
source_posted_at=posted,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def _board(self, company: Company) -> str:
|
|
187
|
+
return f"{company.oracle_host or company.slug}-{company.oracle_site or ''}"
|