geo-engine-python 0.1.3__tar.gz → 0.2.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.
- geo_engine_python-0.2.0/PKG-INFO +278 -0
- geo_engine_python-0.2.0/README.md +259 -0
- geo_engine_python-0.2.0/geo_engine_python.egg-info/PKG-INFO +278 -0
- geo_engine_python-0.2.0/geo_engine_python.egg-info/SOURCES.txt +20 -0
- geo_engine_python-0.2.0/geoengine/__init__.py +55 -0
- geo_engine_python-0.2.0/geoengine/_http.py +208 -0
- geo_engine_python-0.2.0/geoengine/_version.py +1 -0
- geo_engine_python-0.2.0/geoengine/client.py +122 -0
- geo_engine_python-0.2.0/geoengine/errors.py +146 -0
- geo_engine_python-0.2.0/geoengine/geoidentity.py +186 -0
- geo_engine_python-0.2.0/geoengine/ingester.py +73 -0
- geo_engine_python-0.2.0/geoengine/models.py +218 -0
- geo_engine_python-0.2.0/geoengine/spatial.py +75 -0
- geo_engine_python-0.2.0/geoengine/users.py +72 -0
- geo_engine_python-0.2.0/geoengine/validation.py +57 -0
- {geo_engine_python-0.1.3 → geo_engine_python-0.2.0}/pyproject.toml +6 -3
- {geo_engine_python-0.1.3 → geo_engine_python-0.2.0}/setup.py +2 -2
- geo_engine_python-0.2.0/tests/test_sdk.py +617 -0
- geo_engine_python-0.1.3/PKG-INFO +0 -122
- geo_engine_python-0.1.3/README.md +0 -103
- geo_engine_python-0.1.3/geo_engine_python.egg-info/PKG-INFO +0 -122
- geo_engine_python-0.1.3/geo_engine_python.egg-info/SOURCES.txt +0 -9
- geo_engine_python-0.1.3/geoengine/__init__.py +0 -1
- {geo_engine_python-0.1.3 → geo_engine_python-0.2.0}/geo_engine_python.egg-info/dependency_links.txt +0 -0
- {geo_engine_python-0.1.3 → geo_engine_python-0.2.0}/geo_engine_python.egg-info/requires.txt +0 -0
- {geo_engine_python-0.1.3 → geo_engine_python-0.2.0}/geo_engine_python.egg-info/top_level.txt +0 -0
- {geo_engine_python-0.1.3 → geo_engine_python-0.2.0}/setup.cfg +0 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: geo-engine-python
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: SDK oficial para Geo-Engine: rastreo, geocercas, validación espacial, usuarios y GeoIdentity
|
|
5
|
+
Home-page: https://github.com/AlexG695/geo-engine-python
|
|
6
|
+
Author: AlexG695
|
|
7
|
+
Author-email: AlexG <alexg@geoengine.dev>
|
|
8
|
+
Project-URL: Homepage, https://geoengine.dev
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/AlexG695/geo-engine/issues
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.7
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: requests>=2.25.0
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: home-page
|
|
18
|
+
Dynamic: requires-python
|
|
19
|
+
|
|
20
|
+
# 🐍 Geo-Engine Python SDK
|
|
21
|
+
|
|
22
|
+
> *Read this in Spanish: [README.es.md](./README.es.md)*
|
|
23
|
+
|
|
24
|
+
Official **Python** client for the **Geo-Engine** platform.
|
|
25
|
+
Send real-time locations, manage geofences, validate points, administer organization members, and define GeoIdentity access rules from **Django**, **Flask**, **FastAPI**, scripts, or **Data Science** environments.
|
|
26
|
+
|
|
27
|
+
Feature parity with the official Go SDK (`v2.1.0`), except for gRPC transport.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## ✨ Features
|
|
32
|
+
|
|
33
|
+
- 📍 **Real-time Ingestion:** Coordinate ingestion via HTTP REST.
|
|
34
|
+
- 🗺️ **Spatial Module:** Point-in-polygon validation against a single geofence or against all geofences.
|
|
35
|
+
- 👥 **Users Module:** Organization members, roles, and email invitations.
|
|
36
|
+
- 🛡️ **GeoIdentity Module:** Access rules (IP blacklists, geographic boundaries, Tor/datacenter blocking) assignable to users or to the entire organization.
|
|
37
|
+
- 🚧 **Geofences:** Creation with automatic GeoJSON polygon normalization.
|
|
38
|
+
- 🔄 **AsyncIngester:** Background queue with auto-flush by batch size and time interval.
|
|
39
|
+
- 🔐 **Security:** HMAC-SHA256 signature (`X-Signature`) with anti-replay timestamp and JWT Bearer token support.
|
|
40
|
+
- 🧯 **Typed Errors** for every business case.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 🚀 Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install geo-engine-python
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## ⚡ Quick Start
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from geoengine import GeoEngine
|
|
56
|
+
|
|
57
|
+
# Connects to production environment by default
|
|
58
|
+
client = GeoEngine(api_key="geo_live_123456")
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
res = client.send_location(device_id="truck-01", lat=19.4326, lng=-99.1332)
|
|
62
|
+
print(f"✅ Sent: {res}")
|
|
63
|
+
except Exception as e:
|
|
64
|
+
print(f"❌ Error: {e}")
|
|
65
|
+
finally:
|
|
66
|
+
client.close()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Also works as a context manager: `with GeoEngine("geo_live_...") as client: ...`
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 🗺️ Spatial Module
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from geoengine import GeofenceNotFoundError
|
|
77
|
+
|
|
78
|
+
# Against a specific geofence
|
|
79
|
+
try:
|
|
80
|
+
res = client.spatial.validate_point("geofence_123", 19.4326, -99.1332)
|
|
81
|
+
if res.is_inside():
|
|
82
|
+
print(f"Inside {res.geofence_name} ({res.geofence_id})")
|
|
83
|
+
print(res) # [INSIDE] geofence=geofence_123 (Downtown) at (19.432600, -99.133200)
|
|
84
|
+
except GeofenceNotFoundError:
|
|
85
|
+
print("The geofence does not exist")
|
|
86
|
+
|
|
87
|
+
# Against all active geofences in the organization
|
|
88
|
+
res = client.spatial.validate_general(19.4326, -99.1332)
|
|
89
|
+
if res.has_matches():
|
|
90
|
+
print(res.total_matches, res.geofence_names())
|
|
91
|
+
print(res.contains_geofence("CDMX Downtown"), res.find_match("CDMX Downtown"))
|
|
92
|
+
|
|
93
|
+
# Fluent query builder
|
|
94
|
+
single = client.spatial.check(19.4326, -99.1332).in_geofence("geofence_123").execute()
|
|
95
|
+
general = client.spatial.check(19.4326, -99.1332).in_all_geofences().execute()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 👥 Users Module
|
|
101
|
+
|
|
102
|
+
`client.users` manages members of the organization that owns the API key:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from geoengine import Roles, InvitationPendingError, LastAdminError
|
|
106
|
+
|
|
107
|
+
# List members and available roles
|
|
108
|
+
users = client.users.list()
|
|
109
|
+
roles = client.users.list_roles()
|
|
110
|
+
user = client.users.get("user_123")
|
|
111
|
+
|
|
112
|
+
# Invite a new member (GeoEngine sends the invitation email)
|
|
113
|
+
try:
|
|
114
|
+
inv = client.users.invite("ana@acme.com", Roles.VIEWER, lang="en") # "es" (default) | "en" | "pt"
|
|
115
|
+
print("Invitation link:", inv.invite_url)
|
|
116
|
+
except InvitationPendingError:
|
|
117
|
+
print("There is already a pending invitation for this email")
|
|
118
|
+
|
|
119
|
+
# Update a member's role or revoke their access
|
|
120
|
+
try:
|
|
121
|
+
client.users.update_role("user_123", Roles.DISPATCHER)
|
|
122
|
+
client.users.remove("user_123")
|
|
123
|
+
except LastAdminError:
|
|
124
|
+
print("The organization must retain at least one administrator")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 🛡️ GeoIdentity Module
|
|
130
|
+
|
|
131
|
+
`client.geoidentity` creates access rules and assigns them to a user or to the entire organization:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from geoengine import RuleAction, SubjectAssignment, SubjectType, polygon_from_lat_lng
|
|
135
|
+
|
|
136
|
+
area = polygon_from_lat_lng([
|
|
137
|
+
[19.40, -99.20], [19.40, -99.10], [19.50, -99.10], # [lat, lng]
|
|
138
|
+
])
|
|
139
|
+
|
|
140
|
+
rule = client.geoidentity.create_rule(
|
|
141
|
+
"CDMX Only",
|
|
142
|
+
action=RuleAction.BLOCK,
|
|
143
|
+
black_ip_list=["203.0.113.0/24"],
|
|
144
|
+
block_tor=True,
|
|
145
|
+
geo_limit=area,
|
|
146
|
+
assigned_subjects=[SubjectAssignment(SubjectType.USER, "user_123")],
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Query, update, and delete
|
|
150
|
+
rules = client.geoidentity.list_rules(is_active=True)
|
|
151
|
+
detail = client.geoidentity.get_rule(rule.id) # includes assigned_subjects
|
|
152
|
+
client.geoidentity.update_rule(rule.id, is_active=False, geo_limit=None) # None removes the geographic boundary
|
|
153
|
+
client.geoidentity.delete_rule(rule.id)
|
|
154
|
+
|
|
155
|
+
# Assign or unassign the rule
|
|
156
|
+
client.geoidentity.assign_to_user(rule.id, "user_456")
|
|
157
|
+
client.geoidentity.assign_to_organization(rule.id)
|
|
158
|
+
client.geoidentity.unassign_from_user(rule.id, "user_456")
|
|
159
|
+
|
|
160
|
+
# Rules that apply to a user (direct, by role, or organization-wide)
|
|
161
|
+
user_rules = client.geoidentity.list_user_rules("user_123")
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
> A rule created without `assigned_subjects` applies to the entire organization. A rule must always retain at least one subject: removing the last one raises `LastSubjectError` (deactivate or delete the rule instead).
|
|
165
|
+
>
|
|
166
|
+
> `update_rule` only sends the parameters you provide; unspecified parameters remain unchanged.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 🚧 Geofences
|
|
171
|
+
|
|
172
|
+
The SDK takes `[latitude, longitude]` pairs, converts them to GeoJSON, and closes the polygon:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
client.create_geofence(
|
|
176
|
+
"North Logistics Park",
|
|
177
|
+
[[19.4326, -99.1332], [19.4426, -99.1332], [19.4426, -99.1232]],
|
|
178
|
+
webhook_url="https://my-api.com/webhooks/geofences", # optional
|
|
179
|
+
)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 🔄 Asynchronous Ingestion (`AsyncIngester`)
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from geoengine import LocationPing
|
|
188
|
+
|
|
189
|
+
# 5000 pings queue; flushes every 100 items or every 0.5 s
|
|
190
|
+
ingester = client.create_async_ingester(buffer_size=5000, batch_size=100, flush_interval=0.5)
|
|
191
|
+
|
|
192
|
+
ok = ingester.enqueue(LocationPing("truck-45", 19.4326, -99.1332))
|
|
193
|
+
if not ok:
|
|
194
|
+
print("Queue full or ingester stopped")
|
|
195
|
+
|
|
196
|
+
# On service shutdown: flushes pending items and stops the background thread
|
|
197
|
+
ingester.stop()
|
|
198
|
+
client.close()
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Background sending errors are silently dropped.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## 🔧 Advanced Configuration
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
client = GeoEngine(
|
|
209
|
+
api_key="sk_test_...",
|
|
210
|
+
environment="test", # "live" (default) | "test"
|
|
211
|
+
jwt_token="my.jwt.token", # optional
|
|
212
|
+
timeout=5, # seconds
|
|
213
|
+
ingest_url="http://localhost:8080",
|
|
214
|
+
management_url="http://localhost:8081",
|
|
215
|
+
api_service_url="http://localhost:8082", # Spatial, Users, and GeoIdentity
|
|
216
|
+
# or a single URL for all three (local tests / mocks):
|
|
217
|
+
# base_url="http://localhost:8080",
|
|
218
|
+
)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## 🧯 Errors
|
|
224
|
+
|
|
225
|
+
All errors inherit from `GeoEngineError`.
|
|
226
|
+
|
|
227
|
+
| Error | When it occurs |
|
|
228
|
+
| :--- | :--- |
|
|
229
|
+
| `ValidationError` (also `ValueError`) and subclasses: `DeviceIDRequiredError`, `GeofenceIDRequiredError`, `InvalidLatitudeError`, `InvalidLongitudeError`, `InvalidCoordinatesError`, `UserIDRequiredError`, `RuleIDRequiredError`, `RuleNameRequiredError`, `EmailRequiredError`, `RoleRequiredError` | Invalid parameter; API is not called |
|
|
230
|
+
| `APIError` | Ingestion or management HTTP error (`status_code`, `api_message`) |
|
|
231
|
+
| `APIServiceError` | API service error; adds `code` and `details` |
|
|
232
|
+
| `GeofenceNotFoundError` | Geofence does not exist (Spatial, 204/404) |
|
|
233
|
+
| `UserNotFoundError` | `USER_NOT_FOUND`, `USER_REVOKED` |
|
|
234
|
+
| `RuleNotFoundError` | `RULE_NOT_FOUND` |
|
|
235
|
+
| `UserAlreadyExistsError` | `USER_ALREADY_EXISTS` |
|
|
236
|
+
| `InvitationPendingError` | `INVITATION_PENDING` |
|
|
237
|
+
| `LastAdminError` | `LAST_ADMIN` |
|
|
238
|
+
| `LastSubjectError` | `LAST_SUBJECT` |
|
|
239
|
+
| `AuthenticationError` / `ForbiddenError` / `QuotaExceededError` | 401 / 403 / 402 |
|
|
240
|
+
| `NetworkError`, `RequestTimeoutError` | Network failure or request timeout |
|
|
241
|
+
| `ClientClosedError` | Client used after calling `close()` |
|
|
242
|
+
|
|
243
|
+
The SDK does not automatically retry requests.
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## 🔐 Authentication
|
|
248
|
+
|
|
249
|
+
The SDK authenticates using **API Keys** and signs each request.
|
|
250
|
+
Keep them secret and **never expose them in frontend applications**.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## 🧪 Testing
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
python -m unittest discover -s tests -t .
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## 🧩 Compatibility
|
|
263
|
+
|
|
264
|
+
* Python **3.7+**
|
|
265
|
+
* Django, Flask, FastAPI, CLI scripts, Jupyter / Data Science
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## 📄 License
|
|
270
|
+
|
|
271
|
+
MIT © Geo-Engine
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## 🤝 Support
|
|
276
|
+
|
|
277
|
+
Have questions or suggestions?
|
|
278
|
+
Open an issue or contact us at **[support@geoengine.dev](mailto:support@geoengine.dev)**
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# 🐍 Geo-Engine Python SDK
|
|
2
|
+
|
|
3
|
+
> *Read this in Spanish: [README.es.md](./README.es.md)*
|
|
4
|
+
|
|
5
|
+
Official **Python** client for the **Geo-Engine** platform.
|
|
6
|
+
Send real-time locations, manage geofences, validate points, administer organization members, and define GeoIdentity access rules from **Django**, **Flask**, **FastAPI**, scripts, or **Data Science** environments.
|
|
7
|
+
|
|
8
|
+
Feature parity with the official Go SDK (`v2.1.0`), except for gRPC transport.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## ✨ Features
|
|
13
|
+
|
|
14
|
+
- 📍 **Real-time Ingestion:** Coordinate ingestion via HTTP REST.
|
|
15
|
+
- 🗺️ **Spatial Module:** Point-in-polygon validation against a single geofence or against all geofences.
|
|
16
|
+
- 👥 **Users Module:** Organization members, roles, and email invitations.
|
|
17
|
+
- 🛡️ **GeoIdentity Module:** Access rules (IP blacklists, geographic boundaries, Tor/datacenter blocking) assignable to users or to the entire organization.
|
|
18
|
+
- 🚧 **Geofences:** Creation with automatic GeoJSON polygon normalization.
|
|
19
|
+
- 🔄 **AsyncIngester:** Background queue with auto-flush by batch size and time interval.
|
|
20
|
+
- 🔐 **Security:** HMAC-SHA256 signature (`X-Signature`) with anti-replay timestamp and JWT Bearer token support.
|
|
21
|
+
- 🧯 **Typed Errors** for every business case.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 🚀 Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install geo-engine-python
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## ⚡ Quick Start
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from geoengine import GeoEngine
|
|
37
|
+
|
|
38
|
+
# Connects to production environment by default
|
|
39
|
+
client = GeoEngine(api_key="geo_live_123456")
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
res = client.send_location(device_id="truck-01", lat=19.4326, lng=-99.1332)
|
|
43
|
+
print(f"✅ Sent: {res}")
|
|
44
|
+
except Exception as e:
|
|
45
|
+
print(f"❌ Error: {e}")
|
|
46
|
+
finally:
|
|
47
|
+
client.close()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Also works as a context manager: `with GeoEngine("geo_live_...") as client: ...`
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 🗺️ Spatial Module
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from geoengine import GeofenceNotFoundError
|
|
58
|
+
|
|
59
|
+
# Against a specific geofence
|
|
60
|
+
try:
|
|
61
|
+
res = client.spatial.validate_point("geofence_123", 19.4326, -99.1332)
|
|
62
|
+
if res.is_inside():
|
|
63
|
+
print(f"Inside {res.geofence_name} ({res.geofence_id})")
|
|
64
|
+
print(res) # [INSIDE] geofence=geofence_123 (Downtown) at (19.432600, -99.133200)
|
|
65
|
+
except GeofenceNotFoundError:
|
|
66
|
+
print("The geofence does not exist")
|
|
67
|
+
|
|
68
|
+
# Against all active geofences in the organization
|
|
69
|
+
res = client.spatial.validate_general(19.4326, -99.1332)
|
|
70
|
+
if res.has_matches():
|
|
71
|
+
print(res.total_matches, res.geofence_names())
|
|
72
|
+
print(res.contains_geofence("CDMX Downtown"), res.find_match("CDMX Downtown"))
|
|
73
|
+
|
|
74
|
+
# Fluent query builder
|
|
75
|
+
single = client.spatial.check(19.4326, -99.1332).in_geofence("geofence_123").execute()
|
|
76
|
+
general = client.spatial.check(19.4326, -99.1332).in_all_geofences().execute()
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 👥 Users Module
|
|
82
|
+
|
|
83
|
+
`client.users` manages members of the organization that owns the API key:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from geoengine import Roles, InvitationPendingError, LastAdminError
|
|
87
|
+
|
|
88
|
+
# List members and available roles
|
|
89
|
+
users = client.users.list()
|
|
90
|
+
roles = client.users.list_roles()
|
|
91
|
+
user = client.users.get("user_123")
|
|
92
|
+
|
|
93
|
+
# Invite a new member (GeoEngine sends the invitation email)
|
|
94
|
+
try:
|
|
95
|
+
inv = client.users.invite("ana@acme.com", Roles.VIEWER, lang="en") # "es" (default) | "en" | "pt"
|
|
96
|
+
print("Invitation link:", inv.invite_url)
|
|
97
|
+
except InvitationPendingError:
|
|
98
|
+
print("There is already a pending invitation for this email")
|
|
99
|
+
|
|
100
|
+
# Update a member's role or revoke their access
|
|
101
|
+
try:
|
|
102
|
+
client.users.update_role("user_123", Roles.DISPATCHER)
|
|
103
|
+
client.users.remove("user_123")
|
|
104
|
+
except LastAdminError:
|
|
105
|
+
print("The organization must retain at least one administrator")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 🛡️ GeoIdentity Module
|
|
111
|
+
|
|
112
|
+
`client.geoidentity` creates access rules and assigns them to a user or to the entire organization:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from geoengine import RuleAction, SubjectAssignment, SubjectType, polygon_from_lat_lng
|
|
116
|
+
|
|
117
|
+
area = polygon_from_lat_lng([
|
|
118
|
+
[19.40, -99.20], [19.40, -99.10], [19.50, -99.10], # [lat, lng]
|
|
119
|
+
])
|
|
120
|
+
|
|
121
|
+
rule = client.geoidentity.create_rule(
|
|
122
|
+
"CDMX Only",
|
|
123
|
+
action=RuleAction.BLOCK,
|
|
124
|
+
black_ip_list=["203.0.113.0/24"],
|
|
125
|
+
block_tor=True,
|
|
126
|
+
geo_limit=area,
|
|
127
|
+
assigned_subjects=[SubjectAssignment(SubjectType.USER, "user_123")],
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Query, update, and delete
|
|
131
|
+
rules = client.geoidentity.list_rules(is_active=True)
|
|
132
|
+
detail = client.geoidentity.get_rule(rule.id) # includes assigned_subjects
|
|
133
|
+
client.geoidentity.update_rule(rule.id, is_active=False, geo_limit=None) # None removes the geographic boundary
|
|
134
|
+
client.geoidentity.delete_rule(rule.id)
|
|
135
|
+
|
|
136
|
+
# Assign or unassign the rule
|
|
137
|
+
client.geoidentity.assign_to_user(rule.id, "user_456")
|
|
138
|
+
client.geoidentity.assign_to_organization(rule.id)
|
|
139
|
+
client.geoidentity.unassign_from_user(rule.id, "user_456")
|
|
140
|
+
|
|
141
|
+
# Rules that apply to a user (direct, by role, or organization-wide)
|
|
142
|
+
user_rules = client.geoidentity.list_user_rules("user_123")
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
> A rule created without `assigned_subjects` applies to the entire organization. A rule must always retain at least one subject: removing the last one raises `LastSubjectError` (deactivate or delete the rule instead).
|
|
146
|
+
>
|
|
147
|
+
> `update_rule` only sends the parameters you provide; unspecified parameters remain unchanged.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 🚧 Geofences
|
|
152
|
+
|
|
153
|
+
The SDK takes `[latitude, longitude]` pairs, converts them to GeoJSON, and closes the polygon:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
client.create_geofence(
|
|
157
|
+
"North Logistics Park",
|
|
158
|
+
[[19.4326, -99.1332], [19.4426, -99.1332], [19.4426, -99.1232]],
|
|
159
|
+
webhook_url="https://my-api.com/webhooks/geofences", # optional
|
|
160
|
+
)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 🔄 Asynchronous Ingestion (`AsyncIngester`)
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from geoengine import LocationPing
|
|
169
|
+
|
|
170
|
+
# 5000 pings queue; flushes every 100 items or every 0.5 s
|
|
171
|
+
ingester = client.create_async_ingester(buffer_size=5000, batch_size=100, flush_interval=0.5)
|
|
172
|
+
|
|
173
|
+
ok = ingester.enqueue(LocationPing("truck-45", 19.4326, -99.1332))
|
|
174
|
+
if not ok:
|
|
175
|
+
print("Queue full or ingester stopped")
|
|
176
|
+
|
|
177
|
+
# On service shutdown: flushes pending items and stops the background thread
|
|
178
|
+
ingester.stop()
|
|
179
|
+
client.close()
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Background sending errors are silently dropped.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## 🔧 Advanced Configuration
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
client = GeoEngine(
|
|
190
|
+
api_key="sk_test_...",
|
|
191
|
+
environment="test", # "live" (default) | "test"
|
|
192
|
+
jwt_token="my.jwt.token", # optional
|
|
193
|
+
timeout=5, # seconds
|
|
194
|
+
ingest_url="http://localhost:8080",
|
|
195
|
+
management_url="http://localhost:8081",
|
|
196
|
+
api_service_url="http://localhost:8082", # Spatial, Users, and GeoIdentity
|
|
197
|
+
# or a single URL for all three (local tests / mocks):
|
|
198
|
+
# base_url="http://localhost:8080",
|
|
199
|
+
)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## 🧯 Errors
|
|
205
|
+
|
|
206
|
+
All errors inherit from `GeoEngineError`.
|
|
207
|
+
|
|
208
|
+
| Error | When it occurs |
|
|
209
|
+
| :--- | :--- |
|
|
210
|
+
| `ValidationError` (also `ValueError`) and subclasses: `DeviceIDRequiredError`, `GeofenceIDRequiredError`, `InvalidLatitudeError`, `InvalidLongitudeError`, `InvalidCoordinatesError`, `UserIDRequiredError`, `RuleIDRequiredError`, `RuleNameRequiredError`, `EmailRequiredError`, `RoleRequiredError` | Invalid parameter; API is not called |
|
|
211
|
+
| `APIError` | Ingestion or management HTTP error (`status_code`, `api_message`) |
|
|
212
|
+
| `APIServiceError` | API service error; adds `code` and `details` |
|
|
213
|
+
| `GeofenceNotFoundError` | Geofence does not exist (Spatial, 204/404) |
|
|
214
|
+
| `UserNotFoundError` | `USER_NOT_FOUND`, `USER_REVOKED` |
|
|
215
|
+
| `RuleNotFoundError` | `RULE_NOT_FOUND` |
|
|
216
|
+
| `UserAlreadyExistsError` | `USER_ALREADY_EXISTS` |
|
|
217
|
+
| `InvitationPendingError` | `INVITATION_PENDING` |
|
|
218
|
+
| `LastAdminError` | `LAST_ADMIN` |
|
|
219
|
+
| `LastSubjectError` | `LAST_SUBJECT` |
|
|
220
|
+
| `AuthenticationError` / `ForbiddenError` / `QuotaExceededError` | 401 / 403 / 402 |
|
|
221
|
+
| `NetworkError`, `RequestTimeoutError` | Network failure or request timeout |
|
|
222
|
+
| `ClientClosedError` | Client used after calling `close()` |
|
|
223
|
+
|
|
224
|
+
The SDK does not automatically retry requests.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## 🔐 Authentication
|
|
229
|
+
|
|
230
|
+
The SDK authenticates using **API Keys** and signs each request.
|
|
231
|
+
Keep them secret and **never expose them in frontend applications**.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## 🧪 Testing
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
python -m unittest discover -s tests -t .
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## 🧩 Compatibility
|
|
244
|
+
|
|
245
|
+
* Python **3.7+**
|
|
246
|
+
* Django, Flask, FastAPI, CLI scripts, Jupyter / Data Science
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## 📄 License
|
|
251
|
+
|
|
252
|
+
MIT © Geo-Engine
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## 🤝 Support
|
|
257
|
+
|
|
258
|
+
Have questions or suggestions?
|
|
259
|
+
Open an issue or contact us at **[support@geoengine.dev](mailto:support@geoengine.dev)**
|