lumera 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.
lumera-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: lumera
3
+ Version: 0.1.0
4
+ Summary: Lumera library
5
+ Requires-Python: >=3.8
6
+ Requires-Dist: requests
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: lumera
3
+ Version: 0.1.0
4
+ Summary: Lumera library
5
+ Requires-Python: >=3.8
6
+ Requires-Dist: requests
@@ -0,0 +1,7 @@
1
+ lumera.py
2
+ pyproject.toml
3
+ lumera.egg-info/PKG-INFO
4
+ lumera.egg-info/SOURCES.txt
5
+ lumera.egg-info/dependency_links.txt
6
+ lumera.egg-info/requires.txt
7
+ lumera.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ requests
@@ -0,0 +1 @@
1
+ lumera
lumera-0.1.0/lumera.py ADDED
@@ -0,0 +1,229 @@
1
+ """Minimal client helpers to interact with the Lumera HTTP API.
2
+
3
+ This module focuses on:
4
+ 1. Authenticating with the Lumera backend using the personal token kept in the
5
+ LUMERA_TOKEN environment variable (or /root/.env inside the container).
6
+ 2. Fetching OAuth / API-key access tokens for any connected provider via the
7
+ `/connections/{provider}/access-token` endpoint (Google, HubSpot, …).
8
+ 3. Convenience helpers for uploading local files as documents (existing logic).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import datetime as _dt
14
+ import mimetypes
15
+ import os
16
+ import pathlib
17
+ import time as _time
18
+ from typing import Dict, Tuple
19
+
20
+ import requests
21
+
22
+ BASE_URL = "https://app.lumerahq.com/api/documents"
23
+ API_BASE = "https://app.lumerahq.com/api"
24
+ TOKEN_ENV = "LUMERA_TOKEN"
25
+ ENV_PATH = "/root/.env"
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Internal helpers
30
+ # ---------------------------------------------------------------------------
31
+
32
+
33
+ def _ensure_token() -> str:
34
+ """Return the personal Lumera token, loading /root/.env if necessary."""
35
+
36
+ token = os.getenv(TOKEN_ENV)
37
+ if token:
38
+ return token
39
+
40
+ try:
41
+ with open(ENV_PATH) as fp:
42
+ for line in fp:
43
+ if line.strip().startswith("#") or "=" not in line:
44
+ continue
45
+ k, v = line.strip().split("=", 1)
46
+ if k.strip() == TOKEN_ENV:
47
+ token = v.strip().strip("'\"")
48
+ os.environ[TOKEN_ENV] = token # cache
49
+ return token
50
+ except FileNotFoundError:
51
+ pass
52
+
53
+ raise RuntimeError(
54
+ f"{TOKEN_ENV} environment variable not set and {ENV_PATH} not found"
55
+ )
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Provider-agnostic access-token retrieval
60
+ # ---------------------------------------------------------------------------
61
+
62
+
63
+ # _token_cache maps provider → (access_token, expiry_epoch_seconds). For tokens
64
+ # without an explicit expiry (e.g. API keys) we store `float('+inf')` so that
65
+ # they are never considered stale.
66
+ _token_cache: dict[str, Tuple[str, float]] = {}
67
+
68
+
69
+ def _parse_expiry(expires_at) -> float:
70
+ """Convert `expires_at` from the API (may be ISO8601 or epoch) to epoch seconds.
71
+
72
+ Returns +inf if `expires_at` is falsy/None.
73
+ """
74
+
75
+ if not expires_at:
76
+ return float("inf")
77
+
78
+ if isinstance(expires_at, (int, float)):
79
+ return float(expires_at)
80
+
81
+ # Assume RFC 3339 / ISO 8601 string.
82
+ if isinstance(expires_at, str):
83
+ if expires_at.endswith("Z"):
84
+ expires_at = expires_at[:-1] + "+00:00"
85
+ return _dt.datetime.fromisoformat(expires_at).timestamp()
86
+
87
+ raise TypeError(f"Unsupported expires_at format: {type(expires_at)!r}")
88
+
89
+
90
+ def _fetch_access_token(provider: str) -> Tuple[str, float]:
91
+ """Call the Lumera API to obtain a valid access token for *provider*."""
92
+
93
+ provider = provider.lower().strip()
94
+ if not provider:
95
+ raise ValueError("provider is required")
96
+
97
+ token = _ensure_token()
98
+
99
+ url = f"{API_BASE}/connections/{provider}/access-token"
100
+ headers = {"Authorization": f"token {token}"}
101
+
102
+ resp = requests.get(url, headers=headers, timeout=30)
103
+ resp.raise_for_status()
104
+
105
+ data = resp.json()
106
+ access_token = data.get("access_token")
107
+ expires_at = data.get("expires_at")
108
+
109
+ if not access_token:
110
+ raise RuntimeError(
111
+ f"Malformed response from Lumera when fetching {provider} access token"
112
+ )
113
+
114
+ expiry_ts = _parse_expiry(expires_at)
115
+ return access_token, expiry_ts
116
+
117
+
118
+ def get_access_token(provider: str, min_valid_seconds: int = 900) -> str:
119
+ """Return a cached access token for *provider* valid ≥ *min_valid_seconds*.
120
+
121
+ Automatically refreshes tokens via the Lumera API when they are missing or
122
+ close to expiry. For tokens without an expiry (API keys) the first value
123
+ is cached indefinitely.
124
+ """
125
+
126
+ global _token_cache
127
+
128
+ provider = provider.lower().strip()
129
+ if not provider:
130
+ raise ValueError("provider is required")
131
+
132
+ now = _time.time()
133
+
134
+ cached = _token_cache.get(provider)
135
+ if cached is not None:
136
+ access_token, expiry_ts = cached
137
+ if (expiry_ts - now) >= min_valid_seconds:
138
+ return access_token
139
+
140
+ # (Re)fetch from server
141
+ access_token, expiry_ts = _fetch_access_token(provider)
142
+ _token_cache[provider] = (access_token, expiry_ts)
143
+ return access_token
144
+
145
+
146
+ # Backwards-compatibility wrapper ------------------------------------------------
147
+
148
+
149
+ def get_google_access_token(min_valid_seconds: int = 900) -> str:
150
+ """Legacy helper kept for old notebooks – delegates to get_access_token."""
151
+
152
+ return get_access_token("google", min_valid_seconds=min_valid_seconds)
153
+
154
+
155
+ # ---------------------------------------------------------------------------
156
+ # Document upload helper (unchanged apart from minor refactoring)
157
+ # ---------------------------------------------------------------------------
158
+
159
+
160
+ def _pretty_size(size: int) -> str:
161
+ for unit in ("B", "KB", "MB", "GB"):
162
+ if size < 1024:
163
+ return f"{size:.1f} {unit}" if unit != "B" else f"{size} {unit}"
164
+ size /= 1024
165
+ return f"{size:.1f} TB"
166
+
167
+
168
+ def save_to_lumera(file_path: str) -> Dict:
169
+ """Upload *file_path* to Lumera and return the stored document metadata."""
170
+
171
+ token = _ensure_token()
172
+
173
+ path = pathlib.Path(file_path).expanduser().resolve()
174
+ if not path.is_file():
175
+ raise FileNotFoundError(path)
176
+
177
+ filename = path.name
178
+ size = path.stat().st_size
179
+ mimetype = mimetypes.guess_type(filename)[0] or "application/octet-stream"
180
+ pretty = _pretty_size(size)
181
+
182
+ headers = {
183
+ "Authorization": f"token {token}",
184
+ "Content-Type": "application/json",
185
+ }
186
+
187
+ # 1. Create document record -------------------------------------------------
188
+ resp = requests.post(
189
+ BASE_URL,
190
+ json={
191
+ "title": filename,
192
+ "content": f"File to be uploaded: {filename} ({pretty})",
193
+ "type": mimetype.split("/")[-1],
194
+ "status": "uploading",
195
+ },
196
+ headers=headers,
197
+ timeout=30,
198
+ )
199
+ resp.raise_for_status()
200
+ doc = resp.json()
201
+ doc_id = doc["id"]
202
+
203
+ # 2. Obtain signed upload URL ---------------------------------------------
204
+ resp = requests.post(
205
+ f"{BASE_URL}/{doc_id}/upload-url",
206
+ json={"filename": filename, "content_type": mimetype, "size": size},
207
+ headers=headers,
208
+ timeout=30,
209
+ )
210
+ resp.raise_for_status()
211
+ upload_url: str = resp.json()["upload_url"]
212
+
213
+ # 3. PUT bytes to GCS -------------------------------------------------------
214
+ with open(path, "rb") as fp:
215
+ put = requests.put(upload_url, data=fp, headers={"Content-Type": mimetype}, timeout=120)
216
+ put.raise_for_status()
217
+
218
+ # 4. Mark document as uploaded --------------------------------------------
219
+ resp = requests.put(
220
+ f"{BASE_URL}/{doc_id}",
221
+ json={
222
+ "status": "uploaded",
223
+ "content": f"Uploaded file: {filename} ({pretty})\nFile ID: undefined",
224
+ },
225
+ headers=headers,
226
+ timeout=30,
227
+ )
228
+ resp.raise_for_status()
229
+ return resp.json()
@@ -0,0 +1,9 @@
1
+ [project]
2
+ name = "lumera"
3
+ version = "0.1.0"
4
+ description = "Lumera library"
5
+ requires-python = ">=3.8"
6
+ dependencies = ["requests"]
7
+
8
+ [tool.setuptools]
9
+ py-modules = ["lumera"] # <- single file lumera.py
lumera-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+