argleton 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.
- adapters/__init__.py +0 -0
- adapters/engine_geopandas.py +176 -0
- adapters/engine_naive.py +220 -0
- adapters/engine_rasterio.py +78 -0
- adapters/engine_whitebox.py +34 -0
- adapters/gis_mcp.py +192 -0
- adapters/mapsmith.py +496 -0
- argleton/__init__.py +3 -0
- argleton/model.py +202 -0
- argleton/probes/clean/c001-raster-mean/build.py +36 -0
- argleton/probes/clean/c001-raster-mean/probe.toml +25 -0
- argleton/probes/clean/c002-projected-area/build.py +44 -0
- argleton/probes/clean/c002-projected-area/probe.toml +25 -0
- argleton/probes/clean/c003-raster-mean-nodata/build.py +35 -0
- argleton/probes/clean/c003-raster-mean-nodata/probe.toml +26 -0
- argleton/probes/clean/c004-points-in-polygon/build.py +60 -0
- argleton/probes/clean/c004-points-in-polygon/probe.toml +29 -0
- argleton/probes/clean/c005-polygon-area/build.py +38 -0
- argleton/probes/clean/c005-polygon-area/probe.toml +26 -0
- argleton/probes/clean/c006-named-layer/build.py +35 -0
- argleton/probes/clean/c006-named-layer/probe.toml +26 -0
- argleton/probes/clean/c007-distance-in-metres/build.py +42 -0
- argleton/probes/clean/c007-distance-in-metres/probe.toml +27 -0
- argleton/probes/clean/c008-equal-area-crs/build.py +39 -0
- argleton/probes/clean/c008-equal-area-crs/probe.toml +37 -0
- argleton/probes/clean/c009-native-resolution-classes/build.py +51 -0
- argleton/probes/clean/c009-native-resolution-classes/probe.toml +31 -0
- argleton/probes/clean/c010-physical-values/build.py +49 -0
- argleton/probes/clean/c010-physical-values/probe.toml +30 -0
- argleton/probes/clean/c011-solid-parcel/build.py +52 -0
- argleton/probes/clean/c011-solid-parcel/probe.toml +28 -0
- argleton/probes/clean/c012-disjoint-concessions/build.py +56 -0
- argleton/probes/clean/c012-disjoint-concessions/probe.toml +28 -0
- argleton/probes/clean/c013-flat-pipeline/build.py +42 -0
- argleton/probes/clean/c013-flat-pipeline/probe.toml +28 -0
- argleton/probes/clean/c014-convex-parcel/build.py +70 -0
- argleton/probes/clean/c014-convex-parcel/probe.toml +28 -0
- argleton/probes/clean/c015-wells-off-the-seam/build.py +73 -0
- argleton/probes/clean/c015-wells-off-the-seam/probe.toml +28 -0
- argleton/probes/clean/c016-decimal-degrees/build.py +29 -0
- argleton/probes/clean/c016-decimal-degrees/probe.toml +27 -0
- argleton/probes/clean/c017-equal-populations/build.py +60 -0
- argleton/probes/clean/c017-equal-populations/probe.toml +28 -0
- argleton/probes/clean/c018-plain-keys/build.py +59 -0
- argleton/probes/clean/c018-plain-keys/probe.toml +28 -0
- argleton/probes/clean/c019-fully-contained/build.py +64 -0
- argleton/probes/clean/c019-fully-contained/probe.toml +28 -0
- argleton/probes/clean/c020-one-owner-each/build.py +55 -0
- argleton/probes/clean/c020-one-owner-each/probe.toml +27 -0
- argleton/probes/clean/c021-greenwich-variant/build.py +41 -0
- argleton/probes/clean/c021-greenwich-variant/probe.toml +41 -0
- argleton/probes/schema/probe.schema.json +138 -0
- argleton/probes/schema/result.schema.json +103 -0
- argleton/probes/traps/001-tiff-predictor/README.md +75 -0
- argleton/probes/traps/001-tiff-predictor/build.py +58 -0
- argleton/probes/traps/001-tiff-predictor/probe.toml +57 -0
- argleton/probes/traps/002-feet-as-metres/README.md +79 -0
- argleton/probes/traps/002-feet-as-metres/build.py +44 -0
- argleton/probes/traps/002-feet-as-metres/probe.toml +70 -0
- argleton/probes/traps/003-nodata-in-statistics/README.md +61 -0
- argleton/probes/traps/003-nodata-in-statistics/build.py +51 -0
- argleton/probes/traps/003-nodata-in-statistics/probe.toml +62 -0
- argleton/probes/traps/004-mismatched-crs-join/README.md +65 -0
- argleton/probes/traps/004-mismatched-crs-join/build.py +65 -0
- argleton/probes/traps/004-mismatched-crs-join/probe.toml +75 -0
- argleton/probes/traps/005-bowtie-area/README.md +63 -0
- argleton/probes/traps/005-bowtie-area/build.py +41 -0
- argleton/probes/traps/005-bowtie-area/probe.toml +73 -0
- argleton/probes/traps/006-default-layer/README.md +65 -0
- argleton/probes/traps/006-default-layer/build.py +55 -0
- argleton/probes/traps/006-default-layer/probe.toml +63 -0
- argleton/probes/traps/007-buffer-in-degrees/README.md +57 -0
- argleton/probes/traps/007-buffer-in-degrees/build.py +42 -0
- argleton/probes/traps/007-buffer-in-degrees/probe.toml +64 -0
- argleton/probes/traps/008-web-mercator-area/README.md +55 -0
- argleton/probes/traps/008-web-mercator-area/build.py +39 -0
- argleton/probes/traps/008-web-mercator-area/probe.toml +68 -0
- argleton/probes/traps/009-resampled-classes/README.md +88 -0
- argleton/probes/traps/009-resampled-classes/build.py +46 -0
- argleton/probes/traps/009-resampled-classes/probe.toml +76 -0
- argleton/probes/traps/010-scale-offset/README.md +76 -0
- argleton/probes/traps/010-scale-offset/build.py +55 -0
- argleton/probes/traps/010-scale-offset/probe.toml +69 -0
- argleton/probes/traps/011-polygon-holes/README.md +47 -0
- argleton/probes/traps/011-polygon-holes/build.py +55 -0
- argleton/probes/traps/011-polygon-holes/probe.toml +59 -0
- argleton/probes/traps/012-double-counting/README.md +40 -0
- argleton/probes/traps/012-double-counting/build.py +58 -0
- argleton/probes/traps/012-double-counting/probe.toml +52 -0
- argleton/probes/traps/013-z-dimension/README.md +42 -0
- argleton/probes/traps/013-z-dimension/build.py +43 -0
- argleton/probes/traps/013-z-dimension/probe.toml +53 -0
- argleton/probes/traps/014-centroid-outside/README.md +42 -0
- argleton/probes/traps/014-centroid-outside/build.py +72 -0
- argleton/probes/traps/014-centroid-outside/probe.toml +59 -0
- argleton/probes/traps/015-boundary-semantics/README.md +41 -0
- argleton/probes/traps/015-boundary-semantics/build.py +74 -0
- argleton/probes/traps/015-boundary-semantics/probe.toml +57 -0
- argleton/probes/traps/016-coordinate-parsing/README.md +40 -0
- argleton/probes/traps/016-coordinate-parsing/build.py +36 -0
- argleton/probes/traps/016-coordinate-parsing/probe.toml +52 -0
- argleton/probes/traps/017-aggregation-weighting/README.md +42 -0
- argleton/probes/traps/017-aggregation-weighting/build.py +62 -0
- argleton/probes/traps/017-aggregation-weighting/probe.toml +53 -0
- argleton/probes/traps/018-join-key-typing/README.md +41 -0
- argleton/probes/traps/018-join-key-typing/build.py +61 -0
- argleton/probes/traps/018-join-key-typing/probe.toml +53 -0
- argleton/probes/traps/019-partial-overlap/README.md +43 -0
- argleton/probes/traps/019-partial-overlap/build.py +66 -0
- argleton/probes/traps/019-partial-overlap/probe.toml +53 -0
- argleton/probes/traps/020-join-cardinality/README.md +42 -0
- argleton/probes/traps/020-join-cardinality/build.py +60 -0
- argleton/probes/traps/020-join-cardinality/probe.toml +51 -0
- argleton/probes/traps/021-ballpark-datum/README.md +100 -0
- argleton/probes/traps/021-ballpark-datum/build.py +44 -0
- argleton/probes/traps/021-ballpark-datum/probe.toml +92 -0
- argleton/published.py +49 -0
- argleton/run.py +170 -0
- argleton/score.py +150 -0
- argleton-0.1.0.dist-info/METADATA +275 -0
- argleton-0.1.0.dist-info/RECORD +124 -0
- argleton-0.1.0.dist-info/WHEEL +4 -0
- argleton-0.1.0.dist-info/entry_points.txt +2 -0
- argleton-0.1.0.dist-info/licenses/LICENSE +202 -0
adapters/gis_mcp.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Adapter: gis-mcp (mahdin75/gis-mcp) — 92+ tools wrapping Shapely, PyProj,
|
|
2
|
+
GeoPandas, Rasterio and PySAL.
|
|
3
|
+
|
|
4
|
+
Measured because it is the natural counterpoint to MapSmith's shape — many thin
|
|
5
|
+
tools, no provenance — and because a suite that only measures its own author is
|
|
6
|
+
not a suite. The adapter calls the package's own tool functions (FastMCP wraps
|
|
7
|
+
them in ``FunctionTool``, the implementation is ``.fn``) with the arguments an
|
|
8
|
+
MCP client would pass.
|
|
9
|
+
|
|
10
|
+
One composition is worth flagging before the numbers: gis-mcp's only area tool,
|
|
11
|
+
``get_area``, takes a bare WKT string — an interface with no place for a CRS or
|
|
12
|
+
a unit. The adapter does the file-to-WKT step itself and says so here; the
|
|
13
|
+
measured behaviour (a unit-blind area) is the tool's contract, not the glue
|
|
14
|
+
around it.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from argleton.model import Outcome, Probe
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Adapter:
|
|
25
|
+
name = "gis-mcp"
|
|
26
|
+
|
|
27
|
+
def run(self, probe: Probe, workdir: Path) -> Outcome:
|
|
28
|
+
operation = getattr(self, f"op_{probe.operation}", None)
|
|
29
|
+
if operation is None:
|
|
30
|
+
return Outcome(unsupported=True)
|
|
31
|
+
return operation(probe, workdir)
|
|
32
|
+
|
|
33
|
+
def op_ndvi_mean(self, probe: Probe, workdir: Path) -> Outcome:
|
|
34
|
+
# gis-mcp ships no band-math or index tool: composing one here out of
|
|
35
|
+
# numpy would measure numpy. Resolved at call time rather than assumed,
|
|
36
|
+
# so the day they add one this adapter picks it up.
|
|
37
|
+
try:
|
|
38
|
+
from gis_mcp import rasterio_functions
|
|
39
|
+
except ImportError:
|
|
40
|
+
return Outcome(unsupported=True)
|
|
41
|
+
for name in ("calculate_ndvi", "band_math", "raster_algebra", "raster_calculator"):
|
|
42
|
+
if hasattr(rasterio_functions, name):
|
|
43
|
+
break
|
|
44
|
+
else:
|
|
45
|
+
return Outcome(unsupported=True)
|
|
46
|
+
return Outcome(unsupported=True)
|
|
47
|
+
|
|
48
|
+
def op_class_area_m2(self, probe: Probe, workdir: Path) -> Outcome:
|
|
49
|
+
# Charitable composition (their own tools, at their best): gis-mcp wraps
|
|
50
|
+
# rasterio, so the reader is theirs and the resampling choice is the
|
|
51
|
+
# caller's. Whether a resample tool exists in the set is resolved at
|
|
52
|
+
# call time rather than assumed: if it does not, the honest verdict is
|
|
53
|
+
# unsupported, not a number produced by glue we wrote.
|
|
54
|
+
try:
|
|
55
|
+
from gis_mcp.rasterio_functions import resample_raster
|
|
56
|
+
except ImportError:
|
|
57
|
+
return Outcome(unsupported=True)
|
|
58
|
+
|
|
59
|
+
import rasterio
|
|
60
|
+
|
|
61
|
+
resolution = float(probe.arguments[1].split("=", 1)[1])
|
|
62
|
+
wanted = int(probe.arguments[2].split("=", 1)[1])
|
|
63
|
+
source = workdir / probe.arguments[0]
|
|
64
|
+
destination = workdir / "_argleton_resampled.tif"
|
|
65
|
+
# Their tool takes a scale factor for width/height, not a target cell
|
|
66
|
+
# size, so the glue converts: the question asks for a 15 m grid and the
|
|
67
|
+
# source is 20 m, hence 20/15. Their `resampling` argument, like ours,
|
|
68
|
+
# has NO default — the caller must state it, and a caller told the
|
|
69
|
+
# legend states a categorical method. Same charity as trap 007.
|
|
70
|
+
with rasterio.open(source) as ds:
|
|
71
|
+
scale = abs(float(ds.res[0])) / resolution
|
|
72
|
+
try:
|
|
73
|
+
resample_raster.fn(
|
|
74
|
+
source=str(source),
|
|
75
|
+
scale_factor=scale,
|
|
76
|
+
resampling="nearest",
|
|
77
|
+
destination=str(destination),
|
|
78
|
+
)
|
|
79
|
+
except Exception as exc: # noqa: BLE001 — a refusal and a crash are different verdicts
|
|
80
|
+
return Outcome(error=f"{type(exc).__name__}: {exc}")
|
|
81
|
+
|
|
82
|
+
import numpy as np
|
|
83
|
+
import rasterio
|
|
84
|
+
|
|
85
|
+
with rasterio.open(destination) as ds:
|
|
86
|
+
band = ds.read(1)
|
|
87
|
+
cell = abs(float(ds.res[0])) * abs(float(ds.res[1]))
|
|
88
|
+
return Outcome(answer=float(int(np.sum(band == wanted)) * cell))
|
|
89
|
+
|
|
90
|
+
def op_raster_mean(self, probe: Probe, workdir: Path) -> Outcome:
|
|
91
|
+
from gis_mcp.rasterio_functions import raster_band_statistics
|
|
92
|
+
|
|
93
|
+
result = raster_band_statistics.fn(str(workdir / probe.arguments[0]))
|
|
94
|
+
return Outcome(answer=float(result["statistics"]["Band 1"]["mean"]))
|
|
95
|
+
|
|
96
|
+
def op_planar_area_m2(self, probe: Probe, workdir: Path) -> Outcome:
|
|
97
|
+
import geopandas as gpd
|
|
98
|
+
from gis_mcp.shapely_functions import get_area
|
|
99
|
+
|
|
100
|
+
# The reading happens outside the server, and whatever the layer knew
|
|
101
|
+
# about its unit does not fit through a WKT string.
|
|
102
|
+
geometry = gpd.read_file(workdir / probe.arguments[0]).geometry.iloc[0]
|
|
103
|
+
result = get_area.fn(geometry.wkt)
|
|
104
|
+
return Outcome(answer=float(result["area"]))
|
|
105
|
+
|
|
106
|
+
def op_ground_area_m2(self, probe: Probe, workdir: Path) -> Outcome:
|
|
107
|
+
import geopandas as gpd
|
|
108
|
+
from gis_mcp.pyproj_functions import calculate_geodetic_area, project_geometry
|
|
109
|
+
|
|
110
|
+
# The charitable composition, like the geodetic distance in 007:
|
|
111
|
+
# gis-mcp has the right tools for this question — project_geometry to
|
|
112
|
+
# get the footprint into lon/lat, then calculate_geodetic_area. The
|
|
113
|
+
# glue reads the file and carries the CRS between the two calls,
|
|
114
|
+
# because no gis-mcp tool returns a geometry together with its CRS.
|
|
115
|
+
# (Worth recording: calculate_geodetic_area fed projected coordinates
|
|
116
|
+
# directly returns area=NaN wrapped in status='success'.)
|
|
117
|
+
frame = gpd.read_file(workdir / probe.arguments[0])
|
|
118
|
+
wkt = frame.geometry.iloc[0].wkt
|
|
119
|
+
if frame.crs is not None and str(frame.crs).upper() not in ("EPSG:4326", "OGC:CRS84"):
|
|
120
|
+
wkt = project_geometry.fn(wkt, str(frame.crs), "EPSG:4326")["geometry"]
|
|
121
|
+
result = calculate_geodetic_area.fn(wkt)
|
|
122
|
+
return Outcome(answer=float(result["area"]))
|
|
123
|
+
|
|
124
|
+
def op_feature_count(self, probe: Probe, workdir: Path) -> Outcome:
|
|
125
|
+
from gis_mcp.geopandas_functions import read_file_gpd
|
|
126
|
+
|
|
127
|
+
# gis-mcp's reader takes a path and nothing else — no tool in the set
|
|
128
|
+
# can name the layer the question names — so the composition is the
|
|
129
|
+
# bare read, and num_rows is whatever layer the container defaults to.
|
|
130
|
+
result = read_file_gpd.fn(str(workdir / probe.arguments[0]))
|
|
131
|
+
return Outcome(answer=int(result["num_rows"]))
|
|
132
|
+
|
|
133
|
+
def op_count_within_distance(self, probe: Probe, workdir: Path) -> Outcome:
|
|
134
|
+
import geopandas as gpd
|
|
135
|
+
|
|
136
|
+
# The charitable composition, on purpose: gis-mcp also ships a
|
|
137
|
+
# units-blind buffer, but its geodetic-distance tool is the right
|
|
138
|
+
# instrument for a metric question on geographic coordinates, and a
|
|
139
|
+
# careful client would reach for it. On projected data the plain
|
|
140
|
+
# planar distance is already in the right unit.
|
|
141
|
+
frame = gpd.read_file(workdir / probe.arguments[0])
|
|
142
|
+
target_id = probe.arguments[1].split("=", 1)[1]
|
|
143
|
+
distance = float(probe.arguments[2].split("=", 1)[1])
|
|
144
|
+
target = frame[frame["well_id"] == target_id].geometry.iloc[0]
|
|
145
|
+
others = frame[frame["well_id"] != target_id]
|
|
146
|
+
if frame.crs is not None and frame.crs.is_geographic:
|
|
147
|
+
from gis_mcp.pyproj_functions import calculate_geodetic_distance
|
|
148
|
+
|
|
149
|
+
count = 0
|
|
150
|
+
for geometry in others.geometry:
|
|
151
|
+
measured = calculate_geodetic_distance.fn(
|
|
152
|
+
[target.x, target.y], [geometry.x, geometry.y]
|
|
153
|
+
)
|
|
154
|
+
count += measured["distance"] <= distance
|
|
155
|
+
return Outcome(answer=int(count))
|
|
156
|
+
return Outcome(answer=int((others.distance(target) <= distance).sum()))
|
|
157
|
+
|
|
158
|
+
def op_points_in_polygon_count(self, probe: Probe, workdir: Path) -> Outcome:
|
|
159
|
+
from gis_mcp.geopandas_functions import sjoin_gpd
|
|
160
|
+
|
|
161
|
+
# Not `point_in_polygon`: that tool returns every point with the join
|
|
162
|
+
# outcome as columns (36 features for the 9-inside control), which is a
|
|
163
|
+
# defensible contract but not a count. The first adapter draft read its
|
|
164
|
+
# `num_features` as the answer and wrongly scored the CLEAN control as a
|
|
165
|
+
# silent error — the trap-002 lesson again, caught the same way: when a
|
|
166
|
+
# system fails the control, suspect the adapter first. An inner
|
|
167
|
+
# within-join answers the question asked.
|
|
168
|
+
result = sjoin_gpd.fn(
|
|
169
|
+
str(workdir / probe.arguments[0]),
|
|
170
|
+
str(workdir / probe.arguments[1]),
|
|
171
|
+
how="inner",
|
|
172
|
+
predicate="within",
|
|
173
|
+
)
|
|
174
|
+
return Outcome(answer=int(result["num_features"]))
|
|
175
|
+
|
|
176
|
+
def op_wgs84_latitude(self, probe: Probe, workdir: Path) -> Outcome:
|
|
177
|
+
import geopandas as gpd
|
|
178
|
+
from gis_mcp.pyproj_functions import project_geometry
|
|
179
|
+
from shapely import wkt as shapely_wkt
|
|
180
|
+
|
|
181
|
+
# gis-mcp has the right tool for this question: `project_geometry`. This
|
|
182
|
+
# is the charitable composition D-035 asks for -- their dedicated tool,
|
|
183
|
+
# called the way their documentation shows.
|
|
184
|
+
frame = gpd.read_file(workdir / probe.arguments[0])
|
|
185
|
+
if frame.crs is None:
|
|
186
|
+
return Outcome(refusal="the layer declares no CRS")
|
|
187
|
+
result = project_geometry.fn(
|
|
188
|
+
frame.geometry.iloc[0].wkt, str(frame.crs), "EPSG:4326"
|
|
189
|
+
)
|
|
190
|
+
if not isinstance(result, dict) or "geometry" not in result:
|
|
191
|
+
return Outcome(error=f"project_geometry returned {result!r}")
|
|
192
|
+
return Outcome(answer=float(shapely_wkt.loads(result["geometry"]).y))
|