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.
Files changed (124) hide show
  1. adapters/__init__.py +0 -0
  2. adapters/engine_geopandas.py +176 -0
  3. adapters/engine_naive.py +220 -0
  4. adapters/engine_rasterio.py +78 -0
  5. adapters/engine_whitebox.py +34 -0
  6. adapters/gis_mcp.py +192 -0
  7. adapters/mapsmith.py +496 -0
  8. argleton/__init__.py +3 -0
  9. argleton/model.py +202 -0
  10. argleton/probes/clean/c001-raster-mean/build.py +36 -0
  11. argleton/probes/clean/c001-raster-mean/probe.toml +25 -0
  12. argleton/probes/clean/c002-projected-area/build.py +44 -0
  13. argleton/probes/clean/c002-projected-area/probe.toml +25 -0
  14. argleton/probes/clean/c003-raster-mean-nodata/build.py +35 -0
  15. argleton/probes/clean/c003-raster-mean-nodata/probe.toml +26 -0
  16. argleton/probes/clean/c004-points-in-polygon/build.py +60 -0
  17. argleton/probes/clean/c004-points-in-polygon/probe.toml +29 -0
  18. argleton/probes/clean/c005-polygon-area/build.py +38 -0
  19. argleton/probes/clean/c005-polygon-area/probe.toml +26 -0
  20. argleton/probes/clean/c006-named-layer/build.py +35 -0
  21. argleton/probes/clean/c006-named-layer/probe.toml +26 -0
  22. argleton/probes/clean/c007-distance-in-metres/build.py +42 -0
  23. argleton/probes/clean/c007-distance-in-metres/probe.toml +27 -0
  24. argleton/probes/clean/c008-equal-area-crs/build.py +39 -0
  25. argleton/probes/clean/c008-equal-area-crs/probe.toml +37 -0
  26. argleton/probes/clean/c009-native-resolution-classes/build.py +51 -0
  27. argleton/probes/clean/c009-native-resolution-classes/probe.toml +31 -0
  28. argleton/probes/clean/c010-physical-values/build.py +49 -0
  29. argleton/probes/clean/c010-physical-values/probe.toml +30 -0
  30. argleton/probes/clean/c011-solid-parcel/build.py +52 -0
  31. argleton/probes/clean/c011-solid-parcel/probe.toml +28 -0
  32. argleton/probes/clean/c012-disjoint-concessions/build.py +56 -0
  33. argleton/probes/clean/c012-disjoint-concessions/probe.toml +28 -0
  34. argleton/probes/clean/c013-flat-pipeline/build.py +42 -0
  35. argleton/probes/clean/c013-flat-pipeline/probe.toml +28 -0
  36. argleton/probes/clean/c014-convex-parcel/build.py +70 -0
  37. argleton/probes/clean/c014-convex-parcel/probe.toml +28 -0
  38. argleton/probes/clean/c015-wells-off-the-seam/build.py +73 -0
  39. argleton/probes/clean/c015-wells-off-the-seam/probe.toml +28 -0
  40. argleton/probes/clean/c016-decimal-degrees/build.py +29 -0
  41. argleton/probes/clean/c016-decimal-degrees/probe.toml +27 -0
  42. argleton/probes/clean/c017-equal-populations/build.py +60 -0
  43. argleton/probes/clean/c017-equal-populations/probe.toml +28 -0
  44. argleton/probes/clean/c018-plain-keys/build.py +59 -0
  45. argleton/probes/clean/c018-plain-keys/probe.toml +28 -0
  46. argleton/probes/clean/c019-fully-contained/build.py +64 -0
  47. argleton/probes/clean/c019-fully-contained/probe.toml +28 -0
  48. argleton/probes/clean/c020-one-owner-each/build.py +55 -0
  49. argleton/probes/clean/c020-one-owner-each/probe.toml +27 -0
  50. argleton/probes/clean/c021-greenwich-variant/build.py +41 -0
  51. argleton/probes/clean/c021-greenwich-variant/probe.toml +41 -0
  52. argleton/probes/schema/probe.schema.json +138 -0
  53. argleton/probes/schema/result.schema.json +103 -0
  54. argleton/probes/traps/001-tiff-predictor/README.md +75 -0
  55. argleton/probes/traps/001-tiff-predictor/build.py +58 -0
  56. argleton/probes/traps/001-tiff-predictor/probe.toml +57 -0
  57. argleton/probes/traps/002-feet-as-metres/README.md +79 -0
  58. argleton/probes/traps/002-feet-as-metres/build.py +44 -0
  59. argleton/probes/traps/002-feet-as-metres/probe.toml +70 -0
  60. argleton/probes/traps/003-nodata-in-statistics/README.md +61 -0
  61. argleton/probes/traps/003-nodata-in-statistics/build.py +51 -0
  62. argleton/probes/traps/003-nodata-in-statistics/probe.toml +62 -0
  63. argleton/probes/traps/004-mismatched-crs-join/README.md +65 -0
  64. argleton/probes/traps/004-mismatched-crs-join/build.py +65 -0
  65. argleton/probes/traps/004-mismatched-crs-join/probe.toml +75 -0
  66. argleton/probes/traps/005-bowtie-area/README.md +63 -0
  67. argleton/probes/traps/005-bowtie-area/build.py +41 -0
  68. argleton/probes/traps/005-bowtie-area/probe.toml +73 -0
  69. argleton/probes/traps/006-default-layer/README.md +65 -0
  70. argleton/probes/traps/006-default-layer/build.py +55 -0
  71. argleton/probes/traps/006-default-layer/probe.toml +63 -0
  72. argleton/probes/traps/007-buffer-in-degrees/README.md +57 -0
  73. argleton/probes/traps/007-buffer-in-degrees/build.py +42 -0
  74. argleton/probes/traps/007-buffer-in-degrees/probe.toml +64 -0
  75. argleton/probes/traps/008-web-mercator-area/README.md +55 -0
  76. argleton/probes/traps/008-web-mercator-area/build.py +39 -0
  77. argleton/probes/traps/008-web-mercator-area/probe.toml +68 -0
  78. argleton/probes/traps/009-resampled-classes/README.md +88 -0
  79. argleton/probes/traps/009-resampled-classes/build.py +46 -0
  80. argleton/probes/traps/009-resampled-classes/probe.toml +76 -0
  81. argleton/probes/traps/010-scale-offset/README.md +76 -0
  82. argleton/probes/traps/010-scale-offset/build.py +55 -0
  83. argleton/probes/traps/010-scale-offset/probe.toml +69 -0
  84. argleton/probes/traps/011-polygon-holes/README.md +47 -0
  85. argleton/probes/traps/011-polygon-holes/build.py +55 -0
  86. argleton/probes/traps/011-polygon-holes/probe.toml +59 -0
  87. argleton/probes/traps/012-double-counting/README.md +40 -0
  88. argleton/probes/traps/012-double-counting/build.py +58 -0
  89. argleton/probes/traps/012-double-counting/probe.toml +52 -0
  90. argleton/probes/traps/013-z-dimension/README.md +42 -0
  91. argleton/probes/traps/013-z-dimension/build.py +43 -0
  92. argleton/probes/traps/013-z-dimension/probe.toml +53 -0
  93. argleton/probes/traps/014-centroid-outside/README.md +42 -0
  94. argleton/probes/traps/014-centroid-outside/build.py +72 -0
  95. argleton/probes/traps/014-centroid-outside/probe.toml +59 -0
  96. argleton/probes/traps/015-boundary-semantics/README.md +41 -0
  97. argleton/probes/traps/015-boundary-semantics/build.py +74 -0
  98. argleton/probes/traps/015-boundary-semantics/probe.toml +57 -0
  99. argleton/probes/traps/016-coordinate-parsing/README.md +40 -0
  100. argleton/probes/traps/016-coordinate-parsing/build.py +36 -0
  101. argleton/probes/traps/016-coordinate-parsing/probe.toml +52 -0
  102. argleton/probes/traps/017-aggregation-weighting/README.md +42 -0
  103. argleton/probes/traps/017-aggregation-weighting/build.py +62 -0
  104. argleton/probes/traps/017-aggregation-weighting/probe.toml +53 -0
  105. argleton/probes/traps/018-join-key-typing/README.md +41 -0
  106. argleton/probes/traps/018-join-key-typing/build.py +61 -0
  107. argleton/probes/traps/018-join-key-typing/probe.toml +53 -0
  108. argleton/probes/traps/019-partial-overlap/README.md +43 -0
  109. argleton/probes/traps/019-partial-overlap/build.py +66 -0
  110. argleton/probes/traps/019-partial-overlap/probe.toml +53 -0
  111. argleton/probes/traps/020-join-cardinality/README.md +42 -0
  112. argleton/probes/traps/020-join-cardinality/build.py +60 -0
  113. argleton/probes/traps/020-join-cardinality/probe.toml +51 -0
  114. argleton/probes/traps/021-ballpark-datum/README.md +100 -0
  115. argleton/probes/traps/021-ballpark-datum/build.py +44 -0
  116. argleton/probes/traps/021-ballpark-datum/probe.toml +92 -0
  117. argleton/published.py +49 -0
  118. argleton/run.py +170 -0
  119. argleton/score.py +150 -0
  120. argleton-0.1.0.dist-info/METADATA +275 -0
  121. argleton-0.1.0.dist-info/RECORD +124 -0
  122. argleton-0.1.0.dist-info/WHEEL +4 -0
  123. argleton-0.1.0.dist-info/entry_points.txt +2 -0
  124. 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))