lumera 0.9.1__py3-none-any.whl → 0.9.2__py3-none-any.whl

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,34 @@
1
+ """
2
+ Lumera SDK Integrations
3
+
4
+ Third-party service integrations with Lumera credential management.
5
+
6
+ Each integration module provides:
7
+ - A `get_*_client()` or `get_*_service()` function that returns an authenticated client
8
+ - Optional helper functions for common Lumera patterns
9
+
10
+ Example:
11
+ from lumera.integrations import google, get_access_token
12
+
13
+ # Google Sheets with Lumera-managed OAuth
14
+ sheets = google.get_sheets_service()
15
+ data = sheets.spreadsheets().values().get(...)
16
+
17
+ # Google Drive
18
+ drive = google.get_drive_service()
19
+ files = drive.files().list().execute()
20
+
21
+ # Get raw access token for any provider
22
+ token = get_access_token("slack")
23
+
24
+ Available integrations:
25
+ - `google` - Google APIs (Sheets, Drive)
26
+
27
+ Utilities:
28
+ - `get_access_token(provider)` - Get OAuth token for any Lumera-connected provider
29
+ """
30
+
31
+ from .._utils import get_access_token
32
+ from . import google
33
+
34
+ __all__ = ["get_access_token", "google"]
@@ -0,0 +1,338 @@
1
+ """
2
+ Google API Integration
3
+
4
+ Provides authenticated Google API clients using Lumera-managed OAuth credentials.
5
+
6
+ Example:
7
+ from lumera.integrations import google
8
+
9
+ # Get authenticated Sheets service
10
+ sheets = google.get_sheets_service()
11
+ data = sheets.spreadsheets().values().get(
12
+ spreadsheetId="...",
13
+ range="Sheet1!A1:D10"
14
+ ).execute()
15
+
16
+ # Get authenticated Drive service
17
+ drive = google.get_drive_service()
18
+ files = drive.files().list().execute()
19
+
20
+ # Share credentials between services
21
+ creds = google.get_credentials()
22
+ sheets = google.get_sheets_service(credentials=creds)
23
+ drive = google.get_drive_service(credentials=creds)
24
+ """
25
+
26
+ import io
27
+ import logging
28
+ import os
29
+ import re
30
+ from typing import TYPE_CHECKING, Optional, Tuple
31
+
32
+ # When type checking we want access to the concrete ``Resource`` class that
33
+ # ``googleapiclient.discovery.build`` returns. Importing it unconditionally
34
+ # would require ``googleapiclient`` to be available in every execution
35
+ # environment – something we cannot guarantee. By guarding the import with
36
+ # ``TYPE_CHECKING`` we give static analysers (ruff, mypy, etc.) the
37
+ # information they need without introducing a hard runtime dependency.
38
+ # During static analysis we want to import ``Resource`` so that it is a known
39
+ # name for type checkers, but we don't require this import at runtime. Guard
40
+ # it with ``TYPE_CHECKING`` to avoid hard dependencies.
41
+ if TYPE_CHECKING: # pragma: no cover
42
+ from googleapiclient.discovery import Resource # noqa: F401
43
+
44
+ # Always ensure that the symbol ``Resource`` exists at runtime to placate static
45
+ # analysers like ruff (F821) that inspect the AST without executing the code.
46
+ try: # pragma: no cover – optional runtime import
47
+ from googleapiclient.discovery import Resource # type: ignore
48
+ except ModuleNotFoundError: # pragma: no cover – provide a stub fallback
49
+
50
+ class Resource: # noqa: D401
51
+ """Stub replacement for ``googleapiclient.discovery.Resource``."""
52
+
53
+ pass
54
+
55
+
56
+ from google.oauth2.credentials import Credentials
57
+ from googleapiclient.discovery import build
58
+ from googleapiclient.http import MediaFileUpload, MediaIoBaseDownload
59
+
60
+ from .._utils import get_access_token
61
+
62
+ # Module logger
63
+ logger = logging.getLogger(__name__)
64
+
65
+ # =====================================================================================
66
+ # Configuration
67
+ # =====================================================================================
68
+
69
+ MIME_GOOGLE_SHEET = "application/vnd.google-apps.spreadsheet"
70
+ MIME_EXCEL = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
71
+
72
+ # =====================================================================================
73
+ # Authentication & Service Initialization
74
+ # =====================================================================================
75
+
76
+
77
+ def get_credentials() -> Credentials:
78
+ """
79
+ Retrieves a Google OAuth token from Lumera and
80
+ converts it into a Credentials object usable by googleapiclient.
81
+
82
+ Returns:
83
+ google.oauth2.credentials.Credentials: Authenticated credentials object.
84
+
85
+ Raises:
86
+ RuntimeError: If LUMERA_TOKEN is not set or token fetch fails.
87
+ """
88
+ logger.debug("Fetching Google access token from Lumera…")
89
+ access_token = get_access_token("google")
90
+ logger.debug("Access token received.")
91
+ creds = Credentials(token=access_token)
92
+ logger.debug("Credentials object created.")
93
+ return creds
94
+
95
+
96
+ # Backward compatibility alias
97
+ get_google_credentials = get_credentials
98
+
99
+
100
+ def get_sheets_service(credentials: Optional[Credentials] = None) -> "Resource":
101
+ """
102
+ Initializes and returns the Google Sheets API service.
103
+
104
+ If no credentials are provided, this function will automatically fetch a
105
+ Google access token from Lumera and construct the appropriate
106
+ ``google.oauth2.credentials.Credentials`` instance.
107
+
108
+ Args:
109
+ credentials: Optional pre-fetched credentials. If None, fetches from Lumera.
110
+
111
+ Returns:
112
+ Google Sheets API service resource.
113
+ """
114
+ if credentials is None:
115
+ logger.info("No credentials provided; fetching Google token…")
116
+ credentials = get_credentials()
117
+ logger.info("Google Sheets API service being initialized…")
118
+ return build("sheets", "v4", credentials=credentials)
119
+
120
+
121
+ def get_drive_service(credentials: Optional[Credentials] = None) -> "Resource":
122
+ """
123
+ Initializes and returns the Google Drive API service.
124
+
125
+ If no credentials are provided, this function will automatically fetch a
126
+ Google access token from Lumera and construct the appropriate
127
+ ``google.oauth2.credentials.Credentials`` instance.
128
+
129
+ Args:
130
+ credentials: Optional pre-fetched credentials. If None, fetches from Lumera.
131
+
132
+ Returns:
133
+ Google Drive API service resource.
134
+ """
135
+ if credentials is None:
136
+ logger.info("No credentials provided; fetching Google token…")
137
+ credentials = get_credentials()
138
+ logger.info("Google Drive API service being initialized…")
139
+ return build("drive", "v3", credentials=credentials)
140
+
141
+
142
+ # =====================================================================================
143
+ # Google Sheets & Drive Utility Functions
144
+ # =====================================================================================
145
+
146
+
147
+ def get_spreadsheet_and_sheet_id(
148
+ service: "Resource", spreadsheet_url: str, tab_name: str
149
+ ) -> Tuple[Optional[str], Optional[int]]:
150
+ """
151
+ Given a Google Sheets URL and a tab (sheet) name, returns a tuple:
152
+ (spreadsheet_id, sheet_id)
153
+ """
154
+ spreadsheet_id = _extract_spreadsheet_id(spreadsheet_url)
155
+ if not spreadsheet_id:
156
+ return None, None
157
+
158
+ sheet_id = _get_sheet_id_from_name(service, spreadsheet_id, tab_name)
159
+ return spreadsheet_id, sheet_id
160
+
161
+
162
+ def _extract_spreadsheet_id(spreadsheet_url: str) -> Optional[str]:
163
+ """Extracts the spreadsheet ID from a Google Sheets URL."""
164
+ logger.debug(f"Extracting spreadsheet ID from URL: {spreadsheet_url}")
165
+ pattern = r"/d/([a-zA-Z0-9-_]+)"
166
+ match = re.search(pattern, spreadsheet_url)
167
+ if match:
168
+ spreadsheet_id = match.group(1)
169
+ logger.debug(f"Spreadsheet ID extracted: {spreadsheet_id}")
170
+ return spreadsheet_id
171
+ logger.warning("Could not extract Spreadsheet ID.")
172
+ return None
173
+
174
+
175
+ def _get_sheet_id_from_name(
176
+ service: "Resource", spreadsheet_id: str, tab_name: str
177
+ ) -> Optional[int]:
178
+ """Uses the Google Sheets API to fetch the sheet ID corresponding to 'tab_name'."""
179
+ logger.debug(f"Requesting sheet metadata for spreadsheet ID: {spreadsheet_id}")
180
+ response = (
181
+ service.spreadsheets()
182
+ .get(spreadsheetId=spreadsheet_id, fields="sheets.properties")
183
+ .execute()
184
+ )
185
+ logger.debug("Metadata received. Searching for tab…")
186
+
187
+ for sheet in response.get("sheets", []):
188
+ properties = sheet.get("properties", {})
189
+ if properties.get("title") == tab_name:
190
+ sheet_id = properties.get("sheetId")
191
+ logger.debug(f"Match found for tab '{tab_name}'. Sheet ID is {sheet_id}")
192
+ return sheet_id
193
+ logger.warning(f"No sheet found with tab name '{tab_name}'.")
194
+ return None
195
+
196
+
197
+ def sheet_name_from_gid(service: "Resource", spreadsheet_id: str, gid: int) -> Optional[str]:
198
+ """Resolve a sheet's human-readable name (title) from its gid."""
199
+ logger.debug(f"Resolving sheet name from gid={gid} …")
200
+ meta = (
201
+ service.spreadsheets()
202
+ .get(
203
+ spreadsheetId=spreadsheet_id,
204
+ includeGridData=False,
205
+ fields="sheets(properties(sheetId,title))",
206
+ )
207
+ .execute()
208
+ )
209
+
210
+ for sheet in meta.get("sheets", []):
211
+ props = sheet.get("properties", {})
212
+ if props.get("sheetId") == gid:
213
+ title = props["title"]
214
+ logger.debug(f"Sheet gid={gid} corresponds to sheet name='{title}'.")
215
+ return title
216
+ logger.warning(f"No sheet found with gid={gid}")
217
+ return None
218
+
219
+
220
+ def read_cell(service: "Resource", spreadsheet_id: str, range_a1: str) -> Optional[str]:
221
+ """Fetch a single cell value (as string); returns None if empty."""
222
+ logger.debug(f"Reading cell '{range_a1}' …")
223
+ resp = (
224
+ service.spreadsheets()
225
+ .values()
226
+ .get(spreadsheetId=spreadsheet_id, range=range_a1, majorDimension="ROWS")
227
+ .execute()
228
+ )
229
+
230
+ values = resp.get("values", [])
231
+ return values[0][0] if values and values[0] else None
232
+
233
+
234
+ # NOTE: The function performs I/O side-effects and does not return a value.
235
+ def download_file_direct(drive_service: "Resource", file_id: str, dest_path: str) -> None:
236
+ """
237
+ Downloads a file directly from Google Drive using files().get_media
238
+ without any format conversion.
239
+ """
240
+ logger.info(f"Initiating direct download for file ID: {file_id}")
241
+
242
+ request = drive_service.files().get_media(fileId=file_id)
243
+ fh = io.BytesIO()
244
+ downloader = MediaIoBaseDownload(fh, request)
245
+
246
+ done = False
247
+ while not done:
248
+ status, done = downloader.next_chunk()
249
+ if status:
250
+ logger.debug(f"Download progress: {int(status.progress() * 100)}%")
251
+
252
+ with open(dest_path, "wb") as f:
253
+ f.write(fh.getvalue())
254
+ logger.info(f"File saved to: {dest_path}")
255
+
256
+
257
+ def upload_excel_as_google_sheet(
258
+ drive_service: "Resource", local_path: str, desired_name: str
259
+ ) -> Tuple[Optional[str], Optional[str]]:
260
+ """
261
+ Uploads a local XLSX file to Google Drive, converting it to Google Sheets format.
262
+ Returns the file ID and web link.
263
+ """
264
+ logger.info(f"Preparing to upload '{local_path}' as Google Sheet named '{desired_name}'")
265
+
266
+ if not os.path.isfile(local_path):
267
+ logger.error(f"Local file not found at '{local_path}'. Aborting.")
268
+ return None, None
269
+
270
+ media = MediaFileUpload(local_path, mimetype=MIME_EXCEL, resumable=True)
271
+ file_metadata = {"name": desired_name, "mimeType": MIME_GOOGLE_SHEET}
272
+
273
+ logger.info("Initiating Google Drive upload & conversion…")
274
+ request = drive_service.files().create(
275
+ body=file_metadata, media_body=media, fields="id, webViewLink"
276
+ )
277
+
278
+ response = None
279
+ while response is None:
280
+ status, response = request.next_chunk()
281
+ if status:
282
+ logger.debug(f"Upload progress: {int(status.progress() * 100)}%")
283
+
284
+ file_id = response.get("id")
285
+ web_view_link = response.get("webViewLink")
286
+ logger.info(f"Upload completed. File ID: {file_id}")
287
+ return file_id, web_view_link
288
+
289
+
290
+ # Remove rows from a sheet. All parameters are 1-based (both *start_row* and
291
+ # *end_row* are inclusive) mirroring the UI behaviour in Google Sheets.
292
+ def delete_rows_api_call(
293
+ service: "Resource",
294
+ spreadsheet_id: str,
295
+ sheet_gid: int,
296
+ start_row: int,
297
+ end_row: int,
298
+ ) -> None:
299
+ """Executes the API call to delete rows."""
300
+ logger.info(f"Deleting rows {start_row}-{end_row} (1-based inclusive)…")
301
+
302
+ body = {
303
+ "requests": [
304
+ {
305
+ "deleteDimension": {
306
+ "range": {
307
+ "sheetId": sheet_gid,
308
+ "dimension": "ROWS",
309
+ "startIndex": start_row - 1, # 0-based
310
+ "endIndex": end_row, # end-exclusive
311
+ }
312
+ }
313
+ }
314
+ ]
315
+ }
316
+ service.spreadsheets().batchUpdate(spreadsheetId=spreadsheet_id, body=body).execute()
317
+ logger.info("Rows deleted.")
318
+
319
+
320
+ __all__ = [
321
+ # Authentication
322
+ "get_credentials",
323
+ "get_google_credentials", # backward compat alias
324
+ # Services
325
+ "get_sheets_service",
326
+ "get_drive_service",
327
+ # Sheets helpers
328
+ "get_spreadsheet_and_sheet_id",
329
+ "sheet_name_from_gid",
330
+ "read_cell",
331
+ "delete_rows_api_call",
332
+ # Drive helpers
333
+ "download_file_direct",
334
+ "upload_excel_as_google_sheet",
335
+ # Constants
336
+ "MIME_GOOGLE_SHEET",
337
+ "MIME_EXCEL",
338
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lumera
3
- Version: 0.9.1
3
+ Version: 0.9.2
4
4
  Summary: SDK for building on Lumera platform
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: requests
@@ -9,7 +9,9 @@ lumera/pb.py,sha256=fTGjcclIGmXxQsBCDhXVrPyKBkdPJ3DRwNZfICT5X8E,20434
9
9
  lumera/sdk.py,sha256=0Q4b0LXbr7Vl0SpstH1IKEZwGek33Jz-4ICMRe_Z60A,31217
10
10
  lumera/storage.py,sha256=b0W6JNSGfmhJIcmK3vrATXAwxIr_bfrj-hPuQRVLTYU,8206
11
11
  lumera/webhooks.py,sha256=L_Q5YHBJKQNpv7G9Nq0QqlGMRch6x9ptlwu1xD2qwUc,8661
12
- lumera-0.9.1.dist-info/METADATA,sha256=gbS5AHAHUaw4sh8EHbKKb1ONZnzD2ph-lOidiUv49iE,1611
13
- lumera-0.9.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- lumera-0.9.1.dist-info/top_level.txt,sha256=HgfK4XQkpMTnM2E5iWM4kB711FnYqUY9dglzib3pWlE,7
15
- lumera-0.9.1.dist-info/RECORD,,
12
+ lumera/integrations/__init__.py,sha256=LnJmAnFB_p3YMKyeGVdDP4LYlJ85XFNQFAxGo6zF7CI,937
13
+ lumera/integrations/google.py,sha256=QkbBbbDh3I_OToPDFqcivU6sWy2UieHBxZ_TPv5rqK0,11862
14
+ lumera-0.9.2.dist-info/METADATA,sha256=Z26CWbc_x9lmH4nKp_7FQ4JWd0QSsgxiLQC-K6O_QgA,1611
15
+ lumera-0.9.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
+ lumera-0.9.2.dist-info/top_level.txt,sha256=HgfK4XQkpMTnM2E5iWM4kB711FnYqUY9dglzib3pWlE,7
17
+ lumera-0.9.2.dist-info/RECORD,,
File without changes