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,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 ''}"