hirebase 0.1.1__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.
- hirebase/__init__.py +102 -0
- hirebase/_files.py +53 -0
- hirebase/_ops.py +377 -0
- hirebase/_version.py +1 -0
- hirebase/client.py +227 -0
- hirebase/config.py +72 -0
- hirebase/exceptions.py +123 -0
- hirebase/models/__init__.py +50 -0
- hirebase/models/base.py +45 -0
- hirebase/models/common.py +47 -0
- hirebase/models/companies.py +176 -0
- hirebase/models/insights.py +84 -0
- hirebase/models/jobs.py +202 -0
- hirebase/models/neural.py +179 -0
- hirebase/models/resumes.py +47 -0
- hirebase/models/tasks.py +69 -0
- hirebase/py.typed +0 -0
- hirebase/resources/__init__.py +18 -0
- hirebase/resources/companies.py +180 -0
- hirebase/resources/jobs.py +233 -0
- hirebase/resources/resumes.py +123 -0
- hirebase/resources/tasks.py +99 -0
- hirebase/streaming.py +147 -0
- hirebase-0.1.1.dist-info/METADATA +289 -0
- hirebase-0.1.1.dist-info/RECORD +42 -0
- hirebase-0.1.1.dist-info/WHEEL +5 -0
- hirebase-0.1.1.dist-info/entry_points.txt +2 -0
- hirebase-0.1.1.dist-info/licenses/LICENSE +8 -0
- hirebase-0.1.1.dist-info/top_level.txt +2 -0
- hirebase_cli/__init__.py +3 -0
- hirebase_cli/client.py +476 -0
- hirebase_cli/commands/__init__.py +10 -0
- hirebase_cli/commands/blog.py +357 -0
- hirebase_cli/commands/companies.py +222 -0
- hirebase_cli/commands/health.py +39 -0
- hirebase_cli/commands/insights.py +363 -0
- hirebase_cli/commands/jobs.py +355 -0
- hirebase_cli/commands/scraper.py +126 -0
- hirebase_cli/config.py +54 -0
- hirebase_cli/formatters.py +707 -0
- hirebase_cli/main.py +142 -0
- hirebase_cli/models.py +223 -0
hirebase/__init__.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Hirebase Python SDK.
|
|
2
|
+
|
|
3
|
+
A lean, typed client for the Hirebase public API (https://api.hirebase.org).
|
|
4
|
+
|
|
5
|
+
Quickstart:
|
|
6
|
+
|
|
7
|
+
import hirebase
|
|
8
|
+
|
|
9
|
+
client = hirebase.Client(api_key="sk_live_...")
|
|
10
|
+
|
|
11
|
+
# Search jobs (typed results by default)
|
|
12
|
+
result = client.jobs.search({
|
|
13
|
+
"job_titles": ["Software Engineer", "Product Engineer"],
|
|
14
|
+
"locations": [{"city": "San Francisco", "region": "California",
|
|
15
|
+
"country": "United States"}],
|
|
16
|
+
})
|
|
17
|
+
for job in result:
|
|
18
|
+
print(job.job_title, job.company_name)
|
|
19
|
+
|
|
20
|
+
# Async usage
|
|
21
|
+
client = hirebase.AsyncClient(api_key="sk_live_...")
|
|
22
|
+
task = await client.jobs.export(query, format="json")
|
|
23
|
+
success, result = await client.tasks.poll(task)
|
|
24
|
+
if success:
|
|
25
|
+
await client.stream_file(result["download_url"], file_path="jobs.json")
|
|
26
|
+
for job in client.jobs.stream_file("jobs.json"):
|
|
27
|
+
...
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from ._version import __version__
|
|
31
|
+
from .client import AsyncClient, Client
|
|
32
|
+
from .config import DEFAULT_BASE_URL, Settings
|
|
33
|
+
from .exceptions import (
|
|
34
|
+
APIError,
|
|
35
|
+
AuthenticationError,
|
|
36
|
+
ConfigurationError,
|
|
37
|
+
HirebaseError,
|
|
38
|
+
NotFoundError,
|
|
39
|
+
PaymentRequiredError,
|
|
40
|
+
PermissionError_,
|
|
41
|
+
RateLimitError,
|
|
42
|
+
ServerError,
|
|
43
|
+
TaskError,
|
|
44
|
+
TaskFailed,
|
|
45
|
+
TaskTimeout,
|
|
46
|
+
)
|
|
47
|
+
from .models import (
|
|
48
|
+
Company,
|
|
49
|
+
CompanyQuery,
|
|
50
|
+
CompanySearchResult,
|
|
51
|
+
Job,
|
|
52
|
+
JobInsights,
|
|
53
|
+
JobQuery,
|
|
54
|
+
JobSearchResult,
|
|
55
|
+
Location,
|
|
56
|
+
NeuralSearchQuery,
|
|
57
|
+
NeuralVectorQuery,
|
|
58
|
+
ResumeEmbedResponse,
|
|
59
|
+
ResumeRecord,
|
|
60
|
+
SalaryRange,
|
|
61
|
+
Task,
|
|
62
|
+
TaskState,
|
|
63
|
+
YoeRange,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
"__version__",
|
|
68
|
+
"Client",
|
|
69
|
+
"AsyncClient",
|
|
70
|
+
"Settings",
|
|
71
|
+
"DEFAULT_BASE_URL",
|
|
72
|
+
# Models
|
|
73
|
+
"Job",
|
|
74
|
+
"JobQuery",
|
|
75
|
+
"JobSearchResult",
|
|
76
|
+
"NeuralVectorQuery",
|
|
77
|
+
"NeuralSearchQuery",
|
|
78
|
+
"ResumeRecord",
|
|
79
|
+
"ResumeEmbedResponse",
|
|
80
|
+
"Company",
|
|
81
|
+
"CompanyQuery",
|
|
82
|
+
"CompanySearchResult",
|
|
83
|
+
"Task",
|
|
84
|
+
"TaskState",
|
|
85
|
+
"JobInsights",
|
|
86
|
+
"Location",
|
|
87
|
+
"SalaryRange",
|
|
88
|
+
"YoeRange",
|
|
89
|
+
# Exceptions
|
|
90
|
+
"HirebaseError",
|
|
91
|
+
"ConfigurationError",
|
|
92
|
+
"APIError",
|
|
93
|
+
"AuthenticationError",
|
|
94
|
+
"PermissionError_",
|
|
95
|
+
"PaymentRequiredError",
|
|
96
|
+
"NotFoundError",
|
|
97
|
+
"RateLimitError",
|
|
98
|
+
"ServerError",
|
|
99
|
+
"TaskError",
|
|
100
|
+
"TaskFailed",
|
|
101
|
+
"TaskTimeout",
|
|
102
|
+
]
|
hirebase/_files.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Helpers for multipart file uploads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import mimetypes
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, BinaryIO, Dict, Optional, Tuple, Union
|
|
8
|
+
|
|
9
|
+
FileInput = Union[str, Path, bytes, BinaryIO, Tuple[str, Any, Optional[str]]]
|
|
10
|
+
|
|
11
|
+
_ALLOWED_RESUME_TYPES = {
|
|
12
|
+
"application/pdf",
|
|
13
|
+
"application/msword",
|
|
14
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
15
|
+
"text/plain",
|
|
16
|
+
"text/html",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def prepare_upload_file(file: FileInput) -> Dict[str, Tuple[str, Any, str]]:
|
|
21
|
+
"""Return a ``files`` dict suitable for requests/httpx multipart uploads."""
|
|
22
|
+
if isinstance(file, tuple):
|
|
23
|
+
if len(file) == 3:
|
|
24
|
+
name, data, content_type = file
|
|
25
|
+
elif len(file) == 2:
|
|
26
|
+
name, data = file # type: ignore[misc]
|
|
27
|
+
content_type = None
|
|
28
|
+
else:
|
|
29
|
+
raise ValueError("file tuple must be (filename, data) or (filename, data, type)")
|
|
30
|
+
ctype = content_type or _guess_type(name)
|
|
31
|
+
return {"file": (name, data, ctype)}
|
|
32
|
+
|
|
33
|
+
if isinstance(file, (str, Path)):
|
|
34
|
+
path = Path(file)
|
|
35
|
+
ctype = _guess_type(path.name)
|
|
36
|
+
return {"file": (path.name, open(path, "rb"), ctype)}
|
|
37
|
+
|
|
38
|
+
if isinstance(file, bytes):
|
|
39
|
+
return {"file": ("resume.pdf", file, "application/pdf")}
|
|
40
|
+
|
|
41
|
+
if hasattr(file, "read"):
|
|
42
|
+
name = Path(getattr(file, "name", "resume.pdf")).name
|
|
43
|
+
ctype = _guess_type(name)
|
|
44
|
+
return {"file": (name, file, ctype)}
|
|
45
|
+
|
|
46
|
+
raise TypeError(
|
|
47
|
+
"file must be a path, bytes, a file-like object, or (filename, data[, type])"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _guess_type(filename: str) -> str:
|
|
52
|
+
ctype, _ = mimetypes.guess_type(filename)
|
|
53
|
+
return ctype or "application/octet-stream"
|
hirebase/_ops.py
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""Pure request-building and response-parsing logic.
|
|
2
|
+
|
|
3
|
+
This module contains *no* I/O. Each public operation is expressed as:
|
|
4
|
+
|
|
5
|
+
* a ``*_request(...) -> Request`` function that returns the HTTP spec, and
|
|
6
|
+
* a ``parse_*(data, client, return_type) -> ...`` function that turns the
|
|
7
|
+
decoded JSON into typed models.
|
|
8
|
+
|
|
9
|
+
Keeping transport out of here means the sync and async resources share one
|
|
10
|
+
implementation, and the eventual JavaScript SDK can mirror this file almost
|
|
11
|
+
line-for-line.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any, Dict, Optional, Type, Union
|
|
18
|
+
|
|
19
|
+
from .models.companies import (
|
|
20
|
+
Company,
|
|
21
|
+
CompanyQuery,
|
|
22
|
+
CompanySearchResult,
|
|
23
|
+
coerce_company_query,
|
|
24
|
+
)
|
|
25
|
+
from .models.insights import JobInsights
|
|
26
|
+
from .models.jobs import Job, JobQuery, JobSearchResult, coerce_query
|
|
27
|
+
from .models.neural import (
|
|
28
|
+
NeuralSearchQuery,
|
|
29
|
+
NeuralVectorQuery,
|
|
30
|
+
coerce_neural_search,
|
|
31
|
+
coerce_neural_vector,
|
|
32
|
+
extract_job_id,
|
|
33
|
+
merge_job_ids,
|
|
34
|
+
)
|
|
35
|
+
from .models.resumes import ResumeEmbedResponse, ResumeRecord
|
|
36
|
+
from .models.tasks import Task
|
|
37
|
+
|
|
38
|
+
# Sentinel meaning "return typed models" (the default).
|
|
39
|
+
TYPED = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class Request:
|
|
44
|
+
"""An HTTP request specification, independent of any transport."""
|
|
45
|
+
|
|
46
|
+
method: str
|
|
47
|
+
path: str
|
|
48
|
+
params: Optional[Dict[str, Any]] = None
|
|
49
|
+
json: Optional[Dict[str, Any]] = field(default=None)
|
|
50
|
+
# Multipart upload: ``{"file": (filename, fileobj, content_type)}``
|
|
51
|
+
files: Optional[Dict[str, Any]] = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _want_dict(return_type: Optional[Type]) -> bool:
|
|
55
|
+
return return_type is dict
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
59
|
+
# Jobs
|
|
60
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def search_jobs_request(
|
|
64
|
+
query: Optional[Union[JobQuery, dict]],
|
|
65
|
+
page: Optional[int] = None,
|
|
66
|
+
limit: Optional[int] = None,
|
|
67
|
+
) -> Request:
|
|
68
|
+
q = coerce_query(query)
|
|
69
|
+
if page is not None:
|
|
70
|
+
q.page = page
|
|
71
|
+
if limit is not None:
|
|
72
|
+
q.limit = limit
|
|
73
|
+
return Request("POST", "/v2/jobs/search", json=q.to_payload())
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def parse_job_search(
|
|
77
|
+
data: dict, client: Any, return_type: Optional[Type]
|
|
78
|
+
) -> Union[JobSearchResult, dict]:
|
|
79
|
+
if _want_dict(return_type):
|
|
80
|
+
return data
|
|
81
|
+
result = JobSearchResult.model_validate(data)
|
|
82
|
+
for job in result.jobs:
|
|
83
|
+
job._bind(client)
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_job_request(job_id: str) -> Request:
|
|
88
|
+
return Request("GET", f"/v2/jobs/{job_id}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def parse_job(data: dict, client: Any, return_type: Optional[Type]) -> Union[Job, dict]:
|
|
92
|
+
if _want_dict(return_type):
|
|
93
|
+
return data
|
|
94
|
+
return Job.model_validate(data)._bind(client)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def export_jobs_request(
|
|
98
|
+
query: Optional[Union[JobQuery, dict]], format: str = "json"
|
|
99
|
+
) -> Request:
|
|
100
|
+
if format not in ("json", "csv"):
|
|
101
|
+
raise ValueError("format must be 'json' or 'csv'")
|
|
102
|
+
q = coerce_query(query)
|
|
103
|
+
return Request(
|
|
104
|
+
"POST",
|
|
105
|
+
"/v2/jobs/export",
|
|
106
|
+
json={"search": q.to_payload(), "format": format},
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def insights_request(
|
|
111
|
+
query: Optional[Union[JobQuery, dict]],
|
|
112
|
+
path: str = "/v2/jobs/insights",
|
|
113
|
+
) -> Request:
|
|
114
|
+
q = coerce_query(query)
|
|
115
|
+
return Request("POST", path, json=q.to_payload())
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_company_job_request(company_slug: str, job_slug: str) -> Request:
|
|
119
|
+
return Request(
|
|
120
|
+
"GET",
|
|
121
|
+
f"/v2/hirebase/companies/{company_slug}/jobs/{job_slug}",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def resolve_job_id_from_slug(client: Any, company_slug: str, job_slug: str) -> str:
|
|
126
|
+
data = client._request(get_company_job_request(company_slug, job_slug))
|
|
127
|
+
jobs = data.get("jobs") or []
|
|
128
|
+
if not jobs:
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f"No job found for company_slug={company_slug!r} job_slug={job_slug!r}"
|
|
131
|
+
)
|
|
132
|
+
return extract_job_id(jobs[0])
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def prepare_neural_vector(
|
|
136
|
+
client: Any,
|
|
137
|
+
vector: Optional[Union[NeuralVectorQuery, dict]] = None,
|
|
138
|
+
*,
|
|
139
|
+
query: Optional[str] = None,
|
|
140
|
+
vectors: Optional[list] = None,
|
|
141
|
+
job_ids: Optional[list] = None,
|
|
142
|
+
job: Optional[Union[Job, dict, str]] = None,
|
|
143
|
+
jobs: Optional[list] = None,
|
|
144
|
+
artifact_id: Optional[str] = None,
|
|
145
|
+
resume_id: Optional[str] = None,
|
|
146
|
+
company_slug: Optional[str] = None,
|
|
147
|
+
job_slug: Optional[str] = None,
|
|
148
|
+
score_threshold: Optional[float] = None,
|
|
149
|
+
) -> NeuralVectorQuery:
|
|
150
|
+
"""Coerce shortcuts and resolve slug/job references into a vector spec."""
|
|
151
|
+
v = coerce_neural_vector(
|
|
152
|
+
vector,
|
|
153
|
+
query=query,
|
|
154
|
+
vectors=vectors,
|
|
155
|
+
job_ids=job_ids,
|
|
156
|
+
artifact_id=artifact_id,
|
|
157
|
+
resume_id=resume_id,
|
|
158
|
+
score_threshold=score_threshold,
|
|
159
|
+
)
|
|
160
|
+
v = merge_job_ids(v, job=job, jobs=jobs)
|
|
161
|
+
if company_slug and job_slug:
|
|
162
|
+
jid = resolve_job_id_from_slug(client, company_slug, job_slug)
|
|
163
|
+
v = merge_job_ids(v, job_ids=[jid])
|
|
164
|
+
elif company_slug or job_slug:
|
|
165
|
+
raise ValueError("company_slug and job_slug must be provided together")
|
|
166
|
+
return v
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def neural_search_request(
|
|
170
|
+
search: NeuralSearchQuery,
|
|
171
|
+
*,
|
|
172
|
+
page: Optional[int] = None,
|
|
173
|
+
limit: Optional[int] = None,
|
|
174
|
+
) -> Request:
|
|
175
|
+
lexical = search.lexical or JobQuery()
|
|
176
|
+
if page is not None:
|
|
177
|
+
lexical.page = page
|
|
178
|
+
if limit is not None:
|
|
179
|
+
lexical.limit = limit
|
|
180
|
+
body = NeuralSearchQuery(
|
|
181
|
+
vector=search.vector,
|
|
182
|
+
lexical=lexical,
|
|
183
|
+
).to_payload()
|
|
184
|
+
return Request("POST", "/v2/jobs/neural-search", json=body)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def parse_insights(
|
|
188
|
+
data: dict, client: Any, return_type: Optional[Type]
|
|
189
|
+
) -> Union[JobInsights, dict]:
|
|
190
|
+
if _want_dict(return_type):
|
|
191
|
+
return data
|
|
192
|
+
return JobInsights.model_validate(data)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
196
|
+
# Tasks
|
|
197
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def get_task_request(task_id: str) -> Request:
|
|
201
|
+
return Request("GET", f"/v2/tasks/{task_id}")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def parse_task(data: dict, client: Any, return_type: Optional[Type]) -> Union[Task, dict]:
|
|
205
|
+
if _want_dict(return_type):
|
|
206
|
+
return data
|
|
207
|
+
return Task.model_validate(data)._bind(client)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def task_id_of(task: Union[Task, dict, str]) -> str:
|
|
211
|
+
if isinstance(task, str):
|
|
212
|
+
return task
|
|
213
|
+
if isinstance(task, Task):
|
|
214
|
+
return task.id
|
|
215
|
+
if isinstance(task, dict):
|
|
216
|
+
tid = task.get("id")
|
|
217
|
+
if tid:
|
|
218
|
+
return str(tid)
|
|
219
|
+
raise TypeError("Expected a Task, task dict, or task id string.")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
223
|
+
# Companies
|
|
224
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def search_companies_request(
|
|
228
|
+
query: Optional[Union[CompanyQuery, dict]],
|
|
229
|
+
page: Optional[int] = None,
|
|
230
|
+
limit: Optional[int] = None,
|
|
231
|
+
) -> Request:
|
|
232
|
+
q = coerce_company_query(query)
|
|
233
|
+
if page is not None:
|
|
234
|
+
q.page = page
|
|
235
|
+
if limit is not None:
|
|
236
|
+
q.limit = limit
|
|
237
|
+
return Request(
|
|
238
|
+
"POST", "/v2/hirebase/companies/search", json=q.to_payload()
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def parse_company_search(
|
|
243
|
+
data: dict, client: Any, return_type: Optional[Type]
|
|
244
|
+
) -> Union[CompanySearchResult, dict]:
|
|
245
|
+
if _want_dict(return_type):
|
|
246
|
+
return data
|
|
247
|
+
result = CompanySearchResult.model_validate(data)
|
|
248
|
+
for company in result.companies:
|
|
249
|
+
company._bind(client)
|
|
250
|
+
return result
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def get_company_request(slug: str) -> Request:
|
|
254
|
+
return Request("GET", f"/v2/hirebase/companies/{slug}")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def parse_company_detail(
|
|
258
|
+
data: dict,
|
|
259
|
+
client: Any,
|
|
260
|
+
return_type: Optional[Type],
|
|
261
|
+
return_jobs: bool = True,
|
|
262
|
+
) -> Union[Company, dict]:
|
|
263
|
+
"""Parse a ``{company, jobs}`` detail response into a bound Company."""
|
|
264
|
+
if _want_dict(return_type):
|
|
265
|
+
if not return_jobs:
|
|
266
|
+
data = {**data, "jobs": None}
|
|
267
|
+
return data
|
|
268
|
+
|
|
269
|
+
company_payload = dict(data.get("company") or {})
|
|
270
|
+
if return_jobs:
|
|
271
|
+
company_payload["jobs"] = data.get("jobs") or []
|
|
272
|
+
company = Company.model_validate(company_payload)._bind(client)
|
|
273
|
+
if company.jobs:
|
|
274
|
+
for job in company.jobs:
|
|
275
|
+
job._bind(client)
|
|
276
|
+
return company
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def company_jobs_request(
|
|
280
|
+
slug: str,
|
|
281
|
+
page: Optional[int] = None,
|
|
282
|
+
limit: Optional[int] = None,
|
|
283
|
+
sort_by: Optional[str] = None,
|
|
284
|
+
sort_order: Optional[str] = None,
|
|
285
|
+
job_board: Optional[str] = None,
|
|
286
|
+
job_category: Optional[str] = None,
|
|
287
|
+
) -> Request:
|
|
288
|
+
params: Dict[str, Any] = {}
|
|
289
|
+
if page is not None:
|
|
290
|
+
params["page"] = page
|
|
291
|
+
if limit is not None:
|
|
292
|
+
params["limit"] = limit
|
|
293
|
+
if sort_by is not None:
|
|
294
|
+
params["sort_by"] = sort_by
|
|
295
|
+
if sort_order is not None:
|
|
296
|
+
params["sort_order"] = sort_order
|
|
297
|
+
if job_board is not None:
|
|
298
|
+
params["job_board"] = job_board
|
|
299
|
+
if job_category is not None:
|
|
300
|
+
params["job_category"] = job_category
|
|
301
|
+
return Request(
|
|
302
|
+
"GET", f"/v2/hirebase/companies/{slug}/jobs", params=params or None
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def parse_company_jobs(
|
|
307
|
+
data: dict, client: Any, return_type: Optional[Type]
|
|
308
|
+
) -> Union[JobSearchResult, dict]:
|
|
309
|
+
if _want_dict(return_type):
|
|
310
|
+
return data
|
|
311
|
+
# The company-jobs endpoint omits company_count; default to 0.
|
|
312
|
+
payload = {"company_count": 0, **data}
|
|
313
|
+
result = JobSearchResult.model_validate(payload)
|
|
314
|
+
for job in result.jobs:
|
|
315
|
+
job._bind(client)
|
|
316
|
+
return result
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def company_insights_request(
|
|
320
|
+
slug: str, query: Optional[Union[JobQuery, dict]]
|
|
321
|
+
) -> Request:
|
|
322
|
+
q = coerce_query(query)
|
|
323
|
+
return Request(
|
|
324
|
+
"POST",
|
|
325
|
+
f"/v2/hirebase/companies/{slug}/insights",
|
|
326
|
+
json=q.to_payload(),
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
331
|
+
# Resumes
|
|
332
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def resume_upload_request(files: Dict[str, Any]) -> Request:
|
|
336
|
+
return Request("POST", "/v2/resumes/upload/", files=files)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def resume_embed_request(files: Dict[str, Any]) -> Request:
|
|
340
|
+
"""Enterprise: parse + embed in one call; resume is not stored."""
|
|
341
|
+
return Request("POST", "/v2/resumes/embed", files=files)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def resume_get_request(resume_id: str) -> Request:
|
|
345
|
+
return Request("GET", f"/v2/resumes/{resume_id}")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def resume_parse_request(resume_id: str) -> Request:
|
|
349
|
+
return Request("POST", f"/v2/resumes/{resume_id}/parse")
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def parse_resume_record(
|
|
353
|
+
data: dict, return_type: Optional[Type]
|
|
354
|
+
) -> Union[ResumeRecord, dict]:
|
|
355
|
+
if _want_dict(return_type):
|
|
356
|
+
return data
|
|
357
|
+
return ResumeRecord.model_validate(data)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def parse_resume_embed(
|
|
361
|
+
data: dict, return_type: Optional[Type]
|
|
362
|
+
) -> Union[ResumeEmbedResponse, dict]:
|
|
363
|
+
if _want_dict(return_type):
|
|
364
|
+
return data
|
|
365
|
+
return ResumeEmbedResponse.model_validate(data)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def company_slug_of(company: Union[Company, dict, str]) -> str:
|
|
369
|
+
if isinstance(company, str):
|
|
370
|
+
return company
|
|
371
|
+
if isinstance(company, Company):
|
|
372
|
+
return company.company_slug
|
|
373
|
+
if isinstance(company, dict):
|
|
374
|
+
slug = company.get("company_slug")
|
|
375
|
+
if slug:
|
|
376
|
+
return str(slug)
|
|
377
|
+
raise TypeError("Expected a Company, company dict, or slug string.")
|
hirebase/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|