alphaloops-freight-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.
- alphaloops_freight_cli-0.1.0/.github/workflows/publish.yml +45 -0
- alphaloops_freight_cli-0.1.0/.gitignore +207 -0
- alphaloops_freight_cli-0.1.0/LICENSE +21 -0
- alphaloops_freight_cli-0.1.0/Makefile +13 -0
- alphaloops_freight_cli-0.1.0/PKG-INFO +161 -0
- alphaloops_freight_cli-0.1.0/README.md +146 -0
- alphaloops_freight_cli-0.1.0/alphaloops/cli/__init__.py +59 -0
- alphaloops_freight_cli-0.1.0/alphaloops/cli/carriers_cmd.py +131 -0
- alphaloops_freight_cli-0.1.0/alphaloops/cli/contacts_cmd.py +65 -0
- alphaloops_freight_cli-0.1.0/alphaloops/cli/crashes_cmd.py +38 -0
- alphaloops_freight_cli-0.1.0/alphaloops/cli/fleet_cmd.py +52 -0
- alphaloops_freight_cli-0.1.0/alphaloops/cli/inspections_cmd.py +52 -0
- alphaloops_freight_cli-0.1.0/alphaloops/cli/output.py +40 -0
- alphaloops_freight_cli-0.1.0/alphaloops/cli/state.py +20 -0
- alphaloops_freight_cli-0.1.0/pyproject.toml +26 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
name: Build distribution
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
|
|
14
|
+
- uses: actions/setup-python@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.12"
|
|
17
|
+
|
|
18
|
+
- name: Install build tools
|
|
19
|
+
run: pip install build
|
|
20
|
+
|
|
21
|
+
- name: Build package
|
|
22
|
+
run: python -m build
|
|
23
|
+
|
|
24
|
+
- name: Upload dist artifacts
|
|
25
|
+
uses: actions/upload-artifact@v4
|
|
26
|
+
with:
|
|
27
|
+
name: dist
|
|
28
|
+
path: dist/
|
|
29
|
+
|
|
30
|
+
publish:
|
|
31
|
+
name: Publish to PyPI
|
|
32
|
+
needs: build
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
environment: pypi
|
|
35
|
+
permissions:
|
|
36
|
+
id-token: write
|
|
37
|
+
steps:
|
|
38
|
+
- name: Download dist artifacts
|
|
39
|
+
uses: actions/download-artifact@v4
|
|
40
|
+
with:
|
|
41
|
+
name: dist
|
|
42
|
+
path: dist/
|
|
43
|
+
|
|
44
|
+
- name: Publish to PyPI
|
|
45
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
#uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
#poetry.lock
|
|
109
|
+
#poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
#pdm.lock
|
|
116
|
+
#pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
#pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# SageMath parsed files
|
|
135
|
+
*.sage.py
|
|
136
|
+
|
|
137
|
+
# Environments
|
|
138
|
+
.env
|
|
139
|
+
.envrc
|
|
140
|
+
.venv
|
|
141
|
+
env/
|
|
142
|
+
venv/
|
|
143
|
+
ENV/
|
|
144
|
+
env.bak/
|
|
145
|
+
venv.bak/
|
|
146
|
+
|
|
147
|
+
# Spyder project settings
|
|
148
|
+
.spyderproject
|
|
149
|
+
.spyproject
|
|
150
|
+
|
|
151
|
+
# Rope project settings
|
|
152
|
+
.ropeproject
|
|
153
|
+
|
|
154
|
+
# mkdocs documentation
|
|
155
|
+
/site
|
|
156
|
+
|
|
157
|
+
# mypy
|
|
158
|
+
.mypy_cache/
|
|
159
|
+
.dmypy.json
|
|
160
|
+
dmypy.json
|
|
161
|
+
|
|
162
|
+
# Pyre type checker
|
|
163
|
+
.pyre/
|
|
164
|
+
|
|
165
|
+
# pytype static type analyzer
|
|
166
|
+
.pytype/
|
|
167
|
+
|
|
168
|
+
# Cython debug symbols
|
|
169
|
+
cython_debug/
|
|
170
|
+
|
|
171
|
+
# PyCharm
|
|
172
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
173
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
174
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
175
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
176
|
+
#.idea/
|
|
177
|
+
|
|
178
|
+
# Abstra
|
|
179
|
+
# Abstra is an AI-powered process automation framework.
|
|
180
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
181
|
+
# Learn more at https://abstra.io/docs
|
|
182
|
+
.abstra/
|
|
183
|
+
|
|
184
|
+
# Visual Studio Code
|
|
185
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
186
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
188
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
189
|
+
# .vscode/
|
|
190
|
+
|
|
191
|
+
# Ruff stuff:
|
|
192
|
+
.ruff_cache/
|
|
193
|
+
|
|
194
|
+
# PyPI configuration file
|
|
195
|
+
.pypirc
|
|
196
|
+
|
|
197
|
+
# Cursor
|
|
198
|
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
|
199
|
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
|
200
|
+
# refer to https://docs.cursor.com/context/ignore-files
|
|
201
|
+
.cursorignore
|
|
202
|
+
.cursorindexingignore
|
|
203
|
+
|
|
204
|
+
# Marimo
|
|
205
|
+
marimo/_static/
|
|
206
|
+
marimo/_lsp/
|
|
207
|
+
__marimo__/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AlphaLoops, Inc.
|
|
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.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: alphaloops-freight-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AlphaLoops Freight CLI — command-line interface for FMCSA carrier data
|
|
5
|
+
Author: AlphaLoops, Inc.
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Requires-Dist: alphaloops-freight-sdk>=0.1.0
|
|
10
|
+
Requires-Dist: click>=8.0
|
|
11
|
+
Requires-Dist: rich>=13.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# AlphaLoops Freight CLI
|
|
17
|
+
|
|
18
|
+
[](LICENSE)
|
|
19
|
+
[](https://www.python.org/downloads/)
|
|
20
|
+
[](https://pypi.org/project/alphaloops-freight-cli/)
|
|
21
|
+
|
|
22
|
+
Command-line interface for the [AlphaLoops FMCSA API](https://runalphaloops.com/fmcsa-api/docs). Look up carriers, fleet data, inspections, crashes, and contacts from your terminal.
|
|
23
|
+
|
|
24
|
+
Built on the [AlphaLoops Freight SDK](https://github.com/RunAlphaLoop/freight-sdk).
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install alphaloops-freight-cli
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Authentication
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Option 1: Save your key
|
|
36
|
+
al login ak_your_key_here
|
|
37
|
+
|
|
38
|
+
# Option 2: Environment variable
|
|
39
|
+
export ALPHALOOPS_API_KEY=ak_your_key_here
|
|
40
|
+
|
|
41
|
+
# Option 3: Pass it directly
|
|
42
|
+
al --api-key ak_... carriers get 2247505
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Get your API key at [runalphaloops.com](https://runalphaloops.com/).
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
### Carrier Profiles
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Look up by DOT number
|
|
53
|
+
al carriers get 2247505
|
|
54
|
+
|
|
55
|
+
# Look up by MC number
|
|
56
|
+
al carriers mc 624748
|
|
57
|
+
|
|
58
|
+
# Field projection
|
|
59
|
+
al carriers get 2247505 --fields legal_name,total_trucks,total_drivers
|
|
60
|
+
|
|
61
|
+
# Fuzzy search
|
|
62
|
+
al carriers search "Swift Transportation"
|
|
63
|
+
al carriers search "JB Hunt" --state AR --limit 5
|
|
64
|
+
|
|
65
|
+
# Authority history
|
|
66
|
+
al carriers authority 2247505
|
|
67
|
+
|
|
68
|
+
# News
|
|
69
|
+
al carriers news 2247505 --start-date 2025-01-01
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Fleet Data
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
al fleet trucks 2247505
|
|
76
|
+
al fleet trucks 2247505 --limit 200
|
|
77
|
+
al fleet trailers 2247505
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Inspections
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
al inspections list 2247505
|
|
84
|
+
al inspections violations INS-12345
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Crashes
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
al crashes list 2247505
|
|
91
|
+
al crashes list 2247505 --severity FATAL --start-date 2024-01-01
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Contacts
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Search for people
|
|
98
|
+
al contacts search --dot 2247505
|
|
99
|
+
al contacts search --company "Swift" --levels c_suite,vp
|
|
100
|
+
|
|
101
|
+
# Enrich a contact (1 credit)
|
|
102
|
+
al contacts enrich contact_id_here
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## JSON Output
|
|
106
|
+
|
|
107
|
+
Every command supports `--json` for machine-readable output:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
al --json carriers get 2247505
|
|
111
|
+
al --json carriers search "Swift" | jq '.results[].legal_name'
|
|
112
|
+
al --json fleet trucks 2247505 | jq '.results | length'
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
This makes the CLI agent-friendly — pipe to `jq`, feed into scripts, or use from AI agents.
|
|
116
|
+
|
|
117
|
+
## Examples
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Find a carrier and get their fleet size
|
|
121
|
+
al carriers search "Werner Enterprises" --limit 1
|
|
122
|
+
al carriers get 2247505 --fields legal_name,total_trucks,total_drivers
|
|
123
|
+
|
|
124
|
+
# Get all fatal crashes for a carrier
|
|
125
|
+
al --json crashes list 2247505 --severity FATAL | jq '.results[]'
|
|
126
|
+
|
|
127
|
+
# Find C-suite contacts and enrich them
|
|
128
|
+
al --json contacts search --dot 2247505 --levels c_suite | jq '.results[].name'
|
|
129
|
+
al contacts enrich abc123
|
|
130
|
+
|
|
131
|
+
# Pipeline: search → get details → get fleet
|
|
132
|
+
DOT=$(al --json carriers search "Swift" | jq -r '.results[0].dot_number')
|
|
133
|
+
al carriers get "$DOT"
|
|
134
|
+
al fleet trucks "$DOT"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## All Commands
|
|
138
|
+
|
|
139
|
+
| Command | Description |
|
|
140
|
+
|---------|-------------|
|
|
141
|
+
| `al login <key>` | Save API key to `~/.alphaloops` |
|
|
142
|
+
| `al carriers get <dot>` | Carrier profile by DOT number |
|
|
143
|
+
| `al carriers mc <mc>` | Carrier profile by MC number |
|
|
144
|
+
| `al carriers search <name>` | Fuzzy search carriers |
|
|
145
|
+
| `al carriers authority <dot>` | Authority history |
|
|
146
|
+
| `al carriers news <dot>` | News and press mentions |
|
|
147
|
+
| `al fleet trucks <dot>` | Registered trucks |
|
|
148
|
+
| `al fleet trailers <dot>` | Registered trailers |
|
|
149
|
+
| `al inspections list <dot>` | Roadside inspections |
|
|
150
|
+
| `al inspections violations <id>` | Violations for an inspection |
|
|
151
|
+
| `al crashes list <dot>` | Crash history |
|
|
152
|
+
| `al contacts search` | Find contacts at a carrier |
|
|
153
|
+
| `al contacts enrich <id>` | Enrich a contact (email, phone) |
|
|
154
|
+
|
|
155
|
+
## API Documentation
|
|
156
|
+
|
|
157
|
+
Full API reference: [runalphaloops.com/fmcsa-api/docs](https://runalphaloops.com/fmcsa-api/docs)
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT — see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# AlphaLoops Freight CLI
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
[](https://www.python.org/downloads/)
|
|
5
|
+
[](https://pypi.org/project/alphaloops-freight-cli/)
|
|
6
|
+
|
|
7
|
+
Command-line interface for the [AlphaLoops FMCSA API](https://runalphaloops.com/fmcsa-api/docs). Look up carriers, fleet data, inspections, crashes, and contacts from your terminal.
|
|
8
|
+
|
|
9
|
+
Built on the [AlphaLoops Freight SDK](https://github.com/RunAlphaLoop/freight-sdk).
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install alphaloops-freight-cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Authentication
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Option 1: Save your key
|
|
21
|
+
al login ak_your_key_here
|
|
22
|
+
|
|
23
|
+
# Option 2: Environment variable
|
|
24
|
+
export ALPHALOOPS_API_KEY=ak_your_key_here
|
|
25
|
+
|
|
26
|
+
# Option 3: Pass it directly
|
|
27
|
+
al --api-key ak_... carriers get 2247505
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Get your API key at [runalphaloops.com](https://runalphaloops.com/).
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
### Carrier Profiles
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Look up by DOT number
|
|
38
|
+
al carriers get 2247505
|
|
39
|
+
|
|
40
|
+
# Look up by MC number
|
|
41
|
+
al carriers mc 624748
|
|
42
|
+
|
|
43
|
+
# Field projection
|
|
44
|
+
al carriers get 2247505 --fields legal_name,total_trucks,total_drivers
|
|
45
|
+
|
|
46
|
+
# Fuzzy search
|
|
47
|
+
al carriers search "Swift Transportation"
|
|
48
|
+
al carriers search "JB Hunt" --state AR --limit 5
|
|
49
|
+
|
|
50
|
+
# Authority history
|
|
51
|
+
al carriers authority 2247505
|
|
52
|
+
|
|
53
|
+
# News
|
|
54
|
+
al carriers news 2247505 --start-date 2025-01-01
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Fleet Data
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
al fleet trucks 2247505
|
|
61
|
+
al fleet trucks 2247505 --limit 200
|
|
62
|
+
al fleet trailers 2247505
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Inspections
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
al inspections list 2247505
|
|
69
|
+
al inspections violations INS-12345
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Crashes
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
al crashes list 2247505
|
|
76
|
+
al crashes list 2247505 --severity FATAL --start-date 2024-01-01
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Contacts
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Search for people
|
|
83
|
+
al contacts search --dot 2247505
|
|
84
|
+
al contacts search --company "Swift" --levels c_suite,vp
|
|
85
|
+
|
|
86
|
+
# Enrich a contact (1 credit)
|
|
87
|
+
al contacts enrich contact_id_here
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## JSON Output
|
|
91
|
+
|
|
92
|
+
Every command supports `--json` for machine-readable output:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
al --json carriers get 2247505
|
|
96
|
+
al --json carriers search "Swift" | jq '.results[].legal_name'
|
|
97
|
+
al --json fleet trucks 2247505 | jq '.results | length'
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
This makes the CLI agent-friendly — pipe to `jq`, feed into scripts, or use from AI agents.
|
|
101
|
+
|
|
102
|
+
## Examples
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Find a carrier and get their fleet size
|
|
106
|
+
al carriers search "Werner Enterprises" --limit 1
|
|
107
|
+
al carriers get 2247505 --fields legal_name,total_trucks,total_drivers
|
|
108
|
+
|
|
109
|
+
# Get all fatal crashes for a carrier
|
|
110
|
+
al --json crashes list 2247505 --severity FATAL | jq '.results[]'
|
|
111
|
+
|
|
112
|
+
# Find C-suite contacts and enrich them
|
|
113
|
+
al --json contacts search --dot 2247505 --levels c_suite | jq '.results[].name'
|
|
114
|
+
al contacts enrich abc123
|
|
115
|
+
|
|
116
|
+
# Pipeline: search → get details → get fleet
|
|
117
|
+
DOT=$(al --json carriers search "Swift" | jq -r '.results[0].dot_number')
|
|
118
|
+
al carriers get "$DOT"
|
|
119
|
+
al fleet trucks "$DOT"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## All Commands
|
|
123
|
+
|
|
124
|
+
| Command | Description |
|
|
125
|
+
|---------|-------------|
|
|
126
|
+
| `al login <key>` | Save API key to `~/.alphaloops` |
|
|
127
|
+
| `al carriers get <dot>` | Carrier profile by DOT number |
|
|
128
|
+
| `al carriers mc <mc>` | Carrier profile by MC number |
|
|
129
|
+
| `al carriers search <name>` | Fuzzy search carriers |
|
|
130
|
+
| `al carriers authority <dot>` | Authority history |
|
|
131
|
+
| `al carriers news <dot>` | News and press mentions |
|
|
132
|
+
| `al fleet trucks <dot>` | Registered trucks |
|
|
133
|
+
| `al fleet trailers <dot>` | Registered trailers |
|
|
134
|
+
| `al inspections list <dot>` | Roadside inspections |
|
|
135
|
+
| `al inspections violations <id>` | Violations for an inspection |
|
|
136
|
+
| `al crashes list <dot>` | Crash history |
|
|
137
|
+
| `al contacts search` | Find contacts at a carrier |
|
|
138
|
+
| `al contacts enrich <id>` | Enrich a contact (email, phone) |
|
|
139
|
+
|
|
140
|
+
## API Documentation
|
|
141
|
+
|
|
142
|
+
Full API reference: [runalphaloops.com/fmcsa-api/docs](https://runalphaloops.com/fmcsa-api/docs)
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT — see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""AlphaLoops Freight CLI — command-line interface for FMCSA carrier data."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from .state import ClientState, pass_state
|
|
11
|
+
from . import carriers_cmd, fleet_cmd, inspections_cmd, crashes_cmd, contacts_cmd
|
|
12
|
+
|
|
13
|
+
console = Console(stderr=True)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group(invoke_without_command=True)
|
|
17
|
+
@click.option("--json", "output_json", is_flag=True, help="Output raw JSON (for scripts and agents).")
|
|
18
|
+
@click.option("--api-key", envvar="ALPHALOOPS_API_KEY", default=None, help="API key (default: env/config file).")
|
|
19
|
+
@click.version_option(__version__, prog_name="al")
|
|
20
|
+
@click.pass_context
|
|
21
|
+
def main(ctx, output_json, api_key):
|
|
22
|
+
"""AlphaLoops Freight CLI — FMCSA carrier data at your fingertips."""
|
|
23
|
+
ctx.ensure_object(dict)
|
|
24
|
+
ctx.obj = ClientState(api_key=api_key, output_json=output_json)
|
|
25
|
+
|
|
26
|
+
if ctx.invoked_subcommand is None:
|
|
27
|
+
click.echo(ctx.get_help())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
main.add_command(carriers_cmd.carriers)
|
|
31
|
+
main.add_command(fleet_cmd.fleet)
|
|
32
|
+
main.add_command(inspections_cmd.inspections)
|
|
33
|
+
main.add_command(crashes_cmd.crashes)
|
|
34
|
+
main.add_command(contacts_cmd.contacts)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@main.command()
|
|
38
|
+
@click.argument("api_key")
|
|
39
|
+
def login(api_key):
|
|
40
|
+
"""Save your API key to ~/.alphaloops."""
|
|
41
|
+
import os
|
|
42
|
+
config_path = os.path.expanduser("~/.alphaloops")
|
|
43
|
+
with open(config_path, "w") as f:
|
|
44
|
+
f.write(f"api_key={api_key}\n")
|
|
45
|
+
console.print(f"[green]API key saved to {config_path}[/green]")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def cli():
|
|
49
|
+
"""Entry point that catches exceptions cleanly."""
|
|
50
|
+
try:
|
|
51
|
+
main(standalone_mode=False)
|
|
52
|
+
except click.ClickException as e:
|
|
53
|
+
console.print(f"[red]Error:[/red] {e.format_message()}")
|
|
54
|
+
sys.exit(e.exit_code)
|
|
55
|
+
except SystemExit:
|
|
56
|
+
raise
|
|
57
|
+
except Exception as e:
|
|
58
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
59
|
+
sys.exit(1)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Carrier profiles, search, authority, and news commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from .state import pass_state
|
|
6
|
+
from .output import print_json, print_kv, print_list_table
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group()
|
|
10
|
+
def carriers():
|
|
11
|
+
"""Carrier profiles, search, authority history, and news."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@carriers.command()
|
|
16
|
+
@click.argument("dot_number")
|
|
17
|
+
@click.option("--fields", default=None, help="Comma-separated field projection.")
|
|
18
|
+
@pass_state
|
|
19
|
+
def get(state, dot_number, fields):
|
|
20
|
+
"""Look up a carrier by DOT number."""
|
|
21
|
+
field_list = [f.strip() for f in fields.split(",")] if fields else None
|
|
22
|
+
result = state.client.carriers.get(dot_number, fields=field_list)
|
|
23
|
+
|
|
24
|
+
if state.output_json:
|
|
25
|
+
print_json(result)
|
|
26
|
+
else:
|
|
27
|
+
data = result.to_dict() if hasattr(result, "to_dict") else dict(result)
|
|
28
|
+
print_kv({
|
|
29
|
+
"Legal Name": data.get("legal_name"),
|
|
30
|
+
"DOT Number": data.get("dot_number"),
|
|
31
|
+
"MC Number": data.get("mc_number"),
|
|
32
|
+
"State": data.get("phy_state"),
|
|
33
|
+
"Total Trucks": data.get("total_trucks"),
|
|
34
|
+
"Total Drivers": data.get("total_drivers"),
|
|
35
|
+
"Safety Rating": data.get("safety_rating"),
|
|
36
|
+
"Operating Status": data.get("operating_status"),
|
|
37
|
+
}, title="Carrier Profile")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@carriers.command()
|
|
41
|
+
@click.argument("mc_number")
|
|
42
|
+
@click.option("--fields", default=None, help="Comma-separated field projection.")
|
|
43
|
+
@pass_state
|
|
44
|
+
def mc(state, mc_number, fields):
|
|
45
|
+
"""Look up a carrier by MC/MX docket number."""
|
|
46
|
+
field_list = [f.strip() for f in fields.split(",")] if fields else None
|
|
47
|
+
result = state.client.carriers.get_by_mc(mc_number, fields=field_list)
|
|
48
|
+
|
|
49
|
+
if state.output_json:
|
|
50
|
+
print_json(result)
|
|
51
|
+
else:
|
|
52
|
+
data = result.to_dict() if hasattr(result, "to_dict") else dict(result)
|
|
53
|
+
print_kv({
|
|
54
|
+
"Legal Name": data.get("legal_name"),
|
|
55
|
+
"DOT Number": data.get("dot_number"),
|
|
56
|
+
"MC Number": data.get("mc_number"),
|
|
57
|
+
"State": data.get("phy_state"),
|
|
58
|
+
"Total Trucks": data.get("total_trucks"),
|
|
59
|
+
"Total Drivers": data.get("total_drivers"),
|
|
60
|
+
}, title="Carrier Profile")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@carriers.command()
|
|
64
|
+
@click.argument("company_name")
|
|
65
|
+
@click.option("--domain", default=None, help="Company domain filter.")
|
|
66
|
+
@click.option("--state", "state_filter", default=None, help="State abbreviation filter.")
|
|
67
|
+
@click.option("--city", default=None, help="City filter.")
|
|
68
|
+
@click.option("--page", default=1, type=int, help="Page number.")
|
|
69
|
+
@click.option("--limit", default=10, type=int, help="Results per page.")
|
|
70
|
+
@pass_state
|
|
71
|
+
def search(state, company_name, domain, state_filter, city, page, limit):
|
|
72
|
+
"""Fuzzy search for carriers by company name."""
|
|
73
|
+
result = state.client.carriers.search(
|
|
74
|
+
company_name, domain=domain, state=state_filter, city=city,
|
|
75
|
+
page=page, limit=limit,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if state.output_json:
|
|
79
|
+
print_json(result)
|
|
80
|
+
else:
|
|
81
|
+
data = result.to_dict() if hasattr(result, "to_dict") else dict(result)
|
|
82
|
+
rows = data.get("results", [])
|
|
83
|
+
if not rows:
|
|
84
|
+
click.echo("No results found.")
|
|
85
|
+
return
|
|
86
|
+
print_list_table(rows, ["legal_name", "dot_number", "mc_number", "phy_state", "confidence"], title="Search Results")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@carriers.command()
|
|
90
|
+
@click.argument("dot_number")
|
|
91
|
+
@click.option("--limit", default=50, type=int, help="Results per page.")
|
|
92
|
+
@click.option("--offset", default=0, type=int, help="Offset.")
|
|
93
|
+
@pass_state
|
|
94
|
+
def authority(state, dot_number, limit, offset):
|
|
95
|
+
"""Authority history for a carrier."""
|
|
96
|
+
result = state.client.carriers.authority(dot_number, limit=limit, offset=offset)
|
|
97
|
+
|
|
98
|
+
if state.output_json:
|
|
99
|
+
print_json(result)
|
|
100
|
+
else:
|
|
101
|
+
data = result.to_dict() if hasattr(result, "to_dict") else dict(result)
|
|
102
|
+
rows = data.get("results", [])
|
|
103
|
+
if not rows:
|
|
104
|
+
click.echo("No authority records found.")
|
|
105
|
+
return
|
|
106
|
+
print_list_table(rows, ["authority_type", "status", "effective_date"], title="Authority History")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@carriers.command()
|
|
110
|
+
@click.argument("dot_number")
|
|
111
|
+
@click.option("--start-date", default=None, help="Start date (YYYY-MM-DD).")
|
|
112
|
+
@click.option("--end-date", default=None, help="End date (YYYY-MM-DD).")
|
|
113
|
+
@click.option("--page", default=1, type=int, help="Page number.")
|
|
114
|
+
@click.option("--limit", default=25, type=int, help="Results per page.")
|
|
115
|
+
@pass_state
|
|
116
|
+
def news(state, dot_number, start_date, end_date, page, limit):
|
|
117
|
+
"""News articles and press mentions for a carrier."""
|
|
118
|
+
result = state.client.carriers.news(
|
|
119
|
+
dot_number, start_date=start_date, end_date=end_date,
|
|
120
|
+
page=page, limit=limit,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if state.output_json:
|
|
124
|
+
print_json(result)
|
|
125
|
+
else:
|
|
126
|
+
data = result.to_dict() if hasattr(result, "to_dict") else dict(result)
|
|
127
|
+
rows = data.get("results", [])
|
|
128
|
+
if not rows:
|
|
129
|
+
click.echo("No news articles found.")
|
|
130
|
+
return
|
|
131
|
+
print_list_table(rows, ["title", "source", "published_date"], title="News")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Contact search and enrichment commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from .state import pass_state
|
|
6
|
+
from .output import print_json, print_kv, print_list_table
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group()
|
|
10
|
+
def contacts():
|
|
11
|
+
"""Contact search and enrichment."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@contacts.command()
|
|
16
|
+
@click.option("--dot", "dot_number", default=None, help="DOT number.")
|
|
17
|
+
@click.option("--company", "company_name", default=None, help="Company name.")
|
|
18
|
+
@click.option("--title", "job_title", default=None, help="Job title filter.")
|
|
19
|
+
@click.option("--levels", default=None, help="Comma-separated seniority levels (vp,director,manager,c_suite).")
|
|
20
|
+
@click.option("--page", default=1, type=int, help="Page number.")
|
|
21
|
+
@click.option("--limit", default=25, type=int, help="Results per page.")
|
|
22
|
+
@click.option("--no-retry", is_flag=True, help="Don't auto-retry on 202 (async fetch).")
|
|
23
|
+
@pass_state
|
|
24
|
+
def search(state, dot_number, company_name, job_title, levels, page, limit, no_retry):
|
|
25
|
+
"""Search for contacts at a carrier or company."""
|
|
26
|
+
if not dot_number and not company_name:
|
|
27
|
+
raise click.UsageError("Provide --dot or --company.")
|
|
28
|
+
|
|
29
|
+
level_list = [l.strip() for l in levels.split(",")] if levels else None
|
|
30
|
+
result = state.client.contacts.search(
|
|
31
|
+
dot_number=dot_number, company_name=company_name,
|
|
32
|
+
job_title=job_title, job_title_levels=level_list,
|
|
33
|
+
page=page, limit=limit, auto_retry=not no_retry,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if state.output_json:
|
|
37
|
+
print_json(result)
|
|
38
|
+
else:
|
|
39
|
+
data = result.to_dict() if hasattr(result, "to_dict") else dict(result)
|
|
40
|
+
rows = data.get("results", [])
|
|
41
|
+
if not rows:
|
|
42
|
+
click.echo("No contacts found.")
|
|
43
|
+
return
|
|
44
|
+
print_list_table(rows, ["name", "job_title", "seniority"], title="Contacts")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@contacts.command()
|
|
48
|
+
@click.argument("contact_id")
|
|
49
|
+
@pass_state
|
|
50
|
+
def enrich(state, contact_id):
|
|
51
|
+
"""Enrich a contact — get email, phone, work history (1 credit)."""
|
|
52
|
+
result = state.client.contacts.enrich(contact_id)
|
|
53
|
+
|
|
54
|
+
if state.output_json:
|
|
55
|
+
print_json(result)
|
|
56
|
+
else:
|
|
57
|
+
data = result.to_dict() if hasattr(result, "to_dict") else dict(result)
|
|
58
|
+
print_kv({
|
|
59
|
+
"Name": data.get("name"),
|
|
60
|
+
"Email": data.get("email"),
|
|
61
|
+
"Phone": data.get("phone"),
|
|
62
|
+
"Title": data.get("job_title"),
|
|
63
|
+
"Company": data.get("company_name"),
|
|
64
|
+
"LinkedIn": data.get("linkedin_url"),
|
|
65
|
+
}, title="Enriched Contact")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Crash history commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from .state import pass_state
|
|
6
|
+
from .output import print_json, print_list_table
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group()
|
|
10
|
+
def crashes():
|
|
11
|
+
"""Carrier crash history."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@crashes.command("list")
|
|
16
|
+
@click.argument("dot_number")
|
|
17
|
+
@click.option("--start-date", default=None, help="Start date (YYYY-MM-DD).")
|
|
18
|
+
@click.option("--end-date", default=None, help="End date (YYYY-MM-DD).")
|
|
19
|
+
@click.option("--severity", default=None, type=click.Choice(["FATAL", "INJURY", "TOW", "PROPERTY_DAMAGE"], case_sensitive=False), help="Severity filter.")
|
|
20
|
+
@click.option("--page", default=1, type=int, help="Page number.")
|
|
21
|
+
@click.option("--limit", default=25, type=int, help="Results per page.")
|
|
22
|
+
@pass_state
|
|
23
|
+
def list_crashes(state, dot_number, start_date, end_date, severity, page, limit):
|
|
24
|
+
"""List reported crashes for a carrier."""
|
|
25
|
+
result = state.client.crashes.list(
|
|
26
|
+
dot_number, start_date=start_date, end_date=end_date,
|
|
27
|
+
severity=severity, page=page, limit=limit,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if state.output_json:
|
|
31
|
+
print_json(result)
|
|
32
|
+
else:
|
|
33
|
+
data = result.to_dict() if hasattr(result, "to_dict") else dict(result)
|
|
34
|
+
rows = data.get("results", [])
|
|
35
|
+
if not rows:
|
|
36
|
+
click.echo("No crashes found.")
|
|
37
|
+
return
|
|
38
|
+
print_list_table(rows, ["date", "severity", "fatalities", "injuries", "state"], title="Crashes")
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Fleet data commands — trucks and trailers."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from .state import pass_state
|
|
6
|
+
from .output import print_json, print_list_table
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group()
|
|
10
|
+
def fleet():
|
|
11
|
+
"""Truck and trailer fleet data."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@fleet.command()
|
|
16
|
+
@click.argument("dot_number")
|
|
17
|
+
@click.option("--limit", default=50, type=int, help="Results per page.")
|
|
18
|
+
@click.option("--offset", default=0, type=int, help="Offset.")
|
|
19
|
+
@pass_state
|
|
20
|
+
def trucks(state, dot_number, limit, offset):
|
|
21
|
+
"""List registered trucks for a carrier."""
|
|
22
|
+
result = state.client.fleet.trucks(dot_number, limit=limit, offset=offset)
|
|
23
|
+
|
|
24
|
+
if state.output_json:
|
|
25
|
+
print_json(result)
|
|
26
|
+
else:
|
|
27
|
+
data = result.to_dict() if hasattr(result, "to_dict") else dict(result)
|
|
28
|
+
rows = data.get("results", [])
|
|
29
|
+
if not rows:
|
|
30
|
+
click.echo("No trucks found.")
|
|
31
|
+
return
|
|
32
|
+
print_list_table(rows, ["vin", "make", "model_year", "gvw"], title="Trucks")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@fleet.command()
|
|
36
|
+
@click.argument("dot_number")
|
|
37
|
+
@click.option("--limit", default=50, type=int, help="Results per page.")
|
|
38
|
+
@click.option("--offset", default=0, type=int, help="Offset.")
|
|
39
|
+
@pass_state
|
|
40
|
+
def trailers(state, dot_number, limit, offset):
|
|
41
|
+
"""List registered trailers for a carrier."""
|
|
42
|
+
result = state.client.fleet.trailers(dot_number, limit=limit, offset=offset)
|
|
43
|
+
|
|
44
|
+
if state.output_json:
|
|
45
|
+
print_json(result)
|
|
46
|
+
else:
|
|
47
|
+
data = result.to_dict() if hasattr(result, "to_dict") else dict(result)
|
|
48
|
+
rows = data.get("results", [])
|
|
49
|
+
if not rows:
|
|
50
|
+
click.echo("No trailers found.")
|
|
51
|
+
return
|
|
52
|
+
print_list_table(rows, ["vin", "manufacturer", "type", "reefer"], title="Trailers")
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Inspection and violation commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from .state import pass_state
|
|
6
|
+
from .output import print_json, print_list_table
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group()
|
|
10
|
+
def inspections():
|
|
11
|
+
"""Roadside inspections and violation details."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@inspections.command("list")
|
|
16
|
+
@click.argument("dot_number")
|
|
17
|
+
@click.option("--limit", default=50, type=int, help="Results per page.")
|
|
18
|
+
@click.option("--offset", default=0, type=int, help="Offset.")
|
|
19
|
+
@pass_state
|
|
20
|
+
def list_inspections(state, dot_number, limit, offset):
|
|
21
|
+
"""List roadside inspections for a carrier."""
|
|
22
|
+
result = state.client.inspections.list(dot_number, limit=limit, offset=offset)
|
|
23
|
+
|
|
24
|
+
if state.output_json:
|
|
25
|
+
print_json(result)
|
|
26
|
+
else:
|
|
27
|
+
data = result.to_dict() if hasattr(result, "to_dict") else dict(result)
|
|
28
|
+
rows = data.get("results", [])
|
|
29
|
+
if not rows:
|
|
30
|
+
click.echo("No inspections found.")
|
|
31
|
+
return
|
|
32
|
+
print_list_table(rows, ["inspection_id", "date", "state", "oos_total"], title="Inspections")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@inspections.command()
|
|
36
|
+
@click.argument("inspection_id")
|
|
37
|
+
@click.option("--page", default=1, type=int, help="Page number.")
|
|
38
|
+
@click.option("--limit", default=25, type=int, help="Results per page.")
|
|
39
|
+
@pass_state
|
|
40
|
+
def violations(state, inspection_id, page, limit):
|
|
41
|
+
"""List violations for a specific inspection."""
|
|
42
|
+
result = state.client.inspections.violations(inspection_id, page=page, limit=limit)
|
|
43
|
+
|
|
44
|
+
if state.output_json:
|
|
45
|
+
print_json(result)
|
|
46
|
+
else:
|
|
47
|
+
data = result.to_dict() if hasattr(result, "to_dict") else dict(result)
|
|
48
|
+
rows = data.get("results", [])
|
|
49
|
+
if not rows:
|
|
50
|
+
click.echo("No violations found.")
|
|
51
|
+
return
|
|
52
|
+
print_list_table(rows, ["code", "description", "basic_category", "oos"], title="Violations")
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Output helpers — JSON for machines, Rich tables for humans."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def print_json(data):
|
|
12
|
+
"""Print data as formatted JSON."""
|
|
13
|
+
if hasattr(data, "to_dict"):
|
|
14
|
+
data = data.to_dict()
|
|
15
|
+
console.print(json.dumps(data, indent=2, default=str))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def print_kv(pairs, title=None):
|
|
19
|
+
"""Print key-value pairs as a Rich table."""
|
|
20
|
+
table = Table(show_header=False, title=title, title_style="bold")
|
|
21
|
+
table.add_column("Key", style="bold cyan", no_wrap=True)
|
|
22
|
+
table.add_column("Value")
|
|
23
|
+
for k, v in pairs.items():
|
|
24
|
+
table.add_row(str(k), str(v) if v is not None else "—")
|
|
25
|
+
console.print(table)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def print_list_table(rows, columns, title=None):
|
|
29
|
+
"""Print a list of dicts as a Rich table."""
|
|
30
|
+
table = Table(title=title, title_style="bold")
|
|
31
|
+
for col in columns:
|
|
32
|
+
table.add_column(col, no_wrap=(col in ("DOT", "MC", "VIN", "ID")))
|
|
33
|
+
for row in rows:
|
|
34
|
+
vals = []
|
|
35
|
+
for col in columns:
|
|
36
|
+
key = col.lower().replace(" ", "_")
|
|
37
|
+
v = row.get(key) if isinstance(row, dict) else getattr(row, key, None)
|
|
38
|
+
vals.append(str(v) if v is not None else "—")
|
|
39
|
+
table.add_row(*vals)
|
|
40
|
+
console.print(table)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Client state — lazy SDK initialization and output mode."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ClientState:
|
|
7
|
+
def __init__(self, api_key=None, output_json=False):
|
|
8
|
+
self._api_key = api_key
|
|
9
|
+
self.output_json = output_json
|
|
10
|
+
self._client = None
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def client(self):
|
|
14
|
+
if self._client is None:
|
|
15
|
+
from alphaloops.freight import AlphaLoops
|
|
16
|
+
self._client = AlphaLoops(api_key=self._api_key)
|
|
17
|
+
return self._client
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
pass_state = click.make_pass_decorator(ClientState)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "alphaloops-freight-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "AlphaLoops Freight CLI — command-line interface for FMCSA carrier data"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "AlphaLoops, Inc." }]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"alphaloops-freight-sdk>=0.1.0",
|
|
15
|
+
"click>=8.0",
|
|
16
|
+
"rich>=13.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
al = "alphaloops.cli:cli"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["alphaloops"]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
dev = ["ruff"]
|