loxo-cli 0.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.
- loxo_cli-0.1.0/.github/workflows/ci.yml +21 -0
- loxo_cli-0.1.0/.github/workflows/publish.yml +17 -0
- loxo_cli-0.1.0/.gitignore +8 -0
- loxo_cli-0.1.0/CHANGELOG.md +17 -0
- loxo_cli-0.1.0/LICENSE +21 -0
- loxo_cli-0.1.0/PKG-INFO +135 -0
- loxo_cli-0.1.0/README.md +120 -0
- loxo_cli-0.1.0/pyproject.toml +56 -0
- loxo_cli-0.1.0/src/loxo_cli/__init__.py +1 -0
- loxo_cli-0.1.0/src/loxo_cli/__main__.py +132 -0
- loxo_cli-0.1.0/src/loxo_cli/client.py +88 -0
- loxo_cli-0.1.0/src/loxo_cli/commands/__init__.py +0 -0
- loxo_cli-0.1.0/src/loxo_cli/commands/_app.py +28 -0
- loxo_cli-0.1.0/src/loxo_cli/commands/_helpers.py +48 -0
- loxo_cli-0.1.0/src/loxo_cli/commands/activities.py +71 -0
- loxo_cli-0.1.0/src/loxo_cli/commands/api.py +69 -0
- loxo_cli-0.1.0/src/loxo_cli/commands/candidates.py +87 -0
- loxo_cli-0.1.0/src/loxo_cli/commands/companies.py +100 -0
- loxo_cli-0.1.0/src/loxo_cli/commands/configure.py +56 -0
- loxo_cli-0.1.0/src/loxo_cli/commands/deals.py +101 -0
- loxo_cli-0.1.0/src/loxo_cli/commands/jobs.py +76 -0
- loxo_cli-0.1.0/src/loxo_cli/commands/people.py +91 -0
- loxo_cli-0.1.0/src/loxo_cli/commands/ref.py +60 -0
- loxo_cli-0.1.0/src/loxo_cli/commands/webhooks.py +98 -0
- loxo_cli-0.1.0/src/loxo_cli/config.py +140 -0
- loxo_cli-0.1.0/src/loxo_cli/errors.py +65 -0
- loxo_cli-0.1.0/src/loxo_cli/models/__init__.py +20 -0
- loxo_cli-0.1.0/src/loxo_cli/models/base.py +13 -0
- loxo_cli-0.1.0/src/loxo_cli/models/candidate.py +11 -0
- loxo_cli-0.1.0/src/loxo_cli/models/company.py +11 -0
- loxo_cli-0.1.0/src/loxo_cli/models/deal.py +11 -0
- loxo_cli-0.1.0/src/loxo_cli/models/job.py +11 -0
- loxo_cli-0.1.0/src/loxo_cli/models/person.py +14 -0
- loxo_cli-0.1.0/src/loxo_cli/models/reference.py +10 -0
- loxo_cli-0.1.0/src/loxo_cli/models/webhook.py +12 -0
- loxo_cli-0.1.0/src/loxo_cli/output.py +98 -0
- loxo_cli-0.1.0/src/loxo_cli/pagination.py +90 -0
- loxo_cli-0.1.0/tests/conftest.py +0 -0
- loxo_cli-0.1.0/tests/test_app_state.py +36 -0
- loxo_cli-0.1.0/tests/test_client.py +75 -0
- loxo_cli-0.1.0/tests/test_cmd_activities.py +51 -0
- loxo_cli-0.1.0/tests/test_cmd_api.py +74 -0
- loxo_cli-0.1.0/tests/test_cmd_candidates.py +55 -0
- loxo_cli-0.1.0/tests/test_cmd_companies.py +57 -0
- loxo_cli-0.1.0/tests/test_cmd_configure.py +55 -0
- loxo_cli-0.1.0/tests/test_cmd_deals.py +48 -0
- loxo_cli-0.1.0/tests/test_cmd_jobs.py +58 -0
- loxo_cli-0.1.0/tests/test_cmd_people.py +67 -0
- loxo_cli-0.1.0/tests/test_cmd_ref.py +43 -0
- loxo_cli-0.1.0/tests/test_cmd_webhooks.py +80 -0
- loxo_cli-0.1.0/tests/test_config.py +97 -0
- loxo_cli-0.1.0/tests/test_error_exit_codes.py +52 -0
- loxo_cli-0.1.0/tests/test_errors.py +36 -0
- loxo_cli-0.1.0/tests/test_helpers.py +56 -0
- loxo_cli-0.1.0/tests/test_models.py +38 -0
- loxo_cli-0.1.0/tests/test_output.py +52 -0
- loxo_cli-0.1.0/tests/test_pagination.py +73 -0
- loxo_cli-0.1.0/tests/test_smoke.py +17 -0
- loxo_cli-0.1.0/uv.lock +714 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request:
|
|
6
|
+
jobs:
|
|
7
|
+
test:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
strategy:
|
|
10
|
+
matrix:
|
|
11
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: astral-sh/setup-uv@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: ${{ matrix.python-version }}
|
|
17
|
+
- run: uv sync --all-extras --dev
|
|
18
|
+
- run: uv run ruff check src tests
|
|
19
|
+
- run: uv run black --check src tests
|
|
20
|
+
- run: uv run mypy
|
|
21
|
+
- run: uv run pytest -q
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags: ["v*"]
|
|
5
|
+
jobs:
|
|
6
|
+
publish:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
environment: pypi
|
|
9
|
+
permissions:
|
|
10
|
+
id-token: write # trusted publishing (OIDC)
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: astral-sh/setup-uv@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.12"
|
|
16
|
+
- run: uv build
|
|
17
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Initial release of `loxo-cli`.
|
|
8
|
+
- Credential profiles via `loxo configure` (flags > env > `~/.config/loxo/config.toml`),
|
|
9
|
+
including `api_key_cmd` for pulling the key from a secrets manager.
|
|
10
|
+
- Typed command groups: `people`, `jobs`, `companies`, `deals`, `candidates`,
|
|
11
|
+
`activities`, `webhooks`, and `ref` (reference data and custom fields).
|
|
12
|
+
- Generic `loxo api METHOD PATH` escape hatch for any endpoint, with `--all`
|
|
13
|
+
auto-pagination.
|
|
14
|
+
- Scheme-aware pagination (`scroll_id`, `page`, `after_id`) with `--all`.
|
|
15
|
+
- TTY tables plus `--json`/`--jq` for scripting; tolerant models that preserve
|
|
16
|
+
custom/dynamic fields.
|
|
17
|
+
- Documented exit codes (auth, not-found, rate-limited, server, timeout/network).
|
loxo_cli-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrew Mitchell
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
loxo_cli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: loxo-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unofficial command-line interface for the Loxo recruiting ATS/CRM API
|
|
5
|
+
Project-URL: Homepage, https://github.com/alphaomegateam/loxo-cli
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: click>=8
|
|
10
|
+
Requires-Dist: httpx>=0.27
|
|
11
|
+
Requires-Dist: pydantic>=2
|
|
12
|
+
Requires-Dist: rich>=13
|
|
13
|
+
Requires-Dist: typer>=0.12
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# loxo-cli
|
|
17
|
+
|
|
18
|
+
A fast, ergonomic command-line interface for the [Loxo](https://loxo.co) recruiting
|
|
19
|
+
ATS/CRM REST API. It offers typed subcommands for the common resources (people, jobs,
|
|
20
|
+
companies, deals, candidates, activities, webhooks, reference data) plus a generic
|
|
21
|
+
`loxo api` escape hatch that can call any endpoint. Output is human-friendly tables on a
|
|
22
|
+
terminal and clean JSON when piped, so it fits both interactive use and scripts.
|
|
23
|
+
|
|
24
|
+
Unofficial — not affiliated with Loxo, Inc.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uvx loxo-cli # run without installing
|
|
30
|
+
pipx install loxo-cli # or install as a user tool
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quickstart
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
loxo configure # set up a profile
|
|
37
|
+
loxo people list --query "engineer" # human table
|
|
38
|
+
loxo people list --json | jq '.' # JSON for scripts
|
|
39
|
+
loxo api GET jobs/123 # raw escape hatch
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Configuration
|
|
43
|
+
|
|
44
|
+
Credentials resolve with the precedence **flags > environment > config file**.
|
|
45
|
+
|
|
46
|
+
Environment variables:
|
|
47
|
+
|
|
48
|
+
| Variable | Meaning |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `LOXO_API_KEY` | API bearer token |
|
|
51
|
+
| `LOXO_API_SLUG` | Agency slug (the `{slug}` in every request URL) |
|
|
52
|
+
| `LOXO_BASE_URL` | API base URL (default `https://app.loxo.co/api`) |
|
|
53
|
+
| `LOXO_PROFILE` | Default profile name to use |
|
|
54
|
+
|
|
55
|
+
The config file lives at `~/.config/loxo/config.toml` (or `$XDG_CONFIG_HOME/loxo/config.toml`)
|
|
56
|
+
and is written with `0600` permissions. Example:
|
|
57
|
+
|
|
58
|
+
```toml
|
|
59
|
+
default_profile = "prod"
|
|
60
|
+
|
|
61
|
+
[profile.prod]
|
|
62
|
+
slug = "acme"
|
|
63
|
+
base_url = "https://app.loxo.co/api"
|
|
64
|
+
api_key = "your-token"
|
|
65
|
+
|
|
66
|
+
[profile.staging]
|
|
67
|
+
slug = "acme-staging"
|
|
68
|
+
# Pull the key from a secrets manager instead of storing it in plaintext:
|
|
69
|
+
api_key_cmd = "op read op://Private/loxo-staging/credential"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`api_key_cmd` is run on demand and its stdout is used as the key, so the secret never has
|
|
73
|
+
to live in the file. The key is never printed by `loxo configure list`, logged, or shown in
|
|
74
|
+
`--verbose` output.
|
|
75
|
+
|
|
76
|
+
## Commands
|
|
77
|
+
|
|
78
|
+
| Group | What it does |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `people` | List/search, get, create, update people |
|
|
81
|
+
| `jobs` | List, get, create, update jobs |
|
|
82
|
+
| `companies` | List/search, get, create, update companies |
|
|
83
|
+
| `deals` | List, get, create, update deals |
|
|
84
|
+
| `candidates` | List/get/add/update candidates under a job |
|
|
85
|
+
| `activities` | List and add person events (activities) |
|
|
86
|
+
| `webhooks` | Full CRUD for webhooks (with enum validation) |
|
|
87
|
+
| `ref` | Reference lookups: job/activity/source/person types, lists, custom fields, hierarchies |
|
|
88
|
+
| `api` | Generic escape hatch — call any endpoint directly |
|
|
89
|
+
| `configure` | Create and list credential profiles |
|
|
90
|
+
|
|
91
|
+
Custom (dynamic) fields are supported on writes via repeatable `--field key=value`
|
|
92
|
+
(use `key[]=value` to force a list, e.g. hierarchy fields). Discover valid keys with
|
|
93
|
+
`loxo ref custom-fields`.
|
|
94
|
+
|
|
95
|
+
## Output
|
|
96
|
+
|
|
97
|
+
On a terminal, list and object results render as Rich tables. Pipe the command or pass
|
|
98
|
+
`--json` to get machine-readable JSON; `--jq '<path>'` applies a small built-in selector
|
|
99
|
+
(e.g. `--jq '.[].id'`) without needing the `jq` binary.
|
|
100
|
+
|
|
101
|
+
## Exit codes
|
|
102
|
+
|
|
103
|
+
| Code | Meaning |
|
|
104
|
+
|---|---|
|
|
105
|
+
| 0 | Success |
|
|
106
|
+
| 1 | Generic error |
|
|
107
|
+
| 2 | Usage error (bad flags/arguments) |
|
|
108
|
+
| 3 | Authentication/authorization failure (401/403) |
|
|
109
|
+
| 4 | Not found (404) |
|
|
110
|
+
| 5 | Rate limited (429) |
|
|
111
|
+
| 6 | Server error (5xx) |
|
|
112
|
+
| 7 | Timeout or network failure |
|
|
113
|
+
|
|
114
|
+
## Pagination
|
|
115
|
+
|
|
116
|
+
Loxo paginates differently per endpoint: cursor (`scroll_id`), offset (`page`), and keyset
|
|
117
|
+
(`after_id`). `loxo-cli` detects and handles all three. List commands fetch a single page by
|
|
118
|
+
default; pass `--all` to transparently walk every page. The generic `loxo api ... --all`
|
|
119
|
+
auto-detects the scheme (or force it with `--paginate scroll_id|page|after_id`).
|
|
120
|
+
|
|
121
|
+
## Contributing
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
uv sync # install dependencies
|
|
125
|
+
uv run pytest # run the test suite (HTTP is mocked; no live calls)
|
|
126
|
+
uv run ruff check src tests
|
|
127
|
+
uv run black --check src tests
|
|
128
|
+
uv run mypy
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Commits follow [Conventional Commits](https://www.conventionalcommits.org/).
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT. See [LICENSE](LICENSE).
|
loxo_cli-0.1.0/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# loxo-cli
|
|
2
|
+
|
|
3
|
+
A fast, ergonomic command-line interface for the [Loxo](https://loxo.co) recruiting
|
|
4
|
+
ATS/CRM REST API. It offers typed subcommands for the common resources (people, jobs,
|
|
5
|
+
companies, deals, candidates, activities, webhooks, reference data) plus a generic
|
|
6
|
+
`loxo api` escape hatch that can call any endpoint. Output is human-friendly tables on a
|
|
7
|
+
terminal and clean JSON when piped, so it fits both interactive use and scripts.
|
|
8
|
+
|
|
9
|
+
Unofficial — not affiliated with Loxo, Inc.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uvx loxo-cli # run without installing
|
|
15
|
+
pipx install loxo-cli # or install as a user tool
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quickstart
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
loxo configure # set up a profile
|
|
22
|
+
loxo people list --query "engineer" # human table
|
|
23
|
+
loxo people list --json | jq '.' # JSON for scripts
|
|
24
|
+
loxo api GET jobs/123 # raw escape hatch
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Configuration
|
|
28
|
+
|
|
29
|
+
Credentials resolve with the precedence **flags > environment > config file**.
|
|
30
|
+
|
|
31
|
+
Environment variables:
|
|
32
|
+
|
|
33
|
+
| Variable | Meaning |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `LOXO_API_KEY` | API bearer token |
|
|
36
|
+
| `LOXO_API_SLUG` | Agency slug (the `{slug}` in every request URL) |
|
|
37
|
+
| `LOXO_BASE_URL` | API base URL (default `https://app.loxo.co/api`) |
|
|
38
|
+
| `LOXO_PROFILE` | Default profile name to use |
|
|
39
|
+
|
|
40
|
+
The config file lives at `~/.config/loxo/config.toml` (or `$XDG_CONFIG_HOME/loxo/config.toml`)
|
|
41
|
+
and is written with `0600` permissions. Example:
|
|
42
|
+
|
|
43
|
+
```toml
|
|
44
|
+
default_profile = "prod"
|
|
45
|
+
|
|
46
|
+
[profile.prod]
|
|
47
|
+
slug = "acme"
|
|
48
|
+
base_url = "https://app.loxo.co/api"
|
|
49
|
+
api_key = "your-token"
|
|
50
|
+
|
|
51
|
+
[profile.staging]
|
|
52
|
+
slug = "acme-staging"
|
|
53
|
+
# Pull the key from a secrets manager instead of storing it in plaintext:
|
|
54
|
+
api_key_cmd = "op read op://Private/loxo-staging/credential"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`api_key_cmd` is run on demand and its stdout is used as the key, so the secret never has
|
|
58
|
+
to live in the file. The key is never printed by `loxo configure list`, logged, or shown in
|
|
59
|
+
`--verbose` output.
|
|
60
|
+
|
|
61
|
+
## Commands
|
|
62
|
+
|
|
63
|
+
| Group | What it does |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `people` | List/search, get, create, update people |
|
|
66
|
+
| `jobs` | List, get, create, update jobs |
|
|
67
|
+
| `companies` | List/search, get, create, update companies |
|
|
68
|
+
| `deals` | List, get, create, update deals |
|
|
69
|
+
| `candidates` | List/get/add/update candidates under a job |
|
|
70
|
+
| `activities` | List and add person events (activities) |
|
|
71
|
+
| `webhooks` | Full CRUD for webhooks (with enum validation) |
|
|
72
|
+
| `ref` | Reference lookups: job/activity/source/person types, lists, custom fields, hierarchies |
|
|
73
|
+
| `api` | Generic escape hatch — call any endpoint directly |
|
|
74
|
+
| `configure` | Create and list credential profiles |
|
|
75
|
+
|
|
76
|
+
Custom (dynamic) fields are supported on writes via repeatable `--field key=value`
|
|
77
|
+
(use `key[]=value` to force a list, e.g. hierarchy fields). Discover valid keys with
|
|
78
|
+
`loxo ref custom-fields`.
|
|
79
|
+
|
|
80
|
+
## Output
|
|
81
|
+
|
|
82
|
+
On a terminal, list and object results render as Rich tables. Pipe the command or pass
|
|
83
|
+
`--json` to get machine-readable JSON; `--jq '<path>'` applies a small built-in selector
|
|
84
|
+
(e.g. `--jq '.[].id'`) without needing the `jq` binary.
|
|
85
|
+
|
|
86
|
+
## Exit codes
|
|
87
|
+
|
|
88
|
+
| Code | Meaning |
|
|
89
|
+
|---|---|
|
|
90
|
+
| 0 | Success |
|
|
91
|
+
| 1 | Generic error |
|
|
92
|
+
| 2 | Usage error (bad flags/arguments) |
|
|
93
|
+
| 3 | Authentication/authorization failure (401/403) |
|
|
94
|
+
| 4 | Not found (404) |
|
|
95
|
+
| 5 | Rate limited (429) |
|
|
96
|
+
| 6 | Server error (5xx) |
|
|
97
|
+
| 7 | Timeout or network failure |
|
|
98
|
+
|
|
99
|
+
## Pagination
|
|
100
|
+
|
|
101
|
+
Loxo paginates differently per endpoint: cursor (`scroll_id`), offset (`page`), and keyset
|
|
102
|
+
(`after_id`). `loxo-cli` detects and handles all three. List commands fetch a single page by
|
|
103
|
+
default; pass `--all` to transparently walk every page. The generic `loxo api ... --all`
|
|
104
|
+
auto-detects the scheme (or force it with `--paginate scroll_id|page|after_id`).
|
|
105
|
+
|
|
106
|
+
## Contributing
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
uv sync # install dependencies
|
|
110
|
+
uv run pytest # run the test suite (HTTP is mocked; no live calls)
|
|
111
|
+
uv run ruff check src tests
|
|
112
|
+
uv run black --check src tests
|
|
113
|
+
uv run mypy
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Commits follow [Conventional Commits](https://www.conventionalcommits.org/).
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "loxo-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Unofficial command-line interface for the Loxo recruiting ATS/CRM API"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"typer>=0.12",
|
|
10
|
+
"click>=8", # imported directly in errors.py (ClickException base); declare it explicitly
|
|
11
|
+
"httpx>=0.27",
|
|
12
|
+
"pydantic>=2",
|
|
13
|
+
"rich>=13",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
loxo = "loxo_cli.__main__:app"
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://github.com/alphaomegateam/loxo-cli"
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = [
|
|
24
|
+
"pytest>=8",
|
|
25
|
+
"pytest-mock>=3.14",
|
|
26
|
+
"respx>=0.21",
|
|
27
|
+
"ruff>=0.6",
|
|
28
|
+
"black>=24",
|
|
29
|
+
"mypy>=1.11",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["hatchling"]
|
|
34
|
+
build-backend = "hatchling.build"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["src/loxo_cli"]
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
pythonpath = ["src"]
|
|
41
|
+
addopts = "-ra"
|
|
42
|
+
|
|
43
|
+
[tool.ruff]
|
|
44
|
+
line-length = 100
|
|
45
|
+
src = ["src", "tests"]
|
|
46
|
+
|
|
47
|
+
[tool.black]
|
|
48
|
+
line-length = 100
|
|
49
|
+
target-version = ["py311"]
|
|
50
|
+
|
|
51
|
+
[tool.mypy]
|
|
52
|
+
python_version = "3.11"
|
|
53
|
+
mypy_path = "src"
|
|
54
|
+
packages = ["loxo_cli"]
|
|
55
|
+
explicit_package_bases = true
|
|
56
|
+
ignore_missing_imports = true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from loxo_cli import __version__
|
|
11
|
+
from loxo_cli.client import LoxoClient, build_client
|
|
12
|
+
from loxo_cli.commands._app import LoxoGroup
|
|
13
|
+
from loxo_cli.config import LoxoSettings, load_settings
|
|
14
|
+
from loxo_cli.output import render
|
|
15
|
+
|
|
16
|
+
HELP_EPILOG = "Unofficial — not affiliated with Loxo, Inc."
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(
|
|
19
|
+
cls=LoxoGroup,
|
|
20
|
+
help="loxo — command-line interface for the Loxo recruiting API.",
|
|
21
|
+
epilog=HELP_EPILOG,
|
|
22
|
+
no_args_is_help=True,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class AppState:
|
|
28
|
+
profile: Optional[str]
|
|
29
|
+
api_key: Optional[str]
|
|
30
|
+
slug: Optional[str]
|
|
31
|
+
base_url: Optional[str]
|
|
32
|
+
json_out: bool
|
|
33
|
+
jq: Optional[str]
|
|
34
|
+
verbose: bool
|
|
35
|
+
no_color: bool
|
|
36
|
+
config_path: Optional[Path] = None
|
|
37
|
+
_settings: Optional[LoxoSettings] = field(default=None, repr=False)
|
|
38
|
+
|
|
39
|
+
def settings(self) -> LoxoSettings:
|
|
40
|
+
if self._settings is None:
|
|
41
|
+
self._settings = load_settings(
|
|
42
|
+
profile=self.profile,
|
|
43
|
+
api_key=self.api_key,
|
|
44
|
+
slug=self.slug,
|
|
45
|
+
base_url=self.base_url,
|
|
46
|
+
config_path=self.config_path,
|
|
47
|
+
)
|
|
48
|
+
return self._settings
|
|
49
|
+
|
|
50
|
+
def client(self) -> LoxoClient:
|
|
51
|
+
return build_client(self.settings(), verbose=self.verbose)
|
|
52
|
+
|
|
53
|
+
def console(self) -> Console:
|
|
54
|
+
return Console(no_color=self.no_color)
|
|
55
|
+
|
|
56
|
+
def emit(self, data: Any, *, columns: list[str] | None = None) -> None:
|
|
57
|
+
render(
|
|
58
|
+
data,
|
|
59
|
+
as_json=self.json_out,
|
|
60
|
+
jq=self.jq,
|
|
61
|
+
columns=columns,
|
|
62
|
+
console=self.console(),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _version_callback(value: bool) -> None:
|
|
67
|
+
if value:
|
|
68
|
+
typer.echo(__version__)
|
|
69
|
+
raise typer.Exit()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.callback()
|
|
73
|
+
def main(
|
|
74
|
+
ctx: typer.Context,
|
|
75
|
+
version: bool = typer.Option(
|
|
76
|
+
False, "--version", callback=_version_callback, is_eager=True, help="Show version and exit."
|
|
77
|
+
),
|
|
78
|
+
profile: Optional[str] = typer.Option(None, "--profile", help="Config profile."),
|
|
79
|
+
api_key: Optional[str] = typer.Option(None, "--api-key", help="Loxo API key."),
|
|
80
|
+
slug: Optional[str] = typer.Option(None, "--slug", help="Agency slug."),
|
|
81
|
+
base_url: Optional[str] = typer.Option(None, "--base-url", help="API base URL."),
|
|
82
|
+
json_out: bool = typer.Option(False, "--json", help="Force JSON output."),
|
|
83
|
+
jq: Optional[str] = typer.Option(None, "--jq", help="Filter output (dotted path)."),
|
|
84
|
+
quiet: bool = typer.Option(False, "--quiet", help="Suppress non-error output."),
|
|
85
|
+
verbose: bool = typer.Option(False, "-v", "--verbose", help="Log requests to stderr."),
|
|
86
|
+
no_color: bool = typer.Option(False, "--no-color", help="Disable color."),
|
|
87
|
+
) -> None:
|
|
88
|
+
"""loxo CLI. Unofficial — not affiliated with Loxo, Inc."""
|
|
89
|
+
ctx.obj = AppState(
|
|
90
|
+
profile=profile,
|
|
91
|
+
api_key=api_key,
|
|
92
|
+
slug=slug,
|
|
93
|
+
base_url=base_url,
|
|
94
|
+
json_out=json_out,
|
|
95
|
+
jq=jq,
|
|
96
|
+
verbose=verbose,
|
|
97
|
+
no_color=no_color,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
from loxo_cli.commands import api as _api_cmd # noqa: E402
|
|
102
|
+
from loxo_cli.commands.activities import activities_app # noqa: E402
|
|
103
|
+
from loxo_cli.commands.candidates import candidates_app # noqa: E402
|
|
104
|
+
from loxo_cli.commands.companies import companies_app # noqa: E402
|
|
105
|
+
from loxo_cli.commands.configure import configure_app # noqa: E402
|
|
106
|
+
from loxo_cli.commands.deals import deals_app # noqa: E402
|
|
107
|
+
from loxo_cli.commands.jobs import jobs_app # noqa: E402
|
|
108
|
+
from loxo_cli.commands.people import people_app # noqa: E402
|
|
109
|
+
from loxo_cli.commands.ref import ref_app # noqa: E402
|
|
110
|
+
from loxo_cli.commands.webhooks import webhooks_app # noqa: E402
|
|
111
|
+
|
|
112
|
+
_api_cmd.register(app)
|
|
113
|
+
app.add_typer(configure_app, name="configure")
|
|
114
|
+
app.add_typer(people_app, name="people")
|
|
115
|
+
app.add_typer(jobs_app, name="jobs")
|
|
116
|
+
app.add_typer(companies_app, name="companies")
|
|
117
|
+
app.add_typer(deals_app, name="deals")
|
|
118
|
+
app.add_typer(candidates_app, name="candidates")
|
|
119
|
+
app.add_typer(activities_app, name="activities")
|
|
120
|
+
app.add_typer(webhooks_app, name="webhooks")
|
|
121
|
+
app.add_typer(ref_app, name="ref")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def run() -> None:
|
|
125
|
+
# Exit-code mapping happens in LoxoGroup.invoke (commands/_app.py, set via
|
|
126
|
+
# typer.Typer(cls=LoxoGroup)): Typer does NOT honor a raised ClickException's
|
|
127
|
+
# exit_code, so domain errors become typer.Exit with the mapped code.
|
|
128
|
+
app()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
run()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Any, Mapping
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from loxo_cli.config import LoxoSettings
|
|
9
|
+
from loxo_cli.errors import LoxoError
|
|
10
|
+
|
|
11
|
+
TIMEOUT = 30.0
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def url_for(settings: LoxoSettings, endpoint: str) -> str:
|
|
15
|
+
return f"{settings.base_url}/{settings.slug}/{endpoint.lstrip('/')}"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LoxoClient:
|
|
19
|
+
def __init__(self, settings: LoxoSettings, *, verbose: bool = False) -> None:
|
|
20
|
+
self._settings = settings
|
|
21
|
+
self._verbose = verbose
|
|
22
|
+
self._http = httpx.Client(
|
|
23
|
+
headers={
|
|
24
|
+
"Authorization": f"Bearer {settings.api_key}",
|
|
25
|
+
"Accept": "application/json",
|
|
26
|
+
},
|
|
27
|
+
follow_redirects=True,
|
|
28
|
+
timeout=TIMEOUT,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def __enter__(self) -> "LoxoClient":
|
|
32
|
+
return self
|
|
33
|
+
|
|
34
|
+
def __exit__(self, *exc: object) -> None:
|
|
35
|
+
self.close()
|
|
36
|
+
|
|
37
|
+
def close(self) -> None:
|
|
38
|
+
self._http.close()
|
|
39
|
+
|
|
40
|
+
def request(
|
|
41
|
+
self,
|
|
42
|
+
method: str,
|
|
43
|
+
endpoint: str,
|
|
44
|
+
*,
|
|
45
|
+
params: Mapping[str, Any] | None = None,
|
|
46
|
+
json: Any | None = None,
|
|
47
|
+
) -> Any:
|
|
48
|
+
target = url_for(self._settings, endpoint)
|
|
49
|
+
if self._verbose:
|
|
50
|
+
# Method + URL only. Never headers (would leak the bearer token).
|
|
51
|
+
print(f"{method.upper()} {target}", file=sys.stderr)
|
|
52
|
+
headers = {"Content-Type": "application/json"} if json is not None else None
|
|
53
|
+
try:
|
|
54
|
+
response = self._http.request(method, target, params=params, json=json, headers=headers)
|
|
55
|
+
response.raise_for_status()
|
|
56
|
+
except httpx.TimeoutException as exc:
|
|
57
|
+
raise LoxoError(
|
|
58
|
+
f"Loxo {method} {endpoint} timed out", status_code=None, is_timeout=True
|
|
59
|
+
) from exc
|
|
60
|
+
except httpx.HTTPStatusError as exc:
|
|
61
|
+
raise LoxoError(
|
|
62
|
+
f"Loxo {method} {endpoint} returned {exc.response.status_code}: "
|
|
63
|
+
f"{exc.response.text[:500]}",
|
|
64
|
+
status_code=exc.response.status_code,
|
|
65
|
+
) from exc
|
|
66
|
+
except httpx.HTTPError as exc:
|
|
67
|
+
raise LoxoError(
|
|
68
|
+
f"Loxo {method} {endpoint} request failed: {exc}", status_code=None
|
|
69
|
+
) from exc
|
|
70
|
+
if not response.content:
|
|
71
|
+
return None
|
|
72
|
+
return response.json()
|
|
73
|
+
|
|
74
|
+
def get(self, endpoint: str, **kw: Any) -> Any:
|
|
75
|
+
return self.request("GET", endpoint, **kw)
|
|
76
|
+
|
|
77
|
+
def post(self, endpoint: str, **kw: Any) -> Any:
|
|
78
|
+
return self.request("POST", endpoint, **kw)
|
|
79
|
+
|
|
80
|
+
def put(self, endpoint: str, **kw: Any) -> Any:
|
|
81
|
+
return self.request("PUT", endpoint, **kw)
|
|
82
|
+
|
|
83
|
+
def delete(self, endpoint: str, **kw: Any) -> Any:
|
|
84
|
+
return self.request("DELETE", endpoint, **kw)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def build_client(settings: LoxoSettings, *, verbose: bool = False) -> LoxoClient:
|
|
88
|
+
return LoxoClient(settings, verbose=verbose)
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from typer.core import TyperGroup
|
|
7
|
+
|
|
8
|
+
from loxo_cli.errors import ConfigError, LoxoError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LoxoGroup(TyperGroup):
|
|
12
|
+
"""Root command group that maps loxo's domain errors to documented exit codes.
|
|
13
|
+
|
|
14
|
+
Typer's invocation path does NOT honor a raised ``ClickException``'s
|
|
15
|
+
``exit_code`` (it surfaces as a generic exit 1 with no message). Set this as
|
|
16
|
+
the root app's group class via the supported ``typer.Typer(cls=LoxoGroup)``
|
|
17
|
+
hook: its ``invoke`` wraps the entire command tree, so every command — nested
|
|
18
|
+
sub-app commands and root-level commands alike — gets its ``LoxoError`` /
|
|
19
|
+
``ConfigError`` converted into ``typer.Exit`` with the mapped code, with a
|
|
20
|
+
clean message on stderr. Command files stay plain ``typer.Typer``.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def invoke(self, ctx) -> Any: # ctx is typer's vendored-click Context
|
|
24
|
+
try:
|
|
25
|
+
return super().invoke(ctx)
|
|
26
|
+
except (LoxoError, ConfigError) as exc:
|
|
27
|
+
typer.echo(f"Error: {exc.format_message()}", err=True)
|
|
28
|
+
raise typer.Exit(code=exc.exit_code) from exc
|