vicpropertycheck 0.1.1__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.
- vicpropertycheck-0.1.1/PKG-INFO +97 -0
- vicpropertycheck-0.1.1/README.md +57 -0
- vicpropertycheck-0.1.1/pyproject.toml +84 -0
- vicpropertycheck-0.1.1/setup.cfg +4 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/__init__.py +16 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/__main__.py +6 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/cli.py +412 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/config/__init__.py +39 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/config/schemas.py +387 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/config/settings.py +239 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/exceptions/__init__.py +343 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/middleware/__init__.py +5 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/middleware/auth.py +71 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/providers/__init__.py +23 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/providers/base.py +429 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/providers/google_geocoding.py +287 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/providers/google_places_facilities.py +293 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/providers/google_places_nearby.py +296 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/providers/hosted_proxy.py +283 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/providers/openstats_base.py +362 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/providers/openstats_crime.py +352 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/providers/openstats_housing.py +331 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/providers/vicmap_bushfire.py +501 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/providers/vicmap_planning.py +479 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/server.py +641 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/tools/__init__.py +30 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/tools/address.py +69 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/tools/bushfire.py +87 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/tools/crime.py +75 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/tools/housing.py +73 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/tools/planning.py +85 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/tools/summary.py +179 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/utils/__init__.py +91 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/utils/cache.py +516 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/utils/error_handling.py +11 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/utils/geo.py +422 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/utils/logging.py +439 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/utils/rate_limit.py +374 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/utils/response.py +53 -0
- vicpropertycheck-0.1.1/src/vic_property_mcp/utils/validators.py +111 -0
- vicpropertycheck-0.1.1/src/vicpropertycheck.egg-info/PKG-INFO +97 -0
- vicpropertycheck-0.1.1/src/vicpropertycheck.egg-info/SOURCES.txt +44 -0
- vicpropertycheck-0.1.1/src/vicpropertycheck.egg-info/dependency_links.txt +1 -0
- vicpropertycheck-0.1.1/src/vicpropertycheck.egg-info/entry_points.txt +3 -0
- vicpropertycheck-0.1.1/src/vicpropertycheck.egg-info/requires.txt +27 -0
- vicpropertycheck-0.1.1/src/vicpropertycheck.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vicpropertycheck
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Victorian property due diligence — MCP server, CLI, and skill backend (planning, bushfire, crime, housing, facilities)
|
|
5
|
+
Author-email: Roger Liu <zichengliu0226@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Requires-Python: >=3.13.2
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: mcp>=1.25.0
|
|
15
|
+
Requires-Dist: pydantic>=2.12.5
|
|
16
|
+
Requires-Dist: httpx>=0.28.1
|
|
17
|
+
Requires-Dist: shapely>=2.0.6
|
|
18
|
+
Requires-Dist: pyproj>=3.7.0
|
|
19
|
+
Requires-Dist: python-dotenv>=1.0.1
|
|
20
|
+
Requires-Dist: structlog>=24.4.0
|
|
21
|
+
Requires-Dist: aiosqlite>=0.20.0
|
|
22
|
+
Requires-Dist: tenacity>=9.0.0
|
|
23
|
+
Requires-Dist: typing-extensions>=4.12.2
|
|
24
|
+
Requires-Dist: email-validator>=2.2.0
|
|
25
|
+
Requires-Dist: uvicorn>=0.40.0
|
|
26
|
+
Requires-Dist: starlette>=0.50.0
|
|
27
|
+
Requires-Dist: sse-starlette>=2.1.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest<8.3,>=8.2.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-cov>=6.0.0; extra == "dev"
|
|
32
|
+
Requires-Dist: black>=24.10.0; extra == "dev"
|
|
33
|
+
Requires-Dist: isort>=5.13.2; extra == "dev"
|
|
34
|
+
Requires-Dist: flake8>=7.1.1; extra == "dev"
|
|
35
|
+
Requires-Dist: mypy>=1.13.0; extra == "dev"
|
|
36
|
+
Requires-Dist: pylint>=3.3.2; extra == "dev"
|
|
37
|
+
Requires-Dist: pylint-pydantic>=0.3.2; extra == "dev"
|
|
38
|
+
Requires-Dist: pylint-pytest>=1.1.8; extra == "dev"
|
|
39
|
+
Requires-Dist: pre-commit>=4.0.1; extra == "dev"
|
|
40
|
+
|
|
41
|
+
# vicpropertycheck
|
|
42
|
+
|
|
43
|
+
Victorian (Australia) property due diligence — three things in one Python package:
|
|
44
|
+
|
|
45
|
+
1. **MCP server** (`vic-property-mcp`) — exposes property tools to MCP-aware clients.
|
|
46
|
+
2. **CLI** (`vic-property`) — same tools as subcommands, for shell/script use.
|
|
47
|
+
3. **Skill backend** — drives the `vic-property-check` Claude Code skill (see `skill/SKILL.md`).
|
|
48
|
+
|
|
49
|
+
All three share the same providers, cache, and config:
|
|
50
|
+
|
|
51
|
+
- VicMap Planning ArcGIS (zones + overlays)
|
|
52
|
+
- VicMap Bushfire ArcGIS (BMO, prone areas)
|
|
53
|
+
- OpenStats (suburb crime + housing)
|
|
54
|
+
- Google Maps (geocoding + nearby places) — direct API key or hosted proxy with Firebase sign-in
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pipx install vicpropertycheck
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This puts `vic-property-mcp` (server) and `vic-property` (CLI) on your `PATH`.
|
|
63
|
+
|
|
64
|
+
## CLI quick start
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# VIC government APIs need no auth — these work out of the box:
|
|
68
|
+
vic-property planning --lat -37.8113 --lon 144.9737
|
|
69
|
+
vic-property bushfire --lat -37.8113 --lon 144.9737
|
|
70
|
+
vic-property crime --suburb "Melbourne"
|
|
71
|
+
|
|
72
|
+
# Google-backed commands need either:
|
|
73
|
+
# (a) export GOOGLE_MAPS_API_KEY=... (self-hosted)
|
|
74
|
+
# (b) vic-property login (paste blob from vicpropertycheck.com.au/skill-auth)
|
|
75
|
+
vic-property summarize --address "1 Spring St, Melbourne VIC 3000" --pretty
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Every subcommand emits one JSON object on stdout. Errors come back as
|
|
79
|
+
`{"error": "...", "code": "..."}` with a non-zero exit code.
|
|
80
|
+
|
|
81
|
+
## MCP server
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
vic-property-mcp # stdio transport (default)
|
|
85
|
+
VIC_PROPERTY_TRANSPORT__MODE=http vic-property-mcp # HTTP on :8080
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Configuration uses the `VIC_PROPERTY_` env-var prefix; see `.env.example`.
|
|
89
|
+
|
|
90
|
+
## Skill
|
|
91
|
+
|
|
92
|
+
Drop `skill/` into `~/.claude/skills/vic-property-check/`. See `skill/INSTALL.md` for the
|
|
93
|
+
full install + auth walkthrough.
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# vicpropertycheck
|
|
2
|
+
|
|
3
|
+
Victorian (Australia) property due diligence — three things in one Python package:
|
|
4
|
+
|
|
5
|
+
1. **MCP server** (`vic-property-mcp`) — exposes property tools to MCP-aware clients.
|
|
6
|
+
2. **CLI** (`vic-property`) — same tools as subcommands, for shell/script use.
|
|
7
|
+
3. **Skill backend** — drives the `vic-property-check` Claude Code skill (see `skill/SKILL.md`).
|
|
8
|
+
|
|
9
|
+
All three share the same providers, cache, and config:
|
|
10
|
+
|
|
11
|
+
- VicMap Planning ArcGIS (zones + overlays)
|
|
12
|
+
- VicMap Bushfire ArcGIS (BMO, prone areas)
|
|
13
|
+
- OpenStats (suburb crime + housing)
|
|
14
|
+
- Google Maps (geocoding + nearby places) — direct API key or hosted proxy with Firebase sign-in
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pipx install vicpropertycheck
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This puts `vic-property-mcp` (server) and `vic-property` (CLI) on your `PATH`.
|
|
23
|
+
|
|
24
|
+
## CLI quick start
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# VIC government APIs need no auth — these work out of the box:
|
|
28
|
+
vic-property planning --lat -37.8113 --lon 144.9737
|
|
29
|
+
vic-property bushfire --lat -37.8113 --lon 144.9737
|
|
30
|
+
vic-property crime --suburb "Melbourne"
|
|
31
|
+
|
|
32
|
+
# Google-backed commands need either:
|
|
33
|
+
# (a) export GOOGLE_MAPS_API_KEY=... (self-hosted)
|
|
34
|
+
# (b) vic-property login (paste blob from vicpropertycheck.com.au/skill-auth)
|
|
35
|
+
vic-property summarize --address "1 Spring St, Melbourne VIC 3000" --pretty
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Every subcommand emits one JSON object on stdout. Errors come back as
|
|
39
|
+
`{"error": "...", "code": "..."}` with a non-zero exit code.
|
|
40
|
+
|
|
41
|
+
## MCP server
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
vic-property-mcp # stdio transport (default)
|
|
45
|
+
VIC_PROPERTY_TRANSPORT__MODE=http vic-property-mcp # HTTP on :8080
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Configuration uses the `VIC_PROPERTY_` env-var prefix; see `.env.example`.
|
|
49
|
+
|
|
50
|
+
## Skill
|
|
51
|
+
|
|
52
|
+
Drop `skill/` into `~/.claude/skills/vic-property-check/`. See `skill/INSTALL.md` for the
|
|
53
|
+
full install + auth walkthrough.
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vicpropertycheck"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Victorian property due diligence — MCP server, CLI, and skill backend (planning, bushfire, crime, housing, facilities)"
|
|
9
|
+
authors = [{ name = "Roger Liu", email = "zichengliu0226@gmail.com" }]
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.13.2"
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
dependencies = [
|
|
22
|
+
"mcp>=1.25.0",
|
|
23
|
+
"pydantic>=2.12.5",
|
|
24
|
+
"httpx>=0.28.1",
|
|
25
|
+
"shapely>=2.0.6",
|
|
26
|
+
"pyproj>=3.7.0",
|
|
27
|
+
"python-dotenv>=1.0.1",
|
|
28
|
+
"structlog>=24.4.0",
|
|
29
|
+
"aiosqlite>=0.20.0",
|
|
30
|
+
"tenacity>=9.0.0",
|
|
31
|
+
"typing-extensions>=4.12.2",
|
|
32
|
+
"email-validator>=2.2.0",
|
|
33
|
+
"uvicorn>=0.40.0",
|
|
34
|
+
"starlette>=0.50.0",
|
|
35
|
+
"sse-starlette>=2.1.0",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
dev = [
|
|
40
|
+
"pytest>=8.2.0,<8.3",
|
|
41
|
+
"pytest-asyncio>=0.24.0",
|
|
42
|
+
"pytest-cov>=6.0.0",
|
|
43
|
+
"black>=24.10.0",
|
|
44
|
+
"isort>=5.13.2",
|
|
45
|
+
"flake8>=7.1.1",
|
|
46
|
+
"mypy>=1.13.0",
|
|
47
|
+
"pylint>=3.3.2",
|
|
48
|
+
"pylint-pydantic>=0.3.2",
|
|
49
|
+
"pylint-pytest>=1.1.8",
|
|
50
|
+
"pre-commit>=4.0.1",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[project.scripts]
|
|
54
|
+
vic-property-mcp = "vic_property_mcp.server:main"
|
|
55
|
+
vic-property = "vic_property_mcp.cli:main"
|
|
56
|
+
|
|
57
|
+
[tool.setuptools.packages.find]
|
|
58
|
+
where = ["src"]
|
|
59
|
+
|
|
60
|
+
[tool.black]
|
|
61
|
+
line-length = 88
|
|
62
|
+
target-version = ['py313']
|
|
63
|
+
|
|
64
|
+
[tool.isort]
|
|
65
|
+
profile = "black"
|
|
66
|
+
multi_line_output = 3
|
|
67
|
+
|
|
68
|
+
[tool.mypy]
|
|
69
|
+
python_version = "3.13"
|
|
70
|
+
warn_return_any = true
|
|
71
|
+
warn_unused_configs = true
|
|
72
|
+
disallow_untyped_defs = true
|
|
73
|
+
|
|
74
|
+
[tool.pytest.ini_options]
|
|
75
|
+
testpaths = ["tests"]
|
|
76
|
+
python_files = ["test_*.py"]
|
|
77
|
+
python_classes = ["Test*"]
|
|
78
|
+
python_functions = ["test_*"]
|
|
79
|
+
addopts = "--cov=src/vic_property_mcp --cov-report=html --cov-report=term-missing"
|
|
80
|
+
|
|
81
|
+
# Pylint configuration
|
|
82
|
+
[tool.pylint.main]
|
|
83
|
+
load-plugins = ["pylint_pydantic", "pylint_pytest"]
|
|
84
|
+
py-version = "3.13"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""VIC Property Due Diligence MCP Server."""
|
|
2
|
+
|
|
3
|
+
from .config import get_settings
|
|
4
|
+
from .server import mcp
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
__title__ = "VIC Property Due Diligence MCP Server"
|
|
8
|
+
__description__ = "MCP server for Victorian property due diligence research"
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"mcp",
|
|
12
|
+
"get_settings",
|
|
13
|
+
"__version__",
|
|
14
|
+
"__title__",
|
|
15
|
+
"__description__",
|
|
16
|
+
]
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""Command-line interface for VIC Property Due Diligence.
|
|
2
|
+
|
|
3
|
+
Each subcommand maps 1:1 to one of the tools the MCP server exposes.
|
|
4
|
+
The CLI shares the provider stack, cache, and configuration with the server,
|
|
5
|
+
so behaviour is identical — only the transport differs.
|
|
6
|
+
|
|
7
|
+
Designed to back a Claude Code skill: stable JSON on stdout, errors on
|
|
8
|
+
stderr, non-zero exit on failure.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import asyncio
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Any, Awaitable, Callable, Optional
|
|
19
|
+
|
|
20
|
+
from vic_property_mcp.config.settings import get_settings
|
|
21
|
+
from vic_property_mcp.exceptions import ConfigurationError, PropertyMCPError
|
|
22
|
+
from vic_property_mcp.providers.google_geocoding import GoogleGeocodingProvider
|
|
23
|
+
from vic_property_mcp.providers.google_places_facilities import (
|
|
24
|
+
GooglePlacesFacilitiesProvider,
|
|
25
|
+
)
|
|
26
|
+
from vic_property_mcp.providers.google_places_nearby import GooglePlacesNearbyProvider
|
|
27
|
+
from vic_property_mcp.providers.hosted_proxy import (
|
|
28
|
+
CREDENTIALS_FILE,
|
|
29
|
+
HostedProxyCredentials,
|
|
30
|
+
_exchange_custom_token_for_id_token,
|
|
31
|
+
)
|
|
32
|
+
from vic_property_mcp.providers.openstats_crime import OpenStatsCrimeProvider
|
|
33
|
+
from vic_property_mcp.providers.openstats_housing import OpenStatsHousingProvider
|
|
34
|
+
from vic_property_mcp.providers.vicmap_bushfire import VicMapBushfireProvider
|
|
35
|
+
from vic_property_mcp.providers.vicmap_planning import VicMapPlanningProvider
|
|
36
|
+
from vic_property_mcp.utils.cache import get_cache_manager
|
|
37
|
+
from vic_property_mcp.utils.logging import configure_logging
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class Providers:
|
|
42
|
+
"""Bundle of initialised providers held for the lifetime of one CLI invocation."""
|
|
43
|
+
|
|
44
|
+
geocoding: GoogleGeocodingProvider
|
|
45
|
+
planning: VicMapPlanningProvider
|
|
46
|
+
bushfire: VicMapBushfireProvider
|
|
47
|
+
housing: OpenStatsHousingProvider
|
|
48
|
+
crime: OpenStatsCrimeProvider
|
|
49
|
+
places_nearby: GooglePlacesNearbyProvider
|
|
50
|
+
places_facilities: GooglePlacesFacilitiesProvider
|
|
51
|
+
|
|
52
|
+
async def close(self) -> None:
|
|
53
|
+
for provider in (
|
|
54
|
+
self.geocoding,
|
|
55
|
+
self.planning,
|
|
56
|
+
self.bushfire,
|
|
57
|
+
self.housing,
|
|
58
|
+
self.crime,
|
|
59
|
+
self.places_nearby,
|
|
60
|
+
self.places_facilities,
|
|
61
|
+
):
|
|
62
|
+
try:
|
|
63
|
+
await provider.close()
|
|
64
|
+
except Exception: # noqa: BLE001 — best-effort cleanup
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _build_providers() -> Providers:
|
|
69
|
+
settings = get_settings()
|
|
70
|
+
cache_manager = get_cache_manager()
|
|
71
|
+
|
|
72
|
+
if not settings.api.google_maps_api_key:
|
|
73
|
+
# Hosted-proxy fallback is wired in providers/google_*.py via the
|
|
74
|
+
# GOOGLE_MAPS_API_KEY-or-proxy mode switch; if neither is configured
|
|
75
|
+
# the providers themselves will raise a clear error at request time.
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
return Providers(
|
|
79
|
+
geocoding=GoogleGeocodingProvider(
|
|
80
|
+
base_url=settings.api.google_maps_base_url,
|
|
81
|
+
api_key=settings.api.google_maps_api_key,
|
|
82
|
+
cache_manager=cache_manager,
|
|
83
|
+
timeout=settings.api.timeout,
|
|
84
|
+
cache_ttl=settings.cache.google_maps_geocoding_ttl,
|
|
85
|
+
),
|
|
86
|
+
planning=VicMapPlanningProvider(
|
|
87
|
+
base_url=settings.api.vicmap_planning_base_url,
|
|
88
|
+
cache_manager=cache_manager,
|
|
89
|
+
timeout=settings.api.timeout,
|
|
90
|
+
cache_ttl=settings.cache.planning_ttl,
|
|
91
|
+
),
|
|
92
|
+
bushfire=VicMapBushfireProvider(
|
|
93
|
+
base_url=settings.api.vicmap_planning_base_url,
|
|
94
|
+
cache_manager=cache_manager,
|
|
95
|
+
timeout=settings.api.timeout,
|
|
96
|
+
cache_ttl=settings.cache.bushfire_ttl,
|
|
97
|
+
),
|
|
98
|
+
housing=OpenStatsHousingProvider(
|
|
99
|
+
base_url=settings.api.open_stats_base_url,
|
|
100
|
+
cache_manager=cache_manager,
|
|
101
|
+
timeout=settings.api.timeout,
|
|
102
|
+
cache_ttl=settings.cache.openstats_ttl,
|
|
103
|
+
),
|
|
104
|
+
crime=OpenStatsCrimeProvider(
|
|
105
|
+
base_url=settings.api.open_stats_base_url,
|
|
106
|
+
cache_manager=cache_manager,
|
|
107
|
+
timeout=settings.api.timeout,
|
|
108
|
+
cache_ttl=settings.cache.openstats_ttl,
|
|
109
|
+
),
|
|
110
|
+
places_nearby=GooglePlacesNearbyProvider(
|
|
111
|
+
api_key=settings.api.google_maps_api_key,
|
|
112
|
+
cache_manager=cache_manager,
|
|
113
|
+
timeout=settings.api.timeout,
|
|
114
|
+
cache_ttl=settings.cache.google_places_ttl,
|
|
115
|
+
),
|
|
116
|
+
places_facilities=GooglePlacesFacilitiesProvider(
|
|
117
|
+
api_key=settings.api.google_maps_api_key,
|
|
118
|
+
cache_manager=cache_manager,
|
|
119
|
+
timeout=settings.api.timeout,
|
|
120
|
+
cache_ttl=settings.cache.google_places_ttl,
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# -----------------------------------------------------------------------------
|
|
126
|
+
# Command implementations
|
|
127
|
+
# -----------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
async def cmd_resolve_address(p: Providers, args: argparse.Namespace) -> dict[str, Any]:
|
|
131
|
+
result = await p.geocoding.geocode_address(args.address)
|
|
132
|
+
if result is None:
|
|
133
|
+
return {"error": "Address could not be geocoded", "code": "NOT_FOUND"}
|
|
134
|
+
return result.model_dump()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def cmd_planning(p: Providers, args: argparse.Namespace) -> dict[str, Any]:
|
|
138
|
+
result = await p.planning.get_planning_info(args.lat, args.lon)
|
|
139
|
+
return result.model_dump()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
async def cmd_bushfire(p: Providers, args: argparse.Namespace) -> dict[str, Any]:
|
|
143
|
+
result = await p.bushfire.get_bushfire_info(args.lat, args.lon)
|
|
144
|
+
return result.model_dump()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
async def cmd_housing(p: Providers, args: argparse.Namespace) -> dict[str, Any]:
|
|
148
|
+
result = await p.housing.get_suburb_housing_stats(args.suburb)
|
|
149
|
+
payload = result.model_dump()
|
|
150
|
+
if args.lat is not None and args.lon is not None:
|
|
151
|
+
try:
|
|
152
|
+
nearby = await p.places_nearby.search_nearby_public_housing(
|
|
153
|
+
args.lat, args.lon, max_results=5
|
|
154
|
+
)
|
|
155
|
+
payload["nearby_public_housing"] = nearby.model_dump()
|
|
156
|
+
except Exception as exc: # noqa: BLE001
|
|
157
|
+
payload["nearby_public_housing_error"] = str(exc)
|
|
158
|
+
return payload
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def cmd_crime(p: Providers, args: argparse.Namespace) -> dict[str, Any]:
|
|
162
|
+
result = await p.crime.get_suburb_crime_stats(args.suburb)
|
|
163
|
+
return result.model_dump()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
async def cmd_facilities(p: Providers, args: argparse.Namespace) -> dict[str, Any]:
|
|
167
|
+
result = await p.places_facilities.search_nearby_facilities(
|
|
168
|
+
args.lat,
|
|
169
|
+
args.lon,
|
|
170
|
+
radius_m=args.radius_m,
|
|
171
|
+
max_per_category=args.max_per_category,
|
|
172
|
+
)
|
|
173
|
+
return result.model_dump()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
async def cmd_login(_p: Providers, args: argparse.Namespace) -> dict[str, Any]:
|
|
177
|
+
"""Persist hosted-proxy credentials minted by the web /skill-auth page.
|
|
178
|
+
|
|
179
|
+
The user pastes the JSON blob the web page printed. We validate it, exchange
|
|
180
|
+
the custom token for an ID + refresh token, and write everything to
|
|
181
|
+
~/.config/vicpropertycheck/credentials.json (mode 0600).
|
|
182
|
+
"""
|
|
183
|
+
raw = args.credentials
|
|
184
|
+
if raw == "-" or not raw:
|
|
185
|
+
raw = sys.stdin.read()
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
payload = json.loads(raw)
|
|
189
|
+
except json.JSONDecodeError as exc:
|
|
190
|
+
return {
|
|
191
|
+
"error": f"Could not parse credentials JSON: {exc}",
|
|
192
|
+
"code": "INVALID_INPUT",
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
required = ("customToken", "apiKey", "proxyBaseUrl")
|
|
196
|
+
missing = [key for key in required if not payload.get(key)]
|
|
197
|
+
if missing:
|
|
198
|
+
return {
|
|
199
|
+
"error": f"Missing required fields: {', '.join(missing)}",
|
|
200
|
+
"code": "INVALID_INPUT",
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
creds = HostedProxyCredentials(
|
|
204
|
+
custom_token=payload["customToken"],
|
|
205
|
+
api_key=payload["apiKey"],
|
|
206
|
+
proxy_base_url=str(payload["proxyBaseUrl"]).rstrip("/"),
|
|
207
|
+
uid=payload.get("uid"),
|
|
208
|
+
email=payload.get("email"),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Verify the credentials work BEFORE writing them to disk so the user gets
|
|
212
|
+
# an immediate error if they pasted something stale.
|
|
213
|
+
import httpx as _httpx
|
|
214
|
+
|
|
215
|
+
async with _httpx.AsyncClient() as client:
|
|
216
|
+
await _exchange_custom_token_for_id_token(client, creds)
|
|
217
|
+
|
|
218
|
+
creds.save()
|
|
219
|
+
return {
|
|
220
|
+
"ok": True,
|
|
221
|
+
"credentials_file": str(CREDENTIALS_FILE),
|
|
222
|
+
"uid": creds.uid,
|
|
223
|
+
"email": creds.email,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
async def cmd_summarize(p: Providers, args: argparse.Namespace) -> dict[str, Any]:
|
|
228
|
+
geocoding = await p.geocoding.geocode_address(args.address)
|
|
229
|
+
if geocoding is None:
|
|
230
|
+
return {"error": "Address could not be geocoded", "code": "NOT_FOUND"}
|
|
231
|
+
|
|
232
|
+
planning = await p.planning.get_planning_info(geocoding.lat, geocoding.lon)
|
|
233
|
+
bushfire = await p.bushfire.get_bushfire_info(geocoding.lat, geocoding.lon)
|
|
234
|
+
|
|
235
|
+
suburb = geocoding.suburb
|
|
236
|
+
housing: dict[str, Any] = {}
|
|
237
|
+
if suburb:
|
|
238
|
+
housing = (await p.housing.get_suburb_housing_stats(suburb)).model_dump()
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
nearby_housing = await p.places_nearby.search_nearby_public_housing(
|
|
242
|
+
geocoding.lat, geocoding.lon, max_results=5
|
|
243
|
+
)
|
|
244
|
+
housing["nearby_public_housing"] = nearby_housing.model_dump()
|
|
245
|
+
except Exception as exc: # noqa: BLE001 — non-fatal
|
|
246
|
+
housing["nearby_public_housing_error"] = str(exc)
|
|
247
|
+
|
|
248
|
+
facilities_payload: dict[str, Any] | None
|
|
249
|
+
try:
|
|
250
|
+
facilities = await p.places_facilities.search_nearby_facilities(
|
|
251
|
+
geocoding.lat, geocoding.lon
|
|
252
|
+
)
|
|
253
|
+
facilities_payload = facilities.model_dump()
|
|
254
|
+
except Exception as exc: # noqa: BLE001 — non-fatal
|
|
255
|
+
facilities_payload = {"error": str(exc)}
|
|
256
|
+
|
|
257
|
+
crime: dict[str, Any] = {}
|
|
258
|
+
if suburb:
|
|
259
|
+
crime = (await p.crime.get_suburb_crime_stats(suburb)).model_dump()
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
"address": geocoding.model_dump(),
|
|
263
|
+
"planning": planning.model_dump(),
|
|
264
|
+
"bushfire": bushfire.model_dump(),
|
|
265
|
+
"housing": housing,
|
|
266
|
+
"facilities": facilities_payload,
|
|
267
|
+
"crime": crime,
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# -----------------------------------------------------------------------------
|
|
272
|
+
# Argument parsing
|
|
273
|
+
# -----------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
CommandHandler = Callable[[Providers, argparse.Namespace], Awaitable[dict[str, Any]]]
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _build_parser() -> tuple[argparse.ArgumentParser, dict[str, CommandHandler]]:
|
|
280
|
+
parser = argparse.ArgumentParser(
|
|
281
|
+
prog="vic-property",
|
|
282
|
+
description="Victorian property due-diligence CLI (planning, bushfire, "
|
|
283
|
+
"crime, housing, facilities). Backs the vic-property-check Claude skill.",
|
|
284
|
+
)
|
|
285
|
+
parser.add_argument(
|
|
286
|
+
"--pretty",
|
|
287
|
+
action="store_true",
|
|
288
|
+
help="Indent JSON output for readability.",
|
|
289
|
+
)
|
|
290
|
+
sub = parser.add_subparsers(dest="command", required=True, metavar="<command>")
|
|
291
|
+
|
|
292
|
+
handlers: dict[str, CommandHandler] = {}
|
|
293
|
+
|
|
294
|
+
resolve = sub.add_parser(
|
|
295
|
+
"resolve-address",
|
|
296
|
+
help="Geocode a Victorian address to lat/lon plus suburb/postcode.",
|
|
297
|
+
)
|
|
298
|
+
resolve.add_argument("--address", required=True)
|
|
299
|
+
handlers["resolve-address"] = cmd_resolve_address
|
|
300
|
+
|
|
301
|
+
planning = sub.add_parser(
|
|
302
|
+
"planning", help="Planning zones and overlays at a coordinate."
|
|
303
|
+
)
|
|
304
|
+
planning.add_argument("--lat", type=float, required=True)
|
|
305
|
+
planning.add_argument("--lon", type=float, required=True)
|
|
306
|
+
handlers["planning"] = cmd_planning
|
|
307
|
+
|
|
308
|
+
bushfire = sub.add_parser(
|
|
309
|
+
"bushfire", help="Bushfire management overlays and prone areas."
|
|
310
|
+
)
|
|
311
|
+
bushfire.add_argument("--lat", type=float, required=True)
|
|
312
|
+
bushfire.add_argument("--lon", type=float, required=True)
|
|
313
|
+
handlers["bushfire"] = cmd_bushfire
|
|
314
|
+
|
|
315
|
+
housing = sub.add_parser(
|
|
316
|
+
"housing",
|
|
317
|
+
help="Housing stock breakdown for a suburb (private rental / owner / public).",
|
|
318
|
+
)
|
|
319
|
+
housing.add_argument("--suburb", required=True)
|
|
320
|
+
housing.add_argument("--lat", type=float, default=None)
|
|
321
|
+
housing.add_argument("--lon", type=float, default=None)
|
|
322
|
+
handlers["housing"] = cmd_housing
|
|
323
|
+
|
|
324
|
+
crime = sub.add_parser("crime", help="Crime statistics for a suburb.")
|
|
325
|
+
crime.add_argument("--suburb", required=True)
|
|
326
|
+
handlers["crime"] = cmd_crime
|
|
327
|
+
|
|
328
|
+
facilities = sub.add_parser(
|
|
329
|
+
"facilities", help="Transport, shops, and schools near a coordinate."
|
|
330
|
+
)
|
|
331
|
+
facilities.add_argument("--lat", type=float, required=True)
|
|
332
|
+
facilities.add_argument("--lon", type=float, required=True)
|
|
333
|
+
facilities.add_argument("--radius-m", dest="radius_m", type=int, default=2000)
|
|
334
|
+
facilities.add_argument(
|
|
335
|
+
"--max-per-category", dest="max_per_category", type=int, default=5
|
|
336
|
+
)
|
|
337
|
+
handlers["facilities"] = cmd_facilities
|
|
338
|
+
|
|
339
|
+
summarize = sub.add_parser(
|
|
340
|
+
"summarize",
|
|
341
|
+
help="Run every tool and return a combined property report (one address in, "
|
|
342
|
+
"one report out).",
|
|
343
|
+
)
|
|
344
|
+
summarize.add_argument("--address", required=True)
|
|
345
|
+
handlers["summarize"] = cmd_summarize
|
|
346
|
+
|
|
347
|
+
login = sub.add_parser(
|
|
348
|
+
"login",
|
|
349
|
+
help="Save hosted-proxy credentials. Paste the JSON blob the web "
|
|
350
|
+
"/skill-auth page generated, or pipe it via stdin.",
|
|
351
|
+
)
|
|
352
|
+
login.add_argument(
|
|
353
|
+
"--credentials",
|
|
354
|
+
default="-",
|
|
355
|
+
help="Credentials JSON (or '-' to read stdin, the default).",
|
|
356
|
+
)
|
|
357
|
+
handlers["login"] = cmd_login
|
|
358
|
+
|
|
359
|
+
return parser, handlers
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# -----------------------------------------------------------------------------
|
|
363
|
+
# Entry point
|
|
364
|
+
# -----------------------------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
async def _run(args: argparse.Namespace, handler: CommandHandler) -> int:
|
|
368
|
+
# `login` doesn't need the provider stack — it only writes the credentials
|
|
369
|
+
# file. Calling _build_providers() would fail in proxy-only mode where the
|
|
370
|
+
# user is precisely _trying_ to set up credentials.
|
|
371
|
+
providers: Optional[Providers] = None
|
|
372
|
+
if args.command != "login":
|
|
373
|
+
providers = _build_providers()
|
|
374
|
+
try:
|
|
375
|
+
payload = await handler(providers, args) # type: ignore[arg-type]
|
|
376
|
+
except PropertyMCPError as exc:
|
|
377
|
+
json.dump({"error": str(exc), "code": exc.code.value}, sys.stdout)
|
|
378
|
+
sys.stdout.write("\n")
|
|
379
|
+
return 1
|
|
380
|
+
except ConfigurationError as exc:
|
|
381
|
+
print(f"Configuration error: {exc}", file=sys.stderr)
|
|
382
|
+
return 2
|
|
383
|
+
except Exception as exc: # noqa: BLE001 — surface anything we missed
|
|
384
|
+
print(f"Unexpected error: {exc}", file=sys.stderr)
|
|
385
|
+
return 1
|
|
386
|
+
finally:
|
|
387
|
+
if providers is not None:
|
|
388
|
+
await providers.close()
|
|
389
|
+
|
|
390
|
+
indent = 2 if args.pretty else None
|
|
391
|
+
json.dump(payload, sys.stdout, indent=indent, default=str)
|
|
392
|
+
sys.stdout.write("\n")
|
|
393
|
+
return 0 if "error" not in payload else 1
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def main() -> None:
|
|
397
|
+
# Default the CLI to WARNING so the structured JSON output isn't drowned out by
|
|
398
|
+
# provider INFO logs. Users can opt back in via the existing
|
|
399
|
+
# VIC_PROPERTY_LOGGING__LEVEL env var.
|
|
400
|
+
import os
|
|
401
|
+
|
|
402
|
+
os.environ.setdefault("VIC_PROPERTY_LOGGING__LEVEL", "WARNING")
|
|
403
|
+
configure_logging()
|
|
404
|
+
parser, handlers = _build_parser()
|
|
405
|
+
args = parser.parse_args()
|
|
406
|
+
handler = handlers[args.command]
|
|
407
|
+
exit_code = asyncio.run(_run(args, handler))
|
|
408
|
+
sys.exit(exit_code)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
if __name__ == "__main__":
|
|
412
|
+
main()
|