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 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
+
@@ -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
+