vancam 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vancam-0.1.0.dist-info/METADATA +141 -0
- vancam-0.1.0.dist-info/RECORD +9 -0
- vancam-0.1.0.dist-info/WHEEL +5 -0
- vancam-0.1.0.dist-info/entry_points.txt +2 -0
- vancam-0.1.0.dist-info/licenses/LICENSE +21 -0
- vancam-0.1.0.dist-info/top_level.txt +1 -0
- vancam_cli/__init__.py +3 -0
- vancam_cli/cli.py +157 -0
- vancam_cli/client.py +147 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vancam
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for the Vancam camera spatial search API
|
|
5
|
+
Author: DD
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 DD
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://vancam.ai
|
|
29
|
+
Project-URL: Repository, https://github.com/shughestr/vancam-cli
|
|
30
|
+
Classifier: Environment :: Console
|
|
31
|
+
Classifier: Intended Audience :: Developers
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Operating System :: OS Independent
|
|
34
|
+
Classifier: Programming Language :: Python :: 3
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
39
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
40
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
41
|
+
Requires-Python: >=3.9
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
License-File: LICENSE
|
|
44
|
+
Requires-Dist: requests>=2.31.0
|
|
45
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
46
|
+
Provides-Extra: test
|
|
47
|
+
Requires-Dist: pytest>=8.0.0; extra == "test"
|
|
48
|
+
Requires-Dist: responses>=0.25.0; extra == "test"
|
|
49
|
+
Dynamic: license-file
|
|
50
|
+
|
|
51
|
+
# Vancam CLI
|
|
52
|
+
|
|
53
|
+
**Real-world observation for AI agents.**
|
|
54
|
+
|
|
55
|
+
Command-line client for the Vancam camera spatial search API
|
|
56
|
+
(`api.vancam.ai`), giving developers and AI agents instant access to
|
|
57
|
+
**1M+ live public cameras across 100+ countries** — the same backend used
|
|
58
|
+
by the [MCP server](https://github.com/shughestr/mcp-vancam) and the
|
|
59
|
+
[vancam.ai](https://vancam.ai) web app.
|
|
60
|
+
|
|
61
|
+
Instead of relying on outdated web pages or user reports, Vancam provides
|
|
62
|
+
fresh visual evidence from the physical world, letting AI systems answer
|
|
63
|
+
location-based questions with confidence.
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
vancam REST API
|
|
67
|
+
├── vancam-mcp (MCP server)
|
|
68
|
+
├── vancam-cli (this project)
|
|
69
|
+
├── SDKs (future)
|
|
70
|
+
└── vancam.ai (web app)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Install
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install vancam
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Setup
|
|
80
|
+
|
|
81
|
+
No setup required to try it — defaults to a shared, public key if
|
|
82
|
+
`VANCAM_API_KEY` isn't set. That's **one key used by every anonymous
|
|
83
|
+
caller**, so its 1 req/s / 500 requests/month limit is a collective pool
|
|
84
|
+
shared across all of them, not 500 each. For a limit that's yours alone
|
|
85
|
+
(5 req/s, 500 requests/month, not shared), get a free personal key from your
|
|
86
|
+
account dashboard at [vancam.ai/account](https://vancam.ai/account):
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
cp .env.example .env
|
|
90
|
+
# edit .env, or export VANCAM_API_KEY directly
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Usage
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# N nearest cameras to a point
|
|
97
|
+
vancam nearest --lat 49.2827 --lon -123.1207 --limit 5
|
|
98
|
+
|
|
99
|
+
# Cameras within a radius (km)
|
|
100
|
+
vancam radius --lat 49.2827 --lon -123.1207 --radius 1
|
|
101
|
+
|
|
102
|
+
# Cameras along a route
|
|
103
|
+
vancam route --origin-lat 49.2827 --origin-lon -123.1207 \
|
|
104
|
+
--dest-lat 49.2965 --dest-lon -123.0884 --buffer 100
|
|
105
|
+
|
|
106
|
+
# Cameras in a bounding box
|
|
107
|
+
vancam bounds --min-lat 49.2 --min-lon -123.2 --max-lat 49.3 --max-lon -123.0
|
|
108
|
+
|
|
109
|
+
# Single camera by ID
|
|
110
|
+
vancam get 30145
|
|
111
|
+
|
|
112
|
+
# Live image
|
|
113
|
+
vancam image 30145 # prints the image URL
|
|
114
|
+
vancam image 30145 -o camera.jpg # downloads it
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
All commands print JSON to stdout (`--compact` for single-line output),
|
|
118
|
+
so this composes with `jq`:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
vancam nearest --lat 49.2827 --lon -123.1207 --limit 1 | jq '.cameras[0].image_url'
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Options
|
|
125
|
+
|
|
126
|
+
- `--api-key` — override `VANCAM_API_KEY`
|
|
127
|
+
- `--compact` — compact JSON instead of pretty-printed
|
|
128
|
+
|
|
129
|
+
## Use Cases
|
|
130
|
+
|
|
131
|
+
- Verify road and traffic conditions
|
|
132
|
+
- Monitor infrastructure and public spaces
|
|
133
|
+
- Check border crossings and transportation hubs
|
|
134
|
+
- Build AI agents with real-world awareness
|
|
135
|
+
- Power MCP servers, automation, and LLM applications
|
|
136
|
+
|
|
137
|
+
## Learn More
|
|
138
|
+
|
|
139
|
+
- **Website:** https://vancam.ai
|
|
140
|
+
- **API:** https://vancam.ai/api
|
|
141
|
+
- **MCP Server:** https://github.com/shughestr/mcp-vancam
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
vancam-0.1.0.dist-info/licenses/LICENSE,sha256=xw_AVv_gOYHukLSKhzf5gg48NBvNXr11CTSO0KvWhts,1059
|
|
2
|
+
vancam_cli/__init__.py,sha256=9wh8wr5RSdg6u4Gy2v6VNFGo_31PXErJTe60x9DbbVQ,89
|
|
3
|
+
vancam_cli/cli.py,sha256=hHAwevH9EiPN6rLaplBJPlgVP1kBUJoQyBJAmPQdz20,6057
|
|
4
|
+
vancam_cli/client.py,sha256=QbFIvoPoFFWHDV8tPSyKHRmVLUL59Uxkt5qCHRnDtEw,4925
|
|
5
|
+
vancam-0.1.0.dist-info/METADATA,sha256=22ioo4lAdrYH1rciBp_401gZ2mnmYwiZ23PgPRpdTrQ,4919
|
|
6
|
+
vancam-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
7
|
+
vancam-0.1.0.dist-info/entry_points.txt,sha256=pog7gu__k-KHZjiQdi3f-lPyKs1AV6HbgkLiMClYnAw,47
|
|
8
|
+
vancam-0.1.0.dist-info/top_level.txt,sha256=_og1te7KFo_AfOmYQIt2Kz6qe_t2oUNWEjPxPMXCigc,11
|
|
9
|
+
vancam-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DD
|
|
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 @@
|
|
|
1
|
+
vancam_cli
|
vancam_cli/__init__.py
ADDED
vancam_cli/cli.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""vancam — CLI for the Vancam camera spatial search API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
from dotenv import find_dotenv, load_dotenv
|
|
11
|
+
|
|
12
|
+
from .client import VancamAPIError, VancamClient, DEFAULT_BASE_URL
|
|
13
|
+
|
|
14
|
+
# usecwd=True: search from the user's current directory, not the installed
|
|
15
|
+
# package's location (python-dotenv's default walks up from __file__, which
|
|
16
|
+
# would silently pick up this repo's own .env regardless of where the CLI
|
|
17
|
+
# is actually run from).
|
|
18
|
+
load_dotenv(find_dotenv(usecwd=True))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _print_json(data, compact: bool) -> None:
|
|
22
|
+
if compact:
|
|
23
|
+
print(json.dumps(data, separators=(",", ":")))
|
|
24
|
+
else:
|
|
25
|
+
print(json.dumps(data, indent=2))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _client_from_args(args: argparse.Namespace) -> VancamClient:
|
|
29
|
+
return VancamClient(api_key=args.api_key, base_url=args.base_url)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def cmd_radius(args: argparse.Namespace) -> Any:
|
|
33
|
+
return _client_from_args(args).radius(args.lat, args.lon, args.radius, args.limit)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def cmd_nearest(args: argparse.Namespace):
|
|
37
|
+
return _client_from_args(args).nearest(args.lat, args.lon, args.limit)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def cmd_route(args: argparse.Namespace):
|
|
41
|
+
return _client_from_args(args).route(
|
|
42
|
+
args.origin_lat, args.origin_lon, args.dest_lat, args.dest_lon, args.buffer, args.limit
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def cmd_bounds(args: argparse.Namespace):
|
|
47
|
+
return _client_from_args(args).bounds(
|
|
48
|
+
args.min_lat, args.min_lon, args.max_lat, args.max_lon, args.limit
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def cmd_get(args: argparse.Namespace):
|
|
53
|
+
return _client_from_args(args).get_camera(args.asset_id)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def cmd_image(args: argparse.Namespace):
|
|
57
|
+
client = _client_from_args(args)
|
|
58
|
+
if args.output:
|
|
59
|
+
data = client.fetch_image(args.asset_id)
|
|
60
|
+
with open(args.output, "wb") as f:
|
|
61
|
+
f.write(data)
|
|
62
|
+
print(f"Saved to {args.output}", file=sys.stderr)
|
|
63
|
+
return None
|
|
64
|
+
return {"image_url": client.image_url(args.asset_id)}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
68
|
+
# Shared as a `parents=[...]` mixin so these flags work both before AND
|
|
69
|
+
# after the subcommand (argparse doesn't inherit parent-only args to
|
|
70
|
+
# subparsers otherwise).
|
|
71
|
+
#
|
|
72
|
+
# Two copies are needed: the top-level parser needs real defaults so
|
|
73
|
+
# `args.compact` etc. always exist, but if the *subparser* copy also
|
|
74
|
+
# carried those defaults, it would silently reset any value already set
|
|
75
|
+
# by the top-level parser back to the default when the flag isn't
|
|
76
|
+
# repeated after the subcommand (argparse parses both parsers into the
|
|
77
|
+
# same Namespace). default=SUPPRESS on the subparser copy makes it only
|
|
78
|
+
# write when the flag is actually given after the subcommand.
|
|
79
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
80
|
+
common.add_argument("--api-key", help="API key (default: VANCAM_API_KEY env var)")
|
|
81
|
+
common.add_argument("--base-url", default=DEFAULT_BASE_URL, help=argparse.SUPPRESS)
|
|
82
|
+
common.add_argument("--compact", action="store_true", help="Compact JSON output")
|
|
83
|
+
|
|
84
|
+
common_sub = argparse.ArgumentParser(add_help=False)
|
|
85
|
+
common_sub.add_argument("--api-key", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
|
|
86
|
+
common_sub.add_argument("--base-url", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
|
|
87
|
+
common_sub.add_argument(
|
|
88
|
+
"--compact", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
parser = argparse.ArgumentParser(
|
|
92
|
+
prog="vancam",
|
|
93
|
+
description="Query the Vancam camera spatial search API (api.vancam.ai).",
|
|
94
|
+
parents=[common],
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
98
|
+
|
|
99
|
+
p = sub.add_parser("radius", help="Cameras within a radius (km) of a point", parents=[common_sub])
|
|
100
|
+
p.add_argument("--lat", type=float, required=True)
|
|
101
|
+
p.add_argument("--lon", type=float, required=True)
|
|
102
|
+
p.add_argument("--radius", type=float, default=1.0, help="km, max 50 (default: 1.0)")
|
|
103
|
+
p.add_argument("--limit", type=int, default=50)
|
|
104
|
+
p.set_defaults(func=cmd_radius)
|
|
105
|
+
|
|
106
|
+
p = sub.add_parser("nearest", help="N nearest cameras to a point", parents=[common_sub])
|
|
107
|
+
p.add_argument("--lat", type=float, required=True)
|
|
108
|
+
p.add_argument("--lon", type=float, required=True)
|
|
109
|
+
p.add_argument("--limit", type=int, default=5)
|
|
110
|
+
p.set_defaults(func=cmd_nearest)
|
|
111
|
+
|
|
112
|
+
p = sub.add_parser("route", help="Cameras along a straight-line route", parents=[common_sub])
|
|
113
|
+
p.add_argument("--origin-lat", type=float, required=True)
|
|
114
|
+
p.add_argument("--origin-lon", type=float, required=True)
|
|
115
|
+
p.add_argument("--dest-lat", type=float, required=True)
|
|
116
|
+
p.add_argument("--dest-lon", type=float, required=True)
|
|
117
|
+
p.add_argument("--buffer", type=float, default=100.0, help="corridor width in meters")
|
|
118
|
+
p.add_argument("--limit", type=int, default=50)
|
|
119
|
+
p.set_defaults(func=cmd_route)
|
|
120
|
+
|
|
121
|
+
p = sub.add_parser("bounds", help="Cameras inside a bounding box", parents=[common_sub])
|
|
122
|
+
p.add_argument("--min-lat", type=float, required=True)
|
|
123
|
+
p.add_argument("--min-lon", type=float, required=True)
|
|
124
|
+
p.add_argument("--max-lat", type=float, required=True)
|
|
125
|
+
p.add_argument("--max-lon", type=float, required=True)
|
|
126
|
+
p.add_argument("--limit", type=int, default=100)
|
|
127
|
+
p.set_defaults(func=cmd_bounds)
|
|
128
|
+
|
|
129
|
+
p = sub.add_parser("get", help="Camera details by asset ID", parents=[common_sub])
|
|
130
|
+
p.add_argument("asset_id")
|
|
131
|
+
p.set_defaults(func=cmd_get)
|
|
132
|
+
|
|
133
|
+
p = sub.add_parser("image", help="Get (or save) a camera's live image", parents=[common_sub])
|
|
134
|
+
p.add_argument("asset_id")
|
|
135
|
+
p.add_argument("-o", "--output", help="Save image to this file instead of printing the URL")
|
|
136
|
+
p.set_defaults(func=cmd_image)
|
|
137
|
+
|
|
138
|
+
return parser
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def main(argv: Optional[list] = None) -> int:
|
|
142
|
+
parser = build_parser()
|
|
143
|
+
args = parser.parse_args(argv)
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
result = args.func(args)
|
|
147
|
+
except VancamAPIError as e:
|
|
148
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
149
|
+
return 1
|
|
150
|
+
|
|
151
|
+
if result is not None:
|
|
152
|
+
_print_json(result, args.compact)
|
|
153
|
+
return 0
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
if __name__ == "__main__":
|
|
157
|
+
sys.exit(main())
|
vancam_cli/client.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Thin HTTP client for the Vancam REST API (api.vancam.ai)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
DEFAULT_BASE_URL = "https://api.vancam.ai"
|
|
11
|
+
DEFAULT_TIMEOUT_SECONDS = 15
|
|
12
|
+
MAX_LIMIT = 100
|
|
13
|
+
|
|
14
|
+
# Shared, low-limit key (1 req/s, 500/month) — meant to be public, used when
|
|
15
|
+
# the caller hasn't set a personal key. Get a personal free-tier key (higher
|
|
16
|
+
# limits, 5 req/s, 500/month) from the web app's /account page.
|
|
17
|
+
ANONYMOUS_SHARED_API_KEY = "abwSAo91eI8wENsNrZFUE7tC6qGkh9Tw66x4cfcQ"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class VancamAPIError(RuntimeError):
|
|
21
|
+
def __init__(self, message: str, status_code: Optional[int] = None):
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
self.status_code = status_code
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class VancamClient:
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
api_key: Optional[str] = None,
|
|
30
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
31
|
+
timeout: int = DEFAULT_TIMEOUT_SECONDS,
|
|
32
|
+
):
|
|
33
|
+
self.api_key = api_key or os.getenv("VANCAM_API_KEY", ANONYMOUS_SHARED_API_KEY)
|
|
34
|
+
self.base_url = base_url.rstrip("/")
|
|
35
|
+
self.timeout = timeout
|
|
36
|
+
|
|
37
|
+
def _headers(self) -> Dict[str, str]:
|
|
38
|
+
return {"x-api-key": self.api_key} if self.api_key else {}
|
|
39
|
+
|
|
40
|
+
def _get(self, path: str, params: Dict[str, Any]) -> Any:
|
|
41
|
+
if not self.api_key:
|
|
42
|
+
raise VancamAPIError(
|
|
43
|
+
"No API key set. Set VANCAM_API_KEY in your environment or pass --api-key.\n"
|
|
44
|
+
"Get a free key from your account dashboard at https://vancam.ai/account."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
clean = {k: v for k, v in params.items() if v is not None}
|
|
48
|
+
if "limit" in clean:
|
|
49
|
+
clean["limit"] = max(1, min(int(clean["limit"]), MAX_LIMIT))
|
|
50
|
+
|
|
51
|
+
url = f"{self.base_url}{path}"
|
|
52
|
+
try:
|
|
53
|
+
response = requests.get(
|
|
54
|
+
url, params=clean, headers=self._headers(), timeout=self.timeout
|
|
55
|
+
)
|
|
56
|
+
except requests.exceptions.RequestException as e:
|
|
57
|
+
raise VancamAPIError(f"Request failed: {e}") from e
|
|
58
|
+
|
|
59
|
+
if response.status_code == 403:
|
|
60
|
+
raise VancamAPIError(
|
|
61
|
+
"403 Forbidden — missing or invalid API key.", status_code=403
|
|
62
|
+
)
|
|
63
|
+
if response.status_code == 429:
|
|
64
|
+
raise VancamAPIError(
|
|
65
|
+
"429 Too Many Requests — rate limit exceeded for your plan.",
|
|
66
|
+
status_code=429,
|
|
67
|
+
)
|
|
68
|
+
try:
|
|
69
|
+
response.raise_for_status()
|
|
70
|
+
except requests.exceptions.HTTPError as e:
|
|
71
|
+
raise VancamAPIError(
|
|
72
|
+
f"HTTP error {response.status_code}: {response.text}",
|
|
73
|
+
status_code=response.status_code,
|
|
74
|
+
) from e
|
|
75
|
+
|
|
76
|
+
return response.json()
|
|
77
|
+
|
|
78
|
+
def radius(self, lat: float, lon: float, radius: float, limit: Optional[int] = None) -> Any:
|
|
79
|
+
return self._get(
|
|
80
|
+
"/cameras/cameras",
|
|
81
|
+
{"lat": lat, "lon": lon, "radius": radius, "limit": limit},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def nearest(self, lat: float, lon: float, limit: Optional[int] = None) -> Any:
|
|
85
|
+
return self._get(
|
|
86
|
+
"/cameras/cameras",
|
|
87
|
+
{"lat": lat, "lon": lon, "nearest": "true", "limit": limit},
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def route(
|
|
91
|
+
self,
|
|
92
|
+
origin_lat: float,
|
|
93
|
+
origin_lon: float,
|
|
94
|
+
dest_lat: float,
|
|
95
|
+
dest_lon: float,
|
|
96
|
+
buffer: Optional[float] = None,
|
|
97
|
+
limit: Optional[int] = None,
|
|
98
|
+
) -> Any:
|
|
99
|
+
return self._get(
|
|
100
|
+
"/cameras/cameras",
|
|
101
|
+
{
|
|
102
|
+
"origin_lat": origin_lat,
|
|
103
|
+
"origin_lon": origin_lon,
|
|
104
|
+
"dest_lat": dest_lat,
|
|
105
|
+
"dest_lon": dest_lon,
|
|
106
|
+
"buffer": buffer,
|
|
107
|
+
"limit": limit,
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def bounds(
|
|
112
|
+
self,
|
|
113
|
+
min_lat: float,
|
|
114
|
+
min_lon: float,
|
|
115
|
+
max_lat: float,
|
|
116
|
+
max_lon: float,
|
|
117
|
+
limit: Optional[int] = None,
|
|
118
|
+
) -> Any:
|
|
119
|
+
return self._get(
|
|
120
|
+
"/cameras/cameras",
|
|
121
|
+
{
|
|
122
|
+
"min_lat": min_lat,
|
|
123
|
+
"min_lon": min_lon,
|
|
124
|
+
"max_lat": max_lat,
|
|
125
|
+
"max_lon": max_lon,
|
|
126
|
+
"limit": limit,
|
|
127
|
+
},
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def get_camera(self, asset_id: str) -> Any:
|
|
131
|
+
return self._get("/cameras/cameras", {"asset_id": asset_id})
|
|
132
|
+
|
|
133
|
+
def image_url(self, asset_id: str) -> str:
|
|
134
|
+
return f"{self.base_url}/api?asset_id={asset_id}"
|
|
135
|
+
|
|
136
|
+
def fetch_image(self, asset_id: str) -> bytes:
|
|
137
|
+
if not self.api_key:
|
|
138
|
+
raise VancamAPIError(
|
|
139
|
+
"No API key set. Set VANCAM_API_KEY in your environment or pass --api-key."
|
|
140
|
+
)
|
|
141
|
+
url = self.image_url(asset_id)
|
|
142
|
+
try:
|
|
143
|
+
response = requests.get(url, headers=self._headers(), timeout=self.timeout)
|
|
144
|
+
response.raise_for_status()
|
|
145
|
+
except requests.exceptions.RequestException as e:
|
|
146
|
+
raise VancamAPIError(f"Image fetch failed: {e}") from e
|
|
147
|
+
return response.content
|