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.
@@ -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,13 @@
1
+ .PHONY: pypi clean install dev
2
+
3
+ pypi: clean
4
+ pip install build twine -q && python -m build && twine upload dist/*
5
+
6
+ clean:
7
+ rm -rf dist build *.egg-info
8
+
9
+ install:
10
+ pip install -e .
11
+
12
+ dev:
13
+ pip install -e ".[dev]"
@@ -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: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
19
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
20
+ [![PyPI](https://img.shields.io/pypi/v/alphaloops-freight-cli)](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: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
5
+ [![PyPI](https://img.shields.io/pypi/v/alphaloops-freight-cli)](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"]