lamen 0.3.0a1__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.
- lamen-0.3.0a1/PKG-INFO +181 -0
- lamen-0.3.0a1/README.md +163 -0
- lamen-0.3.0a1/lamen/__init__.py +26 -0
- lamen-0.3.0a1/lamen/_version.py +1 -0
- lamen-0.3.0a1/lamen/athena.py +56 -0
- lamen-0.3.0a1/lamen/client.py +81 -0
- lamen-0.3.0a1/lamen/errors.py +46 -0
- lamen-0.3.0a1/lamen/geocoding.py +87 -0
- lamen-0.3.0a1/lamen/http.py +490 -0
- lamen-0.3.0a1/lamen/messaging.py +469 -0
- lamen-0.3.0a1/lamen/pagination.py +18 -0
- lamen-0.3.0a1/lamen/storage.py +149 -0
- lamen-0.3.0a1/lamen/stream.py +40 -0
- lamen-0.3.0a1/lamen/types.py +277 -0
- lamen-0.3.0a1/pyproject.toml +24 -0
lamen-0.3.0a1/PKG-INFO
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lamen
|
|
3
|
+
Version: 0.3.0a1
|
|
4
|
+
Summary: Official Python SDK for Lamen
|
|
5
|
+
Author: Lamen
|
|
6
|
+
Author-email: dev@lamen.dev
|
|
7
|
+
Requires-Python: >=3.10,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Requires-Dist: httpx (>=0.25)
|
|
15
|
+
Requires-Dist: pydantic[email] (>=2.0)
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# Lamen Python SDK
|
|
19
|
+
|
|
20
|
+
🚧 Alpha Release
|
|
21
|
+
|
|
22
|
+
The Lamen Python SDK is currently in active development and may change between versions.
|
|
23
|
+
|
|
24
|
+
It is suitable for controlled testing and internal integrations, but is NOT yet recommended for general public production use.
|
|
25
|
+
|
|
26
|
+
If you are integrating Lamen infrastructure, create an account at https://www.lamen.dev and join the community via the Support section in the dashboard.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Overview
|
|
31
|
+
|
|
32
|
+
The Lamen Python SDK is a backend / machine-to-machine client for interacting with Lamen infrastructure services.
|
|
33
|
+
|
|
34
|
+
It provides API-key-authenticated access to:
|
|
35
|
+
|
|
36
|
+
• Messaging — send events, direct send, register device identities, register email identities, manage web tokens
|
|
37
|
+
• Storage — presigned upload, confirm upload, download URL, list objects, folders, privacy, delete
|
|
38
|
+
• Streaming / CDN — signed streaming and playback URLs
|
|
39
|
+
• Athena — ingest logic and analytics events
|
|
40
|
+
• Geocoding — structured location search
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Authentication
|
|
45
|
+
|
|
46
|
+
All operations require a valid Lamen API key.
|
|
47
|
+
|
|
48
|
+
Authentication is handled automatically using the `x-api-key` header.
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
|
|
52
|
+
from lamen import Lamen
|
|
53
|
+
|
|
54
|
+
lamen = Lamen(api_key="lam_sk_live_...")
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Quick Start (Sync)
|
|
59
|
+
|
|
60
|
+
from lamen import Lamen
|
|
61
|
+
|
|
62
|
+
lamen = Lamen(api_key="lam_sk_live_...")
|
|
63
|
+
|
|
64
|
+
response = lamen.messaging.post_event(
|
|
65
|
+
event="user_signed_up",
|
|
66
|
+
external_user_id="user_123",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
print(response)
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Quick Start (Async)
|
|
74
|
+
|
|
75
|
+
import asyncio
|
|
76
|
+
from datetime import datetime, timezone
|
|
77
|
+
from lamen import AsyncLamen
|
|
78
|
+
|
|
79
|
+
async def main():
|
|
80
|
+
async with AsyncLamen(api_key="lam_sk_live_...") as lamen:
|
|
81
|
+
await lamen.athena.ingest_event(
|
|
82
|
+
external_user_id="user_123",
|
|
83
|
+
event_name="signed_up",
|
|
84
|
+
occurred_at=datetime.now(timezone.utc),
|
|
85
|
+
properties={"plan": "free"},
|
|
86
|
+
idempotency_key="athena:signed_up:user_123:2026-02-01",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
asyncio.run(main())
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Example: Storage Upload Flow
|
|
94
|
+
|
|
95
|
+
from lamen import Lamen
|
|
96
|
+
|
|
97
|
+
lamen = Lamen(api_key="lam_sk_live_...")
|
|
98
|
+
|
|
99
|
+
upload = lamen.storage.create_upload_url(
|
|
100
|
+
filename="image.jpg",
|
|
101
|
+
content_type="image/jpeg",
|
|
102
|
+
size_bytes=1024,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
print(upload.uploadUrl)
|
|
106
|
+
|
|
107
|
+
# Upload the file to upload.uploadUrl using your HTTP client
|
|
108
|
+
|
|
109
|
+
confirmed = lamen.storage.confirm_upload(object_id=upload.object_id)
|
|
110
|
+
|
|
111
|
+
print(confirmed)
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Example: Streaming URL
|
|
116
|
+
|
|
117
|
+
stream = lamen.stream.get_stream_url(
|
|
118
|
+
object_id="your-object-uuid",
|
|
119
|
+
expiry_in_seconds=3600,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
print(stream.url)
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Error Handling
|
|
127
|
+
|
|
128
|
+
The SDK raises typed exceptions:
|
|
129
|
+
|
|
130
|
+
AuthenticationError
|
|
131
|
+
BillingError
|
|
132
|
+
ValidationError
|
|
133
|
+
RateLimitError
|
|
134
|
+
TimeoutError
|
|
135
|
+
NetworkError
|
|
136
|
+
ApiError
|
|
137
|
+
|
|
138
|
+
Example:
|
|
139
|
+
|
|
140
|
+
from lamen import ValidationError
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
lamen.messaging.post_event(event="")
|
|
144
|
+
except ValidationError as e:
|
|
145
|
+
print("Invalid request:", e)
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Retries & Idempotency
|
|
150
|
+
|
|
151
|
+
Retries are automatically enabled for transient failures:
|
|
152
|
+
|
|
153
|
+
408, 409, 425, 429, and 5xx responses.
|
|
154
|
+
|
|
155
|
+
For POST, PATCH, and PUT requests, retries are only enabled if an idempotency_key is provided.
|
|
156
|
+
|
|
157
|
+
Example:
|
|
158
|
+
|
|
159
|
+
from datetime import datetime, timezone
|
|
160
|
+
|
|
161
|
+
lamen.athena.ingest_event(
|
|
162
|
+
external_user_id="user_123",
|
|
163
|
+
event_name="purchase",
|
|
164
|
+
occurred_at=datetime.now(timezone.utc),
|
|
165
|
+
idempotency_key="purchase:user_123:1234",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Status
|
|
171
|
+
|
|
172
|
+
Current version: BETA Testing.
|
|
173
|
+
|
|
174
|
+
Breaking changes may occur before version 1.0.0.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
Proprietary — © Lamen
|
|
181
|
+
|
lamen-0.3.0a1/README.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Lamen Python SDK
|
|
2
|
+
|
|
3
|
+
🚧 Alpha Release
|
|
4
|
+
|
|
5
|
+
The Lamen Python SDK is currently in active development and may change between versions.
|
|
6
|
+
|
|
7
|
+
It is suitable for controlled testing and internal integrations, but is NOT yet recommended for general public production use.
|
|
8
|
+
|
|
9
|
+
If you are integrating Lamen infrastructure, create an account at https://www.lamen.dev and join the community via the Support section in the dashboard.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
The Lamen Python SDK is a backend / machine-to-machine client for interacting with Lamen infrastructure services.
|
|
16
|
+
|
|
17
|
+
It provides API-key-authenticated access to:
|
|
18
|
+
|
|
19
|
+
• Messaging — send events, direct send, register device identities, register email identities, manage web tokens
|
|
20
|
+
• Storage — presigned upload, confirm upload, download URL, list objects, folders, privacy, delete
|
|
21
|
+
• Streaming / CDN — signed streaming and playback URLs
|
|
22
|
+
• Athena — ingest logic and analytics events
|
|
23
|
+
• Geocoding — structured location search
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Authentication
|
|
28
|
+
|
|
29
|
+
All operations require a valid Lamen API key.
|
|
30
|
+
|
|
31
|
+
Authentication is handled automatically using the `x-api-key` header.
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
|
|
35
|
+
from lamen import Lamen
|
|
36
|
+
|
|
37
|
+
lamen = Lamen(api_key="lam_sk_live_...")
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Quick Start (Sync)
|
|
42
|
+
|
|
43
|
+
from lamen import Lamen
|
|
44
|
+
|
|
45
|
+
lamen = Lamen(api_key="lam_sk_live_...")
|
|
46
|
+
|
|
47
|
+
response = lamen.messaging.post_event(
|
|
48
|
+
event="user_signed_up",
|
|
49
|
+
external_user_id="user_123",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
print(response)
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Quick Start (Async)
|
|
57
|
+
|
|
58
|
+
import asyncio
|
|
59
|
+
from datetime import datetime, timezone
|
|
60
|
+
from lamen import AsyncLamen
|
|
61
|
+
|
|
62
|
+
async def main():
|
|
63
|
+
async with AsyncLamen(api_key="lam_sk_live_...") as lamen:
|
|
64
|
+
await lamen.athena.ingest_event(
|
|
65
|
+
external_user_id="user_123",
|
|
66
|
+
event_name="signed_up",
|
|
67
|
+
occurred_at=datetime.now(timezone.utc),
|
|
68
|
+
properties={"plan": "free"},
|
|
69
|
+
idempotency_key="athena:signed_up:user_123:2026-02-01",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
asyncio.run(main())
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Example: Storage Upload Flow
|
|
77
|
+
|
|
78
|
+
from lamen import Lamen
|
|
79
|
+
|
|
80
|
+
lamen = Lamen(api_key="lam_sk_live_...")
|
|
81
|
+
|
|
82
|
+
upload = lamen.storage.create_upload_url(
|
|
83
|
+
filename="image.jpg",
|
|
84
|
+
content_type="image/jpeg",
|
|
85
|
+
size_bytes=1024,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
print(upload.uploadUrl)
|
|
89
|
+
|
|
90
|
+
# Upload the file to upload.uploadUrl using your HTTP client
|
|
91
|
+
|
|
92
|
+
confirmed = lamen.storage.confirm_upload(object_id=upload.object_id)
|
|
93
|
+
|
|
94
|
+
print(confirmed)
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Example: Streaming URL
|
|
99
|
+
|
|
100
|
+
stream = lamen.stream.get_stream_url(
|
|
101
|
+
object_id="your-object-uuid",
|
|
102
|
+
expiry_in_seconds=3600,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
print(stream.url)
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Error Handling
|
|
110
|
+
|
|
111
|
+
The SDK raises typed exceptions:
|
|
112
|
+
|
|
113
|
+
AuthenticationError
|
|
114
|
+
BillingError
|
|
115
|
+
ValidationError
|
|
116
|
+
RateLimitError
|
|
117
|
+
TimeoutError
|
|
118
|
+
NetworkError
|
|
119
|
+
ApiError
|
|
120
|
+
|
|
121
|
+
Example:
|
|
122
|
+
|
|
123
|
+
from lamen import ValidationError
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
lamen.messaging.post_event(event="")
|
|
127
|
+
except ValidationError as e:
|
|
128
|
+
print("Invalid request:", e)
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Retries & Idempotency
|
|
133
|
+
|
|
134
|
+
Retries are automatically enabled for transient failures:
|
|
135
|
+
|
|
136
|
+
408, 409, 425, 429, and 5xx responses.
|
|
137
|
+
|
|
138
|
+
For POST, PATCH, and PUT requests, retries are only enabled if an idempotency_key is provided.
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
|
|
142
|
+
from datetime import datetime, timezone
|
|
143
|
+
|
|
144
|
+
lamen.athena.ingest_event(
|
|
145
|
+
external_user_id="user_123",
|
|
146
|
+
event_name="purchase",
|
|
147
|
+
occurred_at=datetime.now(timezone.utc),
|
|
148
|
+
idempotency_key="purchase:user_123:1234",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Status
|
|
154
|
+
|
|
155
|
+
Current version: BETA Testing.
|
|
156
|
+
|
|
157
|
+
Breaking changes may occur before version 1.0.0.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
Proprietary — © Lamen
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from .client import Lamen, AsyncLamen
|
|
2
|
+
from .errors import (
|
|
3
|
+
ApiError,
|
|
4
|
+
AuthenticationError,
|
|
5
|
+
BillingError,
|
|
6
|
+
NetworkError,
|
|
7
|
+
RateLimitError,
|
|
8
|
+
TimeoutError,
|
|
9
|
+
ValidationError,
|
|
10
|
+
)
|
|
11
|
+
from ._version import __version__
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"Lamen",
|
|
16
|
+
"AsyncLamen",
|
|
17
|
+
"__version__",
|
|
18
|
+
"ApiError",
|
|
19
|
+
"AuthenticationError",
|
|
20
|
+
"BillingError",
|
|
21
|
+
"NetworkError",
|
|
22
|
+
"RateLimitError",
|
|
23
|
+
"TimeoutError",
|
|
24
|
+
"ValidationError",
|
|
25
|
+
]
|
|
26
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0a1"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
from .http import HttpClient, AsyncHttpClient
|
|
6
|
+
from .types import AthenaLogicEventIngest, AthenaIngestResponse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AthenaClient:
|
|
10
|
+
"""Athena ingest (API key): POST /athena/api/events"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, http: HttpClient):
|
|
13
|
+
self._http = http
|
|
14
|
+
|
|
15
|
+
def ingest_event(
|
|
16
|
+
self,
|
|
17
|
+
*,
|
|
18
|
+
external_user_id: str,
|
|
19
|
+
event_name: str,
|
|
20
|
+
occurred_at: datetime,
|
|
21
|
+
properties: Optional[Dict[str, Any]] = None,
|
|
22
|
+
idempotency_key: Optional[str] = None,
|
|
23
|
+
) -> AthenaIngestResponse:
|
|
24
|
+
payload = AthenaLogicEventIngest(
|
|
25
|
+
external_user_id=external_user_id,
|
|
26
|
+
event_name=event_name,
|
|
27
|
+
occurred_at=occurred_at,
|
|
28
|
+
properties=properties,
|
|
29
|
+
idempotency_key=idempotency_key,
|
|
30
|
+
)
|
|
31
|
+
data = self._http.post("/athena/api/events", json=payload.model_dump())
|
|
32
|
+
return AthenaIngestResponse.model_validate(data)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AsyncAthenaClient:
|
|
36
|
+
def __init__(self, http: AsyncHttpClient):
|
|
37
|
+
self._http = http
|
|
38
|
+
|
|
39
|
+
async def ingest_event(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
external_user_id: str,
|
|
43
|
+
event_name: str,
|
|
44
|
+
occurred_at: datetime,
|
|
45
|
+
properties: Optional[Dict[str, Any]] = None,
|
|
46
|
+
idempotency_key: Optional[str] = None,
|
|
47
|
+
) -> AthenaIngestResponse:
|
|
48
|
+
payload = AthenaLogicEventIngest(
|
|
49
|
+
external_user_id=external_user_id,
|
|
50
|
+
event_name=event_name,
|
|
51
|
+
occurred_at=occurred_at,
|
|
52
|
+
properties=properties,
|
|
53
|
+
idempotency_key=idempotency_key,
|
|
54
|
+
)
|
|
55
|
+
data = await self._http.post("/athena/api/events", json=payload.model_dump())
|
|
56
|
+
return AthenaIngestResponse.model_validate(data)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .http import DEFAULT_BASE_URL, DEFAULT_TIMEOUT_S, HttpClient, AsyncHttpClient, RetryConfig
|
|
4
|
+
from .storage import StorageClient, AsyncStorageClient
|
|
5
|
+
from .stream import StreamClient, AsyncStreamClient
|
|
6
|
+
from .athena import AthenaClient, AsyncAthenaClient
|
|
7
|
+
from .messaging import MessagingClient, AsyncMessagingClient
|
|
8
|
+
from .geocoding import GeocodeClient, AsyncGeocodeClient
|
|
9
|
+
from ._version import __version__
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Lamen:
|
|
13
|
+
"""Top-level SDK client (sync)."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
*,
|
|
18
|
+
api_key: str,
|
|
19
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
20
|
+
timeout_s: float = DEFAULT_TIMEOUT_S,
|
|
21
|
+
user_agent: str = f"lamen-python/{__version__}",
|
|
22
|
+
retry: RetryConfig | None = None,
|
|
23
|
+
):
|
|
24
|
+
self._http = HttpClient(
|
|
25
|
+
api_key=api_key,
|
|
26
|
+
base_url=base_url,
|
|
27
|
+
timeout_s=timeout_s,
|
|
28
|
+
user_agent=user_agent,
|
|
29
|
+
retry=retry,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
self.storage = StorageClient(self._http)
|
|
33
|
+
self.stream = StreamClient(self._http)
|
|
34
|
+
self.athena = AthenaClient(self._http)
|
|
35
|
+
self.messaging = MessagingClient(self._http)
|
|
36
|
+
self.geocode = GeocodeClient(self._http)
|
|
37
|
+
|
|
38
|
+
def close(self) -> None:
|
|
39
|
+
self._http.close()
|
|
40
|
+
|
|
41
|
+
def __enter__(self) -> "Lamen":
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
45
|
+
self.close()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AsyncLamen:
|
|
49
|
+
"""Top-level SDK client (async)."""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
*,
|
|
54
|
+
api_key: str,
|
|
55
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
56
|
+
timeout_s: float = DEFAULT_TIMEOUT_S,
|
|
57
|
+
user_agent: str = f"lamen-python/{__version__}",
|
|
58
|
+
retry: RetryConfig | None = None,
|
|
59
|
+
):
|
|
60
|
+
self._http = AsyncHttpClient(
|
|
61
|
+
api_key=api_key,
|
|
62
|
+
base_url=base_url,
|
|
63
|
+
timeout_s=timeout_s,
|
|
64
|
+
user_agent=user_agent,
|
|
65
|
+
retry=retry,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
self.storage = AsyncStorageClient(self._http)
|
|
69
|
+
self.stream = AsyncStreamClient(self._http)
|
|
70
|
+
self.athena = AsyncAthenaClient(self._http)
|
|
71
|
+
self.messaging = AsyncMessagingClient(self._http)
|
|
72
|
+
self.geocode = AsyncGeocodeClient(self._http)
|
|
73
|
+
|
|
74
|
+
async def aclose(self) -> None:
|
|
75
|
+
await self._http.aclose()
|
|
76
|
+
|
|
77
|
+
async def __aenter__(self) -> "AsyncLamen":
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
async def __aexit__(self, exc_type, exc, tb) -> None:
|
|
81
|
+
await self.aclose()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LamenError(Exception):
|
|
8
|
+
"""Base exception for the SDK."""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ApiError(LamenError):
|
|
13
|
+
status_code: int
|
|
14
|
+
message: str
|
|
15
|
+
method: str
|
|
16
|
+
path: str
|
|
17
|
+
response_json: Optional[dict] = None
|
|
18
|
+
request_id: Optional[str] = None
|
|
19
|
+
|
|
20
|
+
def __str__(self) -> str:
|
|
21
|
+
rid = f" request_id={self.request_id}" if self.request_id else ""
|
|
22
|
+
return f"ApiError({self.status_code}) {self.method} {self.path}: {self.message}{rid}"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AuthenticationError(LamenError):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BillingError(LamenError):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ValidationError(LamenError):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class RateLimitError(LamenError):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TimeoutError(LamenError):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class NetworkError(LamenError):
|
|
46
|
+
pass
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
from .http import HttpClient, AsyncHttpClient
|
|
5
|
+
from .types import GeocodeResult
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
class GeocodeClient:
|
|
10
|
+
"""Geocoding client (sync)."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, http: HttpClient, *, logger: logging.Logger | None = None):
|
|
13
|
+
self._http = http
|
|
14
|
+
self._logger = logger or logging.getLogger("lamen.geocode")
|
|
15
|
+
|
|
16
|
+
def search(
|
|
17
|
+
self,
|
|
18
|
+
*,
|
|
19
|
+
query: str,
|
|
20
|
+
limit: int = 10,
|
|
21
|
+
country: Optional[str] = None,
|
|
22
|
+
) -> List[Dict[str, Any]]:
|
|
23
|
+
if not query or not query.strip():
|
|
24
|
+
raise ValueError("Query cannot be empty")
|
|
25
|
+
|
|
26
|
+
params: Dict[str, Any] = {
|
|
27
|
+
"q": query,
|
|
28
|
+
"limit": limit,
|
|
29
|
+
}
|
|
30
|
+
if country:
|
|
31
|
+
params["country"] = country
|
|
32
|
+
|
|
33
|
+
data = self._http.get("/geocode/search", params=params)
|
|
34
|
+
|
|
35
|
+
if isinstance(data, dict) and "value" in data:
|
|
36
|
+
data = data["value"]
|
|
37
|
+
|
|
38
|
+
if isinstance(data, str):
|
|
39
|
+
data = json.loads(data)
|
|
40
|
+
|
|
41
|
+
if not isinstance(data, list):
|
|
42
|
+
raise ValueError("Unexpected geocode response shape")
|
|
43
|
+
|
|
44
|
+
# Now it MUST be a list of dicts
|
|
45
|
+
return [GeocodeResult(**item) for item in data]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AsyncGeocodeClient:
|
|
49
|
+
"""Geocoding client (async)."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, http: AsyncHttpClient, *, logger: logging.Logger | None = None):
|
|
52
|
+
self._http = http
|
|
53
|
+
self._logger = logger or logging.getLogger("lamen.geocode")
|
|
54
|
+
|
|
55
|
+
async def search(
|
|
56
|
+
self,
|
|
57
|
+
*,
|
|
58
|
+
query: str,
|
|
59
|
+
limit: int = 10,
|
|
60
|
+
country: Optional[str] = None,
|
|
61
|
+
) -> List[Dict[str, Any]]:
|
|
62
|
+
if not query or not query.strip():
|
|
63
|
+
raise ValueError("Query cannot be empty")
|
|
64
|
+
|
|
65
|
+
params: Dict[str, Any] = {
|
|
66
|
+
"q": query,
|
|
67
|
+
"limit": limit,
|
|
68
|
+
}
|
|
69
|
+
if country:
|
|
70
|
+
params["country"] = country
|
|
71
|
+
|
|
72
|
+
data = await self._http.get("/geocode/search", params=params)
|
|
73
|
+
|
|
74
|
+
# If Redis-wrapped
|
|
75
|
+
if isinstance(data, dict) and "value" in data:
|
|
76
|
+
data = data["value"]
|
|
77
|
+
|
|
78
|
+
# If JSON string
|
|
79
|
+
if isinstance(data, str):
|
|
80
|
+
data = json.loads(data)
|
|
81
|
+
|
|
82
|
+
if not isinstance(data, list):
|
|
83
|
+
raise ValueError("Unexpected geocode response shape")
|
|
84
|
+
|
|
85
|
+
# Now it MUST be a list of dicts
|
|
86
|
+
return [GeocodeResult(**item) for item in data]
|
|
87
|
+
|