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 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"