worklittle 1.1.0__tar.gz

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.
@@ -0,0 +1,6 @@
1
+ .venv/
2
+ dist/
3
+ *.egg-info/
4
+ __pycache__/
5
+ .pytest_cache/
6
+ *.pyc
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.5
2
+ Name: worklittle
3
+ Version: 1.1.0
4
+ Summary: Official Worklittle Python SDK and CLI for api.worklittle.com
5
+ Project-URL: Homepage, https://docs.worklittle.com/libraries/sdk?lang=python
6
+ Project-URL: Documentation, https://docs.worklittle.com/libraries/overview
7
+ Author: Worklittle
8
+ License-Expression: MIT
9
+ Keywords: api,ats,cli,jobs,sdk,worklittle
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: httpx>=0.27
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=8.0; extra == 'dev'
14
+ Description-Content-Type: text/markdown
15
+
16
+ # worklittle
17
+
18
+ Python SDK and CLI for [api.worklittle.com](https://api.worklittle.com).
19
+
20
+ Registry publish is not live yet. Install from this package:
21
+
22
+ ```bash
23
+ cd packages/sdk-python
24
+ pip install -e .
25
+ export WORKLITTLE_API_KEY=sk-wl-api01-...
26
+ worklittle jobs search --q="software engineer" --limit 1 --pretty
27
+ worklittle jobs search --title="software engineer, -senior" --company="-lucid-motors,-tesla" --limit 5 --pretty
28
+ ```
29
+
30
+ `--company` accepts bare slugs (include) or leading-dash slugs (exclude). Title negatives use `--title` / `--q`. Other `GET /jobs` flags pass through the same way (`--workplace_type`, `--posted_within_days`, …).
31
+
32
+ ```python
33
+ from worklittle import Worklittle
34
+
35
+ wl = Worklittle()
36
+ jobs = wl.jobs.search(q="software engineer", limit=5)
37
+ filtered = wl.jobs.search(
38
+ title="software engineer, -senior",
39
+ company="-lucid-motors,-tesla",
40
+ limit=5,
41
+ )
42
+ ```
43
+
44
+ CLI output is JSON on stdout by default.
45
+
46
+ Docs: https://docs.worklittle.com/libraries/sdk?lang=python
47
+ OpenAPI: https://docs.worklittle.com/openapi/openapi.yaml
@@ -0,0 +1,32 @@
1
+ # worklittle
2
+
3
+ Python SDK and CLI for [api.worklittle.com](https://api.worklittle.com).
4
+
5
+ Registry publish is not live yet. Install from this package:
6
+
7
+ ```bash
8
+ cd packages/sdk-python
9
+ pip install -e .
10
+ export WORKLITTLE_API_KEY=sk-wl-api01-...
11
+ worklittle jobs search --q="software engineer" --limit 1 --pretty
12
+ worklittle jobs search --title="software engineer, -senior" --company="-lucid-motors,-tesla" --limit 5 --pretty
13
+ ```
14
+
15
+ `--company` accepts bare slugs (include) or leading-dash slugs (exclude). Title negatives use `--title` / `--q`. Other `GET /jobs` flags pass through the same way (`--workplace_type`, `--posted_within_days`, …).
16
+
17
+ ```python
18
+ from worklittle import Worklittle
19
+
20
+ wl = Worklittle()
21
+ jobs = wl.jobs.search(q="software engineer", limit=5)
22
+ filtered = wl.jobs.search(
23
+ title="software engineer, -senior",
24
+ company="-lucid-motors,-tesla",
25
+ limit=5,
26
+ )
27
+ ```
28
+
29
+ CLI output is JSON on stdout by default.
30
+
31
+ Docs: https://docs.worklittle.com/libraries/sdk?lang=python
32
+ OpenAPI: https://docs.worklittle.com/openapi/openapi.yaml
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "worklittle"
7
+ version = "1.1.0"
8
+ description = "Official Worklittle Python SDK and CLI for api.worklittle.com"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "Worklittle" }]
13
+ keywords = ["worklittle", "jobs", "ats", "api", "sdk", "cli"]
14
+ dependencies = [
15
+ "httpx>=0.27",
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ dev = ["pytest>=8.0"]
20
+
21
+ [project.scripts]
22
+ worklittle = "worklittle.cli:main"
23
+
24
+ [project.urls]
25
+ Homepage = "https://docs.worklittle.com/libraries/sdk?lang=python"
26
+ Documentation = "https://docs.worklittle.com/libraries/overview"
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["worklittle"]
30
+
31
+ [tool.hatch.build.targets.sdist]
32
+ include = ["worklittle", "README.md", "pyproject.toml", "tests"]
@@ -0,0 +1,17 @@
1
+ import hashlib
2
+ import hmac
3
+ import time
4
+
5
+ from worklittle.webhooks import verify_webhook_signature
6
+
7
+
8
+ def test_verify_webhook_signature():
9
+ secret = "whsec_test"
10
+ timestamp = str(int(time.time()))
11
+ raw = '{"ok":true}'
12
+ sig = "v1=" + hmac.new(
13
+ secret.encode(), f"{timestamp}.{raw}".encode(), hashlib.sha256
14
+ ).hexdigest()
15
+ assert verify_webhook_signature(
16
+ signature_header=sig, timestamp=timestamp, raw_body=raw, secret=secret
17
+ )
@@ -0,0 +1,13 @@
1
+ from .client import Worklittle, create_worklittle
2
+ from .errors import PaymentRequiredError, QuotaExceededError, WorklittleError
3
+ from .webhooks import verify_webhook_signature
4
+
5
+ __all__ = [
6
+ "Worklittle",
7
+ "create_worklittle",
8
+ "WorklittleError",
9
+ "PaymentRequiredError",
10
+ "QuotaExceededError",
11
+ "verify_webhook_signature",
12
+ ]
13
+ __version__ = "1.1.0"
@@ -0,0 +1,275 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import sys
7
+ from typing import Any
8
+
9
+ from .client import Worklittle
10
+ from .errors import WorklittleError
11
+
12
+ # Reserved CLI flags — not forwarded as API query params.
13
+ _RESERVED = frozenset(
14
+ {
15
+ "api-key",
16
+ "base-url",
17
+ "pretty",
18
+ "json",
19
+ "body",
20
+ "id",
21
+ "name",
22
+ "field",
23
+ "h",
24
+ "help",
25
+ }
26
+ )
27
+
28
+
29
+ def main(argv: list[str] | None = None) -> None:
30
+ argv = list(sys.argv[1:] if argv is None else argv)
31
+ parser = argparse.ArgumentParser(
32
+ prog="worklittle",
33
+ description="Worklittle CLI. JSON on stdout by default.",
34
+ add_help=False,
35
+ )
36
+ parser.add_argument("group", nargs="?", default="help")
37
+ parser.add_argument("action", nargs="?", default="")
38
+ parser.add_argument("rest", nargs="*")
39
+ parser.add_argument("--api-key", default=os.environ.get("WORKLITTLE_API_KEY"))
40
+ parser.add_argument("--base-url", default="https://api.worklittle.com")
41
+ parser.add_argument("--pretty", action="store_true")
42
+ parser.add_argument("--body", default="{}")
43
+ parser.add_argument("--id")
44
+ parser.add_argument("--name")
45
+ parser.add_argument("--field")
46
+ # Declared for help / common flags; also merged into query params below.
47
+ parser.add_argument("--q")
48
+ parser.add_argument("--title")
49
+ parser.add_argument("--company")
50
+ parser.add_argument("--limit", type=int)
51
+ parser.add_argument("--cursor")
52
+ parser.add_argument("--include")
53
+ args, unknown = parser.parse_known_args(argv)
54
+
55
+ if args.group in ("help", "", None) or args.group in ("-h", "--help"):
56
+ _out(
57
+ {
58
+ "name": "worklittle",
59
+ "version": "1.1.0",
60
+ "usage": "worklittle <group> <action> [flags]",
61
+ "groups": [
62
+ "jobs",
63
+ "companies",
64
+ "stats",
65
+ "boards",
66
+ "apply",
67
+ "apply-with-ai",
68
+ "applied",
69
+ "candidates",
70
+ "employees",
71
+ "email",
72
+ "documents",
73
+ "attendance",
74
+ "surveys",
75
+ "webhooks",
76
+ "listings",
77
+ "resumes",
78
+ "usage",
79
+ "billing",
80
+ "limits",
81
+ "agent",
82
+ ],
83
+ "note": (
84
+ "JSON stdout by default. Query flags match GET /jobs param names "
85
+ "(e.g. --company=-lucid-motors, --title='engineer, -senior'). "
86
+ "JS CLI (Homebrew) is canonical; Python CLI is a subset."
87
+ ),
88
+ },
89
+ pretty=True,
90
+ )
91
+ return
92
+
93
+ client = Worklittle(api_key=args.api_key, base_url=args.base_url)
94
+ params = _query_params(args, unknown)
95
+ body = json.loads(args.body)
96
+ company = args.company or (args.rest[0] if args.rest else None)
97
+ id_ = args.id or (args.rest[0] if args.rest else None)
98
+
99
+ try:
100
+ result = _dispatch(
101
+ client, args.group, args.action or "", args.rest, params, body, company, id_, args
102
+ )
103
+ except WorklittleError as err:
104
+ print(
105
+ json.dumps({"error": {"code": err.code, "message": str(err), "status": err.status}}),
106
+ file=sys.stderr,
107
+ )
108
+ raise SystemExit(2 if err.status in (401, 403) else 1) from err
109
+ except Exception as err: # noqa: BLE001
110
+ print(json.dumps({"error": {"code": "CLI_ERROR", "message": str(err)}}), file=sys.stderr)
111
+ raise SystemExit(1) from err
112
+
113
+ _out(result, pretty=args.pretty)
114
+
115
+
116
+ def _query_params(args: argparse.Namespace, unknown: list[str]) -> dict[str, Any]:
117
+ """Forward declared + unknown --flags as API query params (JS CLI parity)."""
118
+ params: dict[str, Any] = {}
119
+ for key in ("q", "title", "company", "limit", "cursor", "include"):
120
+ val = getattr(args, key, None)
121
+ if val is not None and val != "":
122
+ params[key] = val
123
+ params.update(_parse_unknown_flags(unknown))
124
+ return params
125
+
126
+
127
+ def _parse_unknown_flags(unknown: list[str]) -> dict[str, Any]:
128
+ out: dict[str, Any] = {}
129
+ i = 0
130
+ while i < len(unknown):
131
+ token = unknown[i]
132
+ if token in ("-h", "--help"):
133
+ i += 1
134
+ continue
135
+ if not token.startswith("--"):
136
+ i += 1
137
+ continue
138
+ raw = token[2:]
139
+ if "=" in raw:
140
+ key, value = raw.split("=", 1)
141
+ if key and key not in _RESERVED and value != "":
142
+ out[key] = _coerce(value)
143
+ i += 1
144
+ continue
145
+ key = raw
146
+ nxt = unknown[i + 1] if i + 1 < len(unknown) else None
147
+ if key in _RESERVED:
148
+ i += 1
149
+ continue
150
+ if nxt is None or nxt.startswith("--"):
151
+ out[key] = True
152
+ i += 1
153
+ else:
154
+ if nxt != "":
155
+ out[key] = _coerce(nxt)
156
+ i += 2
157
+ return out
158
+
159
+
160
+ def _coerce(value: str) -> Any:
161
+ if value.isdigit() or (value.startswith("-") and value[1:].isdigit()):
162
+ try:
163
+ return int(value)
164
+ except ValueError:
165
+ return value
166
+ try:
167
+ if "." in value:
168
+ return float(value)
169
+ except ValueError:
170
+ pass
171
+ low = value.lower()
172
+ if low == "true":
173
+ return True
174
+ if low == "false":
175
+ return False
176
+ return value
177
+
178
+
179
+ def _out(data: object, *, pretty: bool) -> None:
180
+ print(json.dumps(data, indent=2 if pretty else None, default=str))
181
+
182
+
183
+ def _dispatch(client, group, action, rest, params, body, company, id_, args):
184
+ key = f"{group} {action}".strip()
185
+ # Public jobs index
186
+ if key == "jobs search":
187
+ return client.jobs.search(**params)
188
+ if key == "jobs get":
189
+ return client.jobs.get(id_ or rest[0])
190
+ if key == "jobs map":
191
+ return client.jobs.map(**params)
192
+ if key == "jobs salary-average":
193
+ return client.jobs.salary_average(**params)
194
+ if key == "companies search":
195
+ return client.companies.search(**params)
196
+ if key == "stats get":
197
+ return client.stats.get(**params)
198
+ # Public boards + apply
199
+ if key in ("boards get", "boards list"):
200
+ return client.boards.get(company or rest[0], **params)
201
+ if key == "boards jobs":
202
+ return client.boards.list_jobs(company or rest[0], **params)
203
+ if key == "boards job":
204
+ return client.boards.get_job(company or rest[0], id_ or rest[1], **params)
205
+ if key == "boards facets":
206
+ return client.boards.facets(company or rest[0], **params)
207
+ if key == "boards apply":
208
+ return client.boards.apply(company or rest[0], id_ or rest[1], body)
209
+ if key == "apply submit":
210
+ return client.apply.submit(id_ or rest[0], body)
211
+ if key == "apply-with-ai start":
212
+ return client.apply.start_with_ai(body)
213
+ if key in ("apply-with-ai status", "apply-with-ai get"):
214
+ return client.apply.get_with_ai_session(id_ or rest[0])
215
+ if key == "apply-with-ai continue":
216
+ answers = body.get("answers") if isinstance(body.get("answers"), dict) else body
217
+ return client.apply.continue_with_ai(
218
+ id_ or rest[0],
219
+ answers if isinstance(answers, dict) else {},
220
+ )
221
+ if key == "apply-with-ai approve":
222
+ return client.apply.approve_with_ai_submit(id_ or rest[0])
223
+ if key == "apply-with-ai stop":
224
+ return client.apply.stop_with_ai(id_ or rest[0])
225
+ if key in ("applied list", "applied-jobs list"):
226
+ return client.candidates.list(**params)
227
+ if key == "candidates get":
228
+ return client.candidates.get(id_ or rest[0])
229
+ if key == "employees list":
230
+ return client.employees.list(**params)
231
+ if key == "employees get":
232
+ return client.employees.get(id_ or rest[0])
233
+ if key == "email list":
234
+ return client.email.list_messages(**params)
235
+ if key == "email send":
236
+ return client.email.send(body)
237
+ if key == "email sends":
238
+ return client.email.list_sends(**params)
239
+ if key == "email send-get":
240
+ return client.email.get_send(id_ or rest[0])
241
+ if key == "email send-events":
242
+ return client.email.list_send_events(id_ or rest[0])
243
+ if key == "usage list":
244
+ return client.usage.list(**params)
245
+ if key == "billing balance":
246
+ return client.billing.balance()
247
+ if key == "billing invoices":
248
+ return client.billing.invoices(**params)
249
+ if key == "limits get":
250
+ return client.limits.get()
251
+ if key == "limits budget":
252
+ return client.limits.get_budget()
253
+ if key == "documents list":
254
+ return client.documents.list(**params)
255
+ if key == "attendance list":
256
+ return client.attendance.list(**params)
257
+ if key == "surveys list":
258
+ return client.surveys.list(**params)
259
+ if key == "surveys get":
260
+ return client.surveys.get(id_ or rest[0])
261
+ if key == "webhooks list":
262
+ return client.webhooks.list()
263
+ if key == "listings list":
264
+ return client.job_listings.list(**params)
265
+ if key == "resumes create":
266
+ return client.resumes.create(body)
267
+ if key == "agent tools":
268
+ return client.agent.list_tools()
269
+ if key == "agent tool":
270
+ return client.agent.run_tool(args.name or rest[0], body)
271
+ raise ValueError(f"Unknown command: {key}. Run: worklittle help")
272
+
273
+
274
+ if __name__ == "__main__":
275
+ main()
@@ -0,0 +1,462 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import time
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+ from .errors import PaymentRequiredError, QuotaExceededError, WorklittleError
10
+ from .webhooks import verify_webhook_signature
11
+
12
+
13
+ class _Http:
14
+ def __init__(
15
+ self,
16
+ *,
17
+ api_key: str | None,
18
+ base_url: str,
19
+ max_retries: int = 3,
20
+ timeout: float = 60.0,
21
+ ) -> None:
22
+ self.api_key = api_key
23
+ self.base_url = base_url.rstrip("/")
24
+ self.max_retries = max_retries
25
+ self._client = httpx.Client(timeout=timeout)
26
+
27
+ def request(
28
+ self,
29
+ method: str,
30
+ path: str,
31
+ *,
32
+ params: dict[str, Any] | None = None,
33
+ json: Any = None,
34
+ auth: bool = True,
35
+ ) -> Any:
36
+ url = f"{self.base_url}{path}"
37
+ headers: dict[str, str] = {}
38
+ if auth and self.api_key:
39
+ headers["Authorization"] = f"Bearer {self.api_key}"
40
+ attempt = 0
41
+ while True:
42
+ res = self._client.request(method, url, params=_clean(params), json=json, headers=headers)
43
+ if res.status_code == 429 and attempt < self.max_retries:
44
+ retry_after = int(res.headers.get("retry-after", "1") or "1")
45
+ time.sleep(max(1, retry_after))
46
+ attempt += 1
47
+ continue
48
+ request_id = res.headers.get("x-request-id") or res.headers.get("cf-ray")
49
+ if res.status_code == 204:
50
+ return None
51
+ try:
52
+ body = res.json() if res.content else None
53
+ except Exception:
54
+ body = {"raw": res.text}
55
+ if res.is_error:
56
+ err = (body or {}).get("error") if isinstance(body, dict) else None
57
+ message = (
58
+ (err or {}).get("message")
59
+ if isinstance(err, dict)
60
+ else f"{method} {path} failed ({res.status_code})"
61
+ )
62
+ code = (
63
+ (err or {}).get("code", str(res.status_code))
64
+ if isinstance(err, dict)
65
+ else str(res.status_code)
66
+ )
67
+ if res.status_code == 402 or code == "PAYMENT_REQUIRED":
68
+ raise PaymentRequiredError(
69
+ message,
70
+ code=code,
71
+ request_id=request_id,
72
+ body=body,
73
+ )
74
+ if code == "QUOTA_EXCEEDED":
75
+ raise QuotaExceededError(
76
+ message,
77
+ code=code,
78
+ request_id=request_id,
79
+ body=body,
80
+ )
81
+ raise WorklittleError(
82
+ message,
83
+ status=res.status_code,
84
+ code=code,
85
+ request_id=request_id,
86
+ body=body,
87
+ )
88
+ return body
89
+
90
+ def get(self, path: str, params: dict[str, Any] | None = None, auth: bool = True) -> Any:
91
+ return self.request("GET", path, params=params, auth=auth)
92
+
93
+ def post(self, path: str, json: Any = None, params: dict[str, Any] | None = None, auth: bool = True) -> Any:
94
+ return self.request("POST", path, params=params, json=json, auth=auth)
95
+
96
+ def put(self, path: str, json: Any = None, auth: bool = True) -> Any:
97
+ return self.request("PUT", path, json=json, auth=auth)
98
+
99
+ def patch(self, path: str, json: Any = None, params: dict[str, Any] | None = None, auth: bool = True) -> Any:
100
+ return self.request("PATCH", path, params=params, json=json, auth=auth)
101
+
102
+ def delete(self, path: str, params: dict[str, Any] | None = None, auth: bool = True) -> Any:
103
+ return self.request("DELETE", path, params=params, auth=auth)
104
+
105
+
106
+ def _clean(params: dict[str, Any] | None) -> dict[str, Any] | None:
107
+ if not params:
108
+ return None
109
+ return {k: v for k, v in params.items() if v is not None and v != ""}
110
+
111
+
112
+ class Worklittle:
113
+ def __init__(
114
+ self,
115
+ api_key: str | None = None,
116
+ *,
117
+ base_url: str = "https://api.worklittle.com",
118
+ max_retries: int = 3,
119
+ ) -> None:
120
+ self.http = _Http(
121
+ api_key=api_key or os.environ.get("WORKLITTLE_API_KEY"),
122
+ base_url=base_url,
123
+ max_retries=max_retries,
124
+ )
125
+ h = self.http
126
+ self.jobs = _Jobs(h)
127
+ self.companies = _Companies(h)
128
+ self.stats = _Stats(h)
129
+ self.boards = _Boards(h)
130
+ self.apply = _Apply(h)
131
+ self.applied_jobs = _AppliedJobs(h)
132
+ self.resumes = _Resumes(h)
133
+ self.cover_letters = _CoverLetters(h)
134
+ self.agent = _Agent(h)
135
+ self.job_listings = _JobListings(h)
136
+ self.employer_jobs = _EmployerJobs(h)
137
+ self.organization = _Organization(h)
138
+ self.candidates = _Candidates(h)
139
+ self.employees = _Employees(h)
140
+ self.email = _Email(h)
141
+ self.offers = _Offers(h)
142
+ self.webhooks = _Webhooks(h)
143
+ self.documents = _Documents(h)
144
+ self.attendance = _Attendance(h)
145
+ self.surveys = _Surveys(h)
146
+ self.usage = _Usage(h)
147
+ self.billing = _Billing(h)
148
+ self.limits = _Limits(h)
149
+ self.verify_webhook_signature = verify_webhook_signature
150
+
151
+
152
+ def create_worklittle(**kwargs: Any) -> Worklittle:
153
+ return Worklittle(**kwargs)
154
+
155
+
156
+ class _Jobs:
157
+ def __init__(self, http: _Http) -> None:
158
+ self._h = http
159
+
160
+ def search(self, **params: Any) -> Any:
161
+ return self._h.get("/jobs", params)
162
+
163
+ def get(self, id: str) -> Any:
164
+ return self._h.get(f"/jobs/{id}")
165
+
166
+ def map(self, **params: Any) -> Any:
167
+ return self._h.get("/jobs/map", params)
168
+
169
+ def salary_average(self, **params: Any) -> Any:
170
+ return self._h.get("/jobs/salary-average", params)
171
+
172
+
173
+ class _Companies:
174
+ def __init__(self, http: _Http) -> None:
175
+ self._h = http
176
+
177
+ def search(self, **params: Any) -> Any:
178
+ return self._h.get("/companies", params)
179
+
180
+
181
+ class _Stats:
182
+ def __init__(self, http: _Http) -> None:
183
+ self._h = http
184
+
185
+ def get(self, **params: Any) -> Any:
186
+ return self._h.get("/stats", params)
187
+
188
+
189
+ class _Boards:
190
+ def __init__(self, http: _Http) -> None:
191
+ self._h = http
192
+
193
+ def get(self, company: str, **params: Any) -> Any:
194
+ return self._h.get(f"/job-boards/{company}/board", params, auth=bool(self._h.api_key))
195
+
196
+ def list_jobs(self, company: str, **params: Any) -> Any:
197
+ return self._h.get(f"/job-boards/{company}/jobs", params, auth=bool(self._h.api_key))
198
+
199
+ def facets(self, company: str, **params: Any) -> Any:
200
+ return self._h.get(f"/job-boards/{company}/facets", params, auth=bool(self._h.api_key))
201
+
202
+ def get_job(self, company: str, id_or_slug: str, **params: Any) -> Any:
203
+ return self._h.get(
204
+ f"/job-boards/{company}/jobs/{id_or_slug}", params, auth=bool(self._h.api_key)
205
+ )
206
+
207
+ def apply(self, company: str, id_or_slug: str, body: dict[str, Any]) -> Any:
208
+ return self._h.post(f"/job-boards/{company}/jobs/{id_or_slug}/apply", body, auth=False)
209
+
210
+ def parse_resume(self, company: str, id_or_slug: str, resume_file: dict[str, Any]) -> Any:
211
+ return self._h.post(
212
+ f"/job-boards/{company}/jobs/{id_or_slug}/parse-resume",
213
+ {"resume_file": resume_file},
214
+ auth=False,
215
+ )
216
+
217
+
218
+ class _Apply:
219
+ def __init__(self, http: _Http) -> None:
220
+ self._h = http
221
+
222
+ def submit(self, job_id: str, body: dict[str, Any]) -> Any:
223
+ return self._h.post(f"/jobs/{job_id}/apply", body)
224
+
225
+ def start_with_ai(self, body: dict[str, Any]) -> Any:
226
+ return self._h.post("/v1/apply-with-ai", body)
227
+
228
+ def get_with_ai_session(self, session_id: str) -> Any:
229
+ return self._h.get(f"/v1/apply-with-ai/{session_id}")
230
+
231
+ def approve_with_ai_submit(self, session_id: str) -> Any:
232
+ return self._h.post(f"/v1/apply-with-ai/{session_id}/approve-submit", {})
233
+
234
+ def continue_with_ai(self, session_id: str, answers: dict[str, str] | None = None) -> Any:
235
+ return self._h.post(
236
+ f"/v1/apply-with-ai/{session_id}/continue",
237
+ {"answers": answers or {}},
238
+ )
239
+
240
+ def stop_with_ai(self, session_id: str) -> Any:
241
+ return self._h.post(f"/v1/apply-with-ai/{session_id}/stop", {})
242
+
243
+
244
+ class _AppliedJobs:
245
+ def __init__(self, http: _Http) -> None:
246
+ self._h = http
247
+
248
+ def list(self, **params: Any) -> Any:
249
+ return self._h.get("/applied-jobs", params)
250
+
251
+
252
+ class _Resumes:
253
+ def __init__(self, http: _Http) -> None:
254
+ self._h = http
255
+
256
+ def create(self, body: dict[str, Any]) -> Any:
257
+ return self._h.post("/v1/resumes", body)
258
+
259
+
260
+ class _CoverLetters:
261
+ def __init__(self, http: _Http) -> None:
262
+ self._h = http
263
+
264
+ def create(self, body: dict[str, Any]) -> Any:
265
+ return self._h.post("/v1/cover-letters", body)
266
+
267
+
268
+ class _Agent:
269
+ def __init__(self, http: _Http) -> None:
270
+ self._h = http
271
+
272
+ def list_tools(self) -> Any:
273
+ return self._h.get("/v1/agent/tools")
274
+
275
+ def run_tool(self, name: str, arguments: dict[str, Any] | None = None) -> Any:
276
+ return self._h.post("/v1/agent/tool", {"name": name, "arguments": arguments or {}})
277
+
278
+
279
+ class _JobListings:
280
+ def __init__(self, http: _Http) -> None:
281
+ self._h = http
282
+
283
+ def list(self, **params: Any) -> Any:
284
+ return self._h.get("/job-listings", params)
285
+
286
+ def create(self, body: dict[str, Any]) -> Any:
287
+ return self._h.post("/job-listings", body)
288
+
289
+ def update(self, id: str, body: dict[str, Any]) -> Any:
290
+ return self._h.patch(f"/job-listings/{id}", body)
291
+
292
+ def delete(self, id: str) -> Any:
293
+ return self._h.delete(f"/job-listings/{id}")
294
+
295
+
296
+ class _EmployerJobs:
297
+ def __init__(self, http: _Http) -> None:
298
+ self._h = http
299
+
300
+ def list(self, **params: Any) -> Any:
301
+ return self._h.get("/jobs/post", params)
302
+
303
+ def create(self, body: dict[str, Any]) -> Any:
304
+ return self._h.post("/jobs/post", body)
305
+
306
+ def update(self, id: str, body: dict[str, Any]) -> Any:
307
+ return self._h.patch(f"/jobs/post/{id}", body)
308
+
309
+
310
+ class _Organization:
311
+ def __init__(self, http: _Http) -> None:
312
+ self._h = http
313
+
314
+ def list_company_orgs(self, **params: Any) -> Any:
315
+ return self._h.get("/company-organizations", params)
316
+
317
+ def list_interview_questions(self, **params: Any) -> Any:
318
+ return self._h.get("/interview-questions", params)
319
+
320
+
321
+ class _Candidates:
322
+ def __init__(self, http: _Http) -> None:
323
+ self._h = http
324
+
325
+ def list(self, **params: Any) -> Any:
326
+ return self._h.get("/candidates", params)
327
+
328
+ def get(self, id: str) -> Any:
329
+ return self._h.get(f"/candidates/{id}")
330
+
331
+
332
+ class _Employees:
333
+ def __init__(self, http: _Http) -> None:
334
+ self._h = http
335
+
336
+ def list(self, **params: Any) -> Any:
337
+ return self._h.get("/employees", params)
338
+
339
+ def get(self, id: str) -> Any:
340
+ return self._h.get(f"/employees/{id}")
341
+
342
+ def create(self, body: dict[str, Any]) -> Any:
343
+ return self._h.post("/employees", body)
344
+
345
+
346
+ class _Email:
347
+ def __init__(self, http: _Http) -> None:
348
+ self._h = http
349
+
350
+ def list_messages(self, **params: Any) -> Any:
351
+ return self._h.get("/email-messages", params)
352
+
353
+ def send(self, body: dict[str, Any]) -> Any:
354
+ return self._h.post("/email/send", body)
355
+
356
+ def list_sends(self, **params: Any) -> Any:
357
+ return self._h.get("/email/sends", params)
358
+
359
+ def get_send(self, id: str) -> Any:
360
+ return self._h.get(f"/email/sends/{id}")
361
+
362
+ def list_send_events(self, id: str) -> Any:
363
+ return self._h.get(f"/email/sends/{id}/events")
364
+
365
+
366
+ class _Usage:
367
+ def __init__(self, http: _Http) -> None:
368
+ self._h = http
369
+
370
+ def list(self, **params: Any) -> Any:
371
+ """GET /usage — org audit logs (metered + org activity).
372
+
373
+ Optional params: source (all|public_api|platform_ai|org_activity),
374
+ since, until, cursor, limit, api_key_id.
375
+ Rows include where_label / where_href (Chat, Voice, job title, Private, …;
376
+ Chat/Voice hrefs are absolute when a chat id is known).
377
+ Activity rows have ledger_type=org_activity and charged_usd=0.
378
+ """
379
+ return self._h.get("/usage", params)
380
+
381
+
382
+ class _Billing:
383
+ def __init__(self, http: _Http) -> None:
384
+ self._h = http
385
+
386
+ def balance(self) -> Any:
387
+ return self._h.get("/billing/balance")
388
+
389
+ def invoices(self, **params: Any) -> Any:
390
+ return self._h.get("/billing/invoices", params)
391
+
392
+
393
+ class _Limits:
394
+ def __init__(self, http: _Http) -> None:
395
+ self._h = http
396
+
397
+ def get(self) -> Any:
398
+ return self._h.get("/limits")
399
+
400
+ def get_budget(self) -> Any:
401
+ return self._h.get("/limits/budget")
402
+
403
+ def set_budget(self, body: dict[str, Any]) -> Any:
404
+ return self._h.put("/limits/budget", body)
405
+
406
+
407
+ class _Offers:
408
+ def __init__(self, http: _Http) -> None:
409
+ self._h = http
410
+
411
+ def list(self, **params: Any) -> Any:
412
+ return self._h.get("/offers", params)
413
+
414
+
415
+ class _Webhooks:
416
+ def __init__(self, http: _Http) -> None:
417
+ self._h = http
418
+
419
+ def list(self) -> Any:
420
+ return self._h.get("/webhooks")
421
+
422
+ def create(self, body: dict[str, Any]) -> Any:
423
+ return self._h.post("/webhooks", body)
424
+
425
+ def test(self, id: str) -> Any:
426
+ return self._h.post(f"/webhooks/{id}/test", {})
427
+
428
+
429
+ class _Documents:
430
+ def __init__(self, http: _Http) -> None:
431
+ self._h = http
432
+
433
+ def list(self, **params: Any) -> Any:
434
+ return self._h.get("/platform/documents", params)
435
+
436
+ def templates(self, **params: Any) -> Any:
437
+ return self._h.get("/platform/documents/templates", params)
438
+
439
+
440
+ class _Attendance:
441
+ def __init__(self, http: _Http) -> None:
442
+ self._h = http
443
+
444
+ def list(self, **params: Any) -> Any:
445
+ return self._h.get("/attendance", params)
446
+
447
+ def me(self, **params: Any) -> Any:
448
+ return self._h.get("/attendance/me", params)
449
+
450
+
451
+ class _Surveys:
452
+ def __init__(self, http: _Http) -> None:
453
+ self._h = http
454
+
455
+ def list(self, **params: Any) -> Any:
456
+ return self._h.get("/platform/surveys", params)
457
+
458
+ def get(self, id: str) -> Any:
459
+ return self._h.get(f"/platform/surveys/{id}")
460
+
461
+ def create(self, body: dict[str, Any]) -> Any:
462
+ return self._h.post("/platform/surveys", body)
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class WorklittleError(Exception):
5
+ def __init__(
6
+ self,
7
+ message: str,
8
+ *,
9
+ status: int,
10
+ code: str = "UNKNOWN",
11
+ request_id: str | None = None,
12
+ body: object = None,
13
+ ) -> None:
14
+ super().__init__(message)
15
+ self.status = status
16
+ self.code = code
17
+ self.request_id = request_id
18
+ self.body = body
19
+
20
+
21
+ class PaymentRequiredError(WorklittleError):
22
+ """HTTP 402 — do not retry metered requests until billing is fixed."""
23
+
24
+ def __init__(
25
+ self,
26
+ message: str,
27
+ *,
28
+ request_id: str | None = None,
29
+ body: object = None,
30
+ code: str = "PAYMENT_REQUIRED",
31
+ ) -> None:
32
+ super().__init__(message, status=402, code=code, request_id=request_id, body=body)
33
+
34
+
35
+ class QuotaExceededError(WorklittleError):
36
+ """HTTP 429 + QUOTA_EXCEEDED — monthly free quota exhausted; inquire for higher limits."""
37
+
38
+ def __init__(
39
+ self,
40
+ message: str,
41
+ *,
42
+ request_id: str | None = None,
43
+ body: object = None,
44
+ code: str = "QUOTA_EXCEEDED",
45
+ ) -> None:
46
+ super().__init__(message, status=429, code=code, request_id=request_id, body=body)
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import hmac
5
+ import time
6
+
7
+
8
+ def verify_webhook_signature(
9
+ *,
10
+ signature_header: str,
11
+ timestamp: str,
12
+ raw_body: str,
13
+ secret: str,
14
+ tolerance_seconds: int = 300,
15
+ ) -> bool:
16
+ try:
17
+ ts = int(timestamp)
18
+ except ValueError:
19
+ return False
20
+ if abs(int(time.time()) - ts) > tolerance_seconds:
21
+ return False
22
+ received = signature_header.removeprefix("v1=").strip()
23
+ expected = hmac.new(
24
+ secret.encode("utf-8"),
25
+ f"{timestamp}.{raw_body}".encode("utf-8"),
26
+ hashlib.sha256,
27
+ ).hexdigest()
28
+ return hmac.compare_digest(expected, received)