geo-micro 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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Coby Williams
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,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: geo-micro
3
+ Version: 0.1.0
4
+ Summary: A zero-dependency micro-utility for fast GeoJSON sanitisation and spatial checks.
5
+ Author-email: Coby Williams <cobyw16@hotmail.com>
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Topic :: Scientific/Engineering :: GIS
12
+ License-File: LICENSE
13
+ Project-URL: Homepage, https://github.com/LionEmpire/geo-micro
14
+
15
+ # geo-micro
16
+
17
+ A zero-dependency, ultra-lightweight (<10KB) Python utility for GeoJSON sanitisation, coordinate order correction, bounding box calculations, and fast ray-casted point-in-polygon checks.
18
+ Designed for AWS Lambda functions, microservices, and edge computing environments where heavy C-based libraries like Shapely or GDAL are overkill.
19
+
20
+ ## Features
21
+
22
+ - Zero External Dependencies
23
+ - Dirty Data Repair (string coordinates to floats and auto-closes unclosed polygon rings)
24
+ - Coordinate Swapper: (`[Lat, Lng]` to `[Lng, Lat]` and vice versa)
25
+ - Point-in-Polygon
26
+
27
+ ## Quickstart
28
+
29
+ ```python
30
+ from geomicro import GeoMicro, convert_coords
31
+
32
+ # 1. Fix Lat/Lng order for a list of coordinates
33
+ bad_coords = [[-27.468, 153.028], [-27.470, 153.030]]
34
+ fixed = convert_coords(bad_coords, mode="to_geojson")
35
+
36
+ # 2. Check containment and bounding boxes
37
+ polygon_feature = {
38
+ "type": "Polygon",
39
+ "coordinates": [[[153.0, -27.0], [154.0, -27.0], [154.0, -28.0], [153.0, -27.0]]]
40
+ }
41
+
42
+ geo = GeoMicro(polygon_feature)
43
+ print(geo.bbox()) # (153.0, -28.0, 154.0, -27.0)
44
+ print(geo.contains([153.5, -27.5])) # True
45
+ ```
46
+
@@ -0,0 +1,31 @@
1
+ # geo-micro
2
+
3
+ A zero-dependency, ultra-lightweight (<10KB) Python utility for GeoJSON sanitisation, coordinate order correction, bounding box calculations, and fast ray-casted point-in-polygon checks.
4
+ Designed for AWS Lambda functions, microservices, and edge computing environments where heavy C-based libraries like Shapely or GDAL are overkill.
5
+
6
+ ## Features
7
+
8
+ - Zero External Dependencies
9
+ - Dirty Data Repair (string coordinates to floats and auto-closes unclosed polygon rings)
10
+ - Coordinate Swapper: (`[Lat, Lng]` to `[Lng, Lat]` and vice versa)
11
+ - Point-in-Polygon
12
+
13
+ ## Quickstart
14
+
15
+ ```python
16
+ from geomicro import GeoMicro, convert_coords
17
+
18
+ # 1. Fix Lat/Lng order for a list of coordinates
19
+ bad_coords = [[-27.468, 153.028], [-27.470, 153.030]]
20
+ fixed = convert_coords(bad_coords, mode="to_geojson")
21
+
22
+ # 2. Check containment and bounding boxes
23
+ polygon_feature = {
24
+ "type": "Polygon",
25
+ "coordinates": [[[153.0, -27.0], [154.0, -27.0], [154.0, -28.0], [153.0, -27.0]]]
26
+ }
27
+
28
+ geo = GeoMicro(polygon_feature)
29
+ print(geo.bbox()) # (153.0, -28.0, 154.0, -27.0)
30
+ print(geo.contains([153.5, -27.5])) # True
31
+ ```
@@ -0,0 +1,24 @@
1
+ from .core import (
2
+ sanitize_coord,
3
+ swap_lat_lng,
4
+ lat_lng_to_geojson,
5
+ geojson_to_lat_lng,
6
+ convert_coords,
7
+ auto_repair_ring,
8
+ get_bbox,
9
+ point_in_polygon,
10
+ GeoMicro,
11
+ )
12
+
13
+ __version__ = "0.1.0"
14
+ __all__ = [
15
+ "sanitize_coord",
16
+ "swap_lat_lng",
17
+ "lat_lng_to_geojson",
18
+ "geojson_to_lat_lng",
19
+ "convert_coords",
20
+ "auto_repair_ring",
21
+ "get_bbox",
22
+ "point_in_polygon",
23
+ "GeoMicro",
24
+ ]
@@ -0,0 +1,168 @@
1
+ """
2
+ geo-micro: Zero-dependency GeoJSON micro-utility.
3
+ Lightweight, pure-Python geometry sanitisation and spatial checks.
4
+ """
5
+
6
+ from typing import List, Tuple, Union, Dict, Any
7
+
8
+ PointType = Union[List[float], Tuple[float, float]]
9
+ BoundingBox = Tuple[float, float, float, float] # (min_lng, min_lat, max_lng, max_lat)
10
+
11
+
12
+ def sanitize_coord(coord: Any) -> Tuple[float, float]:
13
+ """Ensures coordinates are valid floats."""
14
+ try:
15
+ lng = float(coord[0])
16
+ lat = float(coord[1])
17
+ return lng, lat
18
+ except (IndexError, TypeError, ValueError) as e:
19
+ raise ValueError(f"Invalid coordinate format: {coord}") from e
20
+
21
+
22
+ def lat_lng_to_geojson(point: PointType) -> Tuple[float, float]:
23
+ """
24
+ Converts a single (Latitude, Longitude) pair into standard GeoJSON (Longitude, Latitude).
25
+ Example: (-27.468, 153.028) -> (153.028, -27.468)
26
+ """
27
+ lat, lng = sanitize_coord(point)
28
+ return lng, lat
29
+
30
+
31
+ def geojson_to_lat_lng(point: PointType) -> Tuple[float, float]:
32
+ """
33
+ Converts a single GeoJSON (Longitude, Latitude) pair into standard (Latitude, Longitude).
34
+ Example: (153.028, -27.468) -> (-27.468, 153.028)
35
+ """
36
+ lng, lat = sanitize_coord(point)
37
+ return lat, lng
38
+
39
+ def swap_lat_lng(coord: PointType) -> Tuple[float, float]:
40
+ """Swaps a (Lat, Lng) pair to (Lng, Lat) or vice versa."""
41
+ a, b = sanitize_coord(coord)
42
+ return b, a
43
+
44
+ def convert_coords(coords: List[Any], mode: str = "to_geojson") -> List[Any]:
45
+ """
46
+ Recursively converts coordinates across nested structures (arrays, rings, polygons).
47
+
48
+ :param coords: List of coordinate pairs or nested lists.
49
+ :param mode: 'to_geojson' (swaps Lat/Lng -> Lng/Lat) or 'to_latlng' (swaps Lng/Lat -> Lat/Lng).
50
+ """
51
+ if not coords:
52
+ return []
53
+ if isinstance(coords[0], (int, float, str)):
54
+ if mode == "to_geojson":
55
+ lng, lat = lat_lng_to_geojson(coords)
56
+ return [lng, lat]
57
+ elif mode == "to_latlng":
58
+ lat, lng = geojson_to_lat_lng(coords)
59
+ return [lat, lng]
60
+ else:
61
+ raise ValueError(f"Invalid mode: '{mode}'. Use 'to_geojson' or 'to_latlng'.")
62
+
63
+ return [convert_coords(c, mode=mode) for c in coords]
64
+
65
+
66
+ def auto_repair_ring(ring: List[PointType]) -> List[Tuple[float, float]]:
67
+ """
68
+ Cleans ring coordinates: casts string values to floats and ensures
69
+ the polygon ring is closed (first point equals last point).
70
+ """
71
+ if len(ring) < 3:
72
+ raise ValueError("A polygon ring must contain at least 3 points.")
73
+
74
+ clean_ring = [sanitize_coord(p) for p in ring]
75
+
76
+ # Close ring if open
77
+ if clean_ring[0] != clean_ring[-1]:
78
+ clean_ring.append(clean_ring[0])
79
+
80
+ return clean_ring
81
+
82
+
83
+ def get_bbox(geometry_or_geojson: Dict[str, Any]) -> BoundingBox:
84
+ """
85
+ Computes a (min_lng, min_lat, max_lng, max_lat) bounding box for any
86
+ supported GeoJSON object (Point, LineString, Polygon, Feature, FeatureCollection).
87
+ """
88
+ coords: List[Tuple[float, float]] = []
89
+
90
+ def _extract_coords(obj: Any):
91
+ if isinstance(obj, dict):
92
+ if obj.get("type") == "FeatureCollection":
93
+ for feat in obj.get("features", []):
94
+ _extract_coords(feat)
95
+ elif obj.get("type") == "Feature":
96
+ _extract_coords(obj.get("geometry", {}))
97
+ elif "coordinates" in obj:
98
+ _extract_coords(obj["coordinates"])
99
+ elif isinstance(obj, list):
100
+ if len(obj) >= 2 and isinstance(obj[0], (int, float, str)) and isinstance(obj[1], (int, float, str)):
101
+ coords.append(sanitize_coord(obj))
102
+ else:
103
+ for sub in obj:
104
+ _extract_coords(sub)
105
+
106
+ _extract_coords(geometry_or_geojson)
107
+
108
+ if not coords:
109
+ raise ValueError("No valid coordinates found to calculate bounding box.")
110
+
111
+ lngs = [c[0] for c in coords]
112
+ lats = [c[1] for c in coords]
113
+
114
+ return min(lngs), min(lats), max(lngs), max(lats)
115
+
116
+
117
+ def point_in_polygon(point: PointType, polygon_ring: List[PointType]) -> bool:
118
+ """
119
+ Determines if a point is inside a polygon ring using the Ray-Casting algorithm.
120
+ Expects coordinates in standard GeoJSON [Lng, Lat] format.
121
+ """
122
+ px, py = sanitize_coord(point)
123
+ ring = auto_repair_ring(polygon_ring)
124
+
125
+ inside = False
126
+ n = len(ring)
127
+
128
+ j = n - 1
129
+ for i in range(n):
130
+ xi, yi = ring[i]
131
+ xj, yj = ring[j]
132
+
133
+ # Ray-casting intersection check
134
+ intersect = ((yi > py) != (yj > py)) and (px < (xj - xi) * (py - yi) / (yj - yi + 1e-12) + xi)
135
+ if intersect:
136
+ inside = not inside
137
+ j = i
138
+
139
+ return inside
140
+
141
+
142
+ class GeoMicro:
143
+ """Convenience wrapper class for fluent chaining."""
144
+
145
+ def __init__(self, geojson: Dict[str, Any]):
146
+ self.geojson = geojson
147
+
148
+ def bbox(self) -> BoundingBox:
149
+ return get_bbox(self.geojson)
150
+
151
+ def contains(self, point: PointType) -> bool:
152
+ geom_type = self.geojson.get("type")
153
+
154
+ if geom_type == "Polygon":
155
+ coords = self.geojson.get("coordinates", [])
156
+ elif geom_type == "Feature":
157
+ coords = self.geojson.get("geometry", {}).get("coordinates", [])
158
+ else:
159
+ raise NotImplementedError(f"Point containment for type '{geom_type}' is not directly supported.")
160
+
161
+ if not coords:
162
+ raise ValueError("No coordinates found in GeoJSON object.")
163
+
164
+ outer_ring = coords[0]
165
+ if isinstance(outer_ring, list) and len(outer_ring) > 0 and isinstance(outer_ring[0], list) and isinstance(outer_ring[0][0], (list, tuple)):
166
+ outer_ring = outer_ring[0]
167
+
168
+ return point_in_polygon(point, outer_ring)
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["flit_core >=3.2,<4"]
3
+ build-backend = "flit_core.buildapi"
4
+
5
+ [project]
6
+ name = "geo-micro"
7
+ version = "0.1.0"
8
+ description = "A zero-dependency micro-utility for fast GeoJSON sanitisation and spatial checks."
9
+ readme = "README.md"
10
+ authors = [{ name = "Coby Williams", email = "cobyw16@hotmail.com" }]
11
+ license = { file = "LICENSE" }
12
+ classifiers = [
13
+ "Programming Language :: Python :: 3",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Operating System :: OS Independent",
16
+ "Topic :: Scientific/Engineering :: GIS",
17
+ ]
18
+ requires-python = ">=3.8"
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/LionEmpire/geo-micro"
22
+
23
+ [tool.pytest.ini_options]
24
+ pythonpath = "."
25
+ testpaths = ["tests"]
26
+
27
+ [tool.flit.module]
28
+ name = "geomicro"