terrakio-core 0.3.3__py3-none-any.whl → 0.3.6__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.

Potentially problematic release.


This version of terrakio-core might be problematic. Click here for more details.

@@ -1,10 +1,6 @@
1
- ### implementing generation-tiles in python api
2
- ### function should just generate the json file for mass_stats to pick up.
3
-
4
1
  import geopandas as gpd
5
2
  import shapely.geometry
6
3
  import json
7
- from rich import print
8
4
 
9
5
  def escape_newline(string):
10
6
  if isinstance(string, list):
@@ -28,7 +24,6 @@ def tile_generator(x_min, y_min, x_max, y_max, aoi, crs, res, tile_size, express
28
24
  j_max += 1
29
25
  for j in range(0, int(j_max)):
30
26
  for i in range(0, int(i_max)):
31
- #print(f"Processing tile {i} {j}")
32
27
  x = x_min + i*(tile_size*res)
33
28
  y = y_max - j*(tile_size*res)
34
29
  bbox = shapely.geometry.box(x, y-(tile_size*res), x + (tile_size*res), y)
@@ -62,10 +57,8 @@ def tiles(
62
57
  non_interactive: bool = False,
63
58
  ):
64
59
 
65
- # Create requests for each tile
66
60
  reqs = []
67
61
  x_min, y_min, x_max, y_max, aoi = get_bounds(aoi, crs, to_crs)
68
- #print(f"Bounds: {x_min}, {y_min}, {x_max}, {y_max}")
69
62
 
70
63
  if to_crs is None:
71
64
  to_crs = crs
@@ -73,9 +66,6 @@ def tiles(
73
66
  req_name = f"{name}_{i:02d}_{j:02d}"
74
67
  reqs.append({"group": "tiles", "file": req_name, "request": tile_req})
75
68
 
76
- #print(f"Generated {len(reqs)} tile requests.")
77
-
78
-
79
69
  count = len(reqs)
80
70
  groups = list(set(dic["group"] for dic in reqs))
81
71
 
@@ -91,5 +81,4 @@ def tiles(
91
81
  request_json = json.dumps(reqs)
92
82
  manifest_json = json.dumps(groups)
93
83
 
94
- return body, request_json, manifest_json
95
-
84
+ return body, request_json, manifest_json
@@ -0,0 +1,370 @@
1
+ import asyncio
2
+ import functools
3
+ from typing import Optional, Dict, Any, Union
4
+ from geopandas import GeoDataFrame
5
+ from shapely.geometry.base import BaseGeometry as ShapelyGeometry
6
+ from .async_client import AsyncClient
7
+ from typing import Dict
8
+
9
+
10
+ def sync_wrapper(async_func):
11
+ """
12
+ Decorator to convert async functions to sync functions.
13
+ """
14
+ @functools.wraps(async_func)
15
+ def wrapper(*args, **kwargs):
16
+ # Get or create event loop
17
+ try:
18
+ loop = asyncio.get_running_loop()
19
+ # If we're in an async context, we need to run in a new thread
20
+ import concurrent.futures
21
+ with concurrent.futures.ThreadPoolExecutor() as executor:
22
+ future = executor.submit(asyncio.run, async_func(*args, **kwargs))
23
+ return future.result()
24
+ except RuntimeError:
25
+ # No running loop, we can create a new one
26
+ return asyncio.run(async_func(*args, **kwargs))
27
+ return wrapper
28
+
29
+
30
+ class SyncWrapper:
31
+ """
32
+ Generic synchronous wrapper for any async object.
33
+ Automatically converts all async methods to sync using __getattr__.
34
+ """
35
+
36
+ def __init__(self, async_obj, sync_client):
37
+ self._async_obj = async_obj
38
+ self._sync_client = sync_client
39
+
40
+ def __getattr__(self, name):
41
+ """
42
+ Dynamically wrap any method call to convert async to sync.
43
+ """
44
+ attr = getattr(self._async_obj, name)
45
+
46
+ # If it's a callable (method), wrap it
47
+ if callable(attr):
48
+ def sync_wrapper(*args, **kwargs):
49
+ result = attr(*args, **kwargs)
50
+ # If the result is a coroutine, run it synchronously
51
+ if hasattr(result, '__await__'):
52
+ return self._sync_client._run_async(result)
53
+ return result
54
+ return sync_wrapper
55
+
56
+ # If it's not a callable (like a property), return as-is
57
+ return attr
58
+
59
+
60
+ class SyncClient:
61
+ """
62
+ Synchronous wrapper around AsyncClient that converts all async methods to sync.
63
+ Uses the AsyncClient as a context manager to properly handle session lifecycle.
64
+ """
65
+
66
+ def __init__(self, url: Optional[str] = None, api_key: Optional[str] = None, verbose: bool = False):
67
+ """
68
+ Initialize the synchronous client.
69
+
70
+ Args:
71
+ url (Optional[str]): The API base URL
72
+ api_key (Optional[str]): The API key for authentication
73
+ verbose (bool): Whether to enable verbose logging
74
+ """
75
+ self._async_client = AsyncClient(url=url, api_key=api_key, verbose=verbose)
76
+ self._context_entered = False
77
+ self._closed = False
78
+
79
+ # Initialize endpoint managers
80
+ self._init_endpoints()
81
+
82
+ # Register cleanup on exit
83
+ import atexit
84
+ atexit.register(self._cleanup)
85
+
86
+ def __getattr__(self, name):
87
+ """
88
+ Delegate attribute access to the underlying async client.
89
+ """
90
+ if hasattr(self._async_client, name):
91
+ attr = getattr(self._async_client, name)
92
+
93
+ # If it's a callable (method), wrap it to run synchronously
94
+ if callable(attr):
95
+ def sync_method(*args, **kwargs):
96
+ result = attr(*args, **kwargs)
97
+ # If the result is a coroutine, run it synchronously
98
+ if hasattr(result, '__await__'):
99
+ return self._run_async(result)
100
+ return result
101
+ return sync_method
102
+
103
+ # If it's not a callable (like a property), return as-is
104
+ return attr
105
+
106
+ # If the attribute doesn't exist, raise AttributeError
107
+ raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
108
+
109
+ async def _ensure_context(self):
110
+ """Ensure the async client context is entered."""
111
+ if not self._context_entered and not self._closed:
112
+ await self._async_client.__aenter__()
113
+ self._context_entered = True
114
+
115
+ async def _exit_context(self):
116
+ """Exit the async client context."""
117
+ if self._context_entered and not self._closed:
118
+ await self._async_client.__aexit__(None, None, None)
119
+ self._context_entered = False
120
+
121
+ def _run_async(self, coro):
122
+ """
123
+ Run an async coroutine and return the result synchronously.
124
+ Ensures the AsyncClient context is properly managed.
125
+ """
126
+ async def run_with_context():
127
+ await self._ensure_context()
128
+ return await coro
129
+
130
+ try:
131
+ loop = asyncio.get_running_loop()
132
+ # If we're already in an async context, run in a new thread
133
+ import concurrent.futures
134
+
135
+ with concurrent.futures.ThreadPoolExecutor() as executor:
136
+ future = executor.submit(asyncio.run, run_with_context())
137
+ return future.result()
138
+ except RuntimeError:
139
+ # No running loop, create a new one
140
+ return asyncio.run(run_with_context())
141
+
142
+ def _run_async_single_use(self, coro):
143
+ """
144
+ Run an async coroutine with its own context manager for one-off operations.
145
+ This is useful when you don't want to maintain a persistent session.
146
+ """
147
+ async def run_with_own_context():
148
+ async with self._async_client:
149
+ return await coro
150
+
151
+ try:
152
+ loop = asyncio.get_running_loop()
153
+ # If we're already in an async context, run in a new thread
154
+ import concurrent.futures
155
+
156
+ with concurrent.futures.ThreadPoolExecutor() as executor:
157
+ future = executor.submit(asyncio.run, run_with_own_context())
158
+ return future.result()
159
+ except RuntimeError:
160
+ # No running loop, create a new one
161
+ return asyncio.run(run_with_own_context())
162
+
163
+ def geoquery(
164
+ self,
165
+ expr: str,
166
+ feature: Union[Dict[str, Any], ShapelyGeometry],
167
+ in_crs: str = "epsg:4326",
168
+ out_crs: str = "epsg:4326",
169
+ output: str = "csv",
170
+ resolution: int = -1,
171
+ geom_fix: bool = False,
172
+ **kwargs
173
+ ):
174
+ """
175
+ Compute WCS query for a single geometry (synchronous version).
176
+
177
+ Args:
178
+ expr (str): The WCS expression to evaluate
179
+ feature (Union[Dict[str, Any], ShapelyGeometry]): The geographic feature
180
+ in_crs (str): Input coordinate reference system
181
+ out_crs (str): Output coordinate reference system
182
+ output (str): Output format ('csv' or 'netcdf')
183
+ resolution (int): Resolution parameter
184
+ geom_fix (bool): Whether to fix the geometry (default False)
185
+ **kwargs: Additional parameters to pass to the WCS request
186
+
187
+ Returns:
188
+ Union[pd.DataFrame, xr.Dataset, bytes]: The response data in the requested format
189
+
190
+ Raises:
191
+ APIError: If the API request fails
192
+ """
193
+ coro = self._async_client.geoquery(
194
+ expr=expr,
195
+ feature=feature,
196
+ in_crs=in_crs,
197
+ out_crs=out_crs,
198
+ output=output,
199
+ resolution=resolution,
200
+ geom_fix=geom_fix,
201
+ **kwargs
202
+ )
203
+ return self._run_async(coro)
204
+
205
+ def zonal_stats(
206
+ self,
207
+ gdf: GeoDataFrame,
208
+ expr: str,
209
+ conc: int = 20,
210
+ inplace: bool = False,
211
+ in_crs: str = "epsg:4326",
212
+ out_crs: str = "epsg:4326",
213
+ resolution: int = -1,
214
+ geom_fix: bool = False,
215
+ ):
216
+ """
217
+ Compute zonal statistics for all geometries in a GeoDataFrame (synchronous version).
218
+
219
+ Args:
220
+ gdf (GeoDataFrame): GeoDataFrame containing geometries
221
+ expr (str): Terrakio expression to evaluate, can include spatial aggregations
222
+ conc (int): Number of concurrent requests to make
223
+ inplace (bool): Whether to modify the input GeoDataFrame in place
224
+ in_crs (str): Input coordinate reference system
225
+ out_crs (str): Output coordinate reference system
226
+ resolution (int): Resolution parameter
227
+ geom_fix (bool): Whether to fix the geometry (default False)
228
+
229
+ Returns:
230
+ geopandas.GeoDataFrame: GeoDataFrame with added columns for results, or None if inplace=True
231
+
232
+ Raises:
233
+ ValueError: If concurrency is too high
234
+ APIError: If the API request fails
235
+ """
236
+ coro = self._async_client.zonal_stats(
237
+ gdf=gdf,
238
+ expr=expr,
239
+ conc=conc,
240
+ inplace=inplace,
241
+ in_crs=in_crs,
242
+ out_crs=out_crs,
243
+ resolution=resolution,
244
+ geom_fix=geom_fix
245
+ )
246
+ return self._run_async(coro)
247
+
248
+ def create_dataset_file(
249
+ self,
250
+ aoi: str,
251
+ expression: str,
252
+ output: str,
253
+ in_crs: str = "epsg:4326",
254
+ res: float = 0.0001,
255
+ region: str = "aus",
256
+ to_crs: str = "epsg:4326",
257
+ overwrite: bool = True,
258
+ skip_existing: bool = False,
259
+ non_interactive: bool = True,
260
+ poll_interval: int = 30,
261
+ download_path: str = "/home/user/Downloads",
262
+ ) -> dict:
263
+ """
264
+ Create a dataset file using mass stats operations (synchronous version).
265
+
266
+ Args:
267
+ aoi (str): Area of interest
268
+ expression (str): Terrakio expression to evaluate
269
+ output (str): Output format
270
+ in_crs (str): Input coordinate reference system (default "epsg:4326")
271
+ res (float): Resolution (default 0.0001)
272
+ region (str): Region (default "aus")
273
+ to_crs (str): Target coordinate reference system (default "epsg:4326")
274
+ overwrite (bool): Whether to overwrite existing files (default True)
275
+ skip_existing (bool): Whether to skip existing files (default False)
276
+ non_interactive (bool): Whether to run non-interactively (default True)
277
+ poll_interval (int): Polling interval in seconds (default 30)
278
+ download_path (str): Download path (default "/home/user/Downloads")
279
+
280
+ Returns:
281
+ dict: Dictionary containing generation_task_id and combine_task_id
282
+
283
+ Raises:
284
+ ConfigurationError: If mass stats client is not properly configured
285
+ RuntimeError: If job fails
286
+ """
287
+ coro = self._async_client.create_dataset_file(
288
+ aoi=aoi,
289
+ expression=expression,
290
+ output=output,
291
+ in_crs=in_crs,
292
+ res=res,
293
+ region=region,
294
+ to_crs=to_crs,
295
+ overwrite=overwrite,
296
+ skip_existing=skip_existing,
297
+ non_interactive=non_interactive,
298
+ poll_interval=poll_interval,
299
+ download_path=download_path,
300
+ )
301
+ return self._run_async(coro)
302
+
303
+ def close(self):
304
+ """
305
+ Close the underlying async client session (synchronous version).
306
+ """
307
+ if not self._closed:
308
+ async def close_async():
309
+ await self._exit_context()
310
+
311
+ try:
312
+ loop = asyncio.get_running_loop()
313
+ import concurrent.futures
314
+ with concurrent.futures.ThreadPoolExecutor() as executor:
315
+ future = executor.submit(asyncio.run, close_async())
316
+ future.result()
317
+ except RuntimeError:
318
+ asyncio.run(close_async())
319
+
320
+ self._closed = True
321
+
322
+ def _cleanup(self):
323
+ """Internal cleanup method called by atexit."""
324
+ if not self._closed:
325
+ try:
326
+ self.close()
327
+ except Exception:
328
+ # Ignore errors during cleanup
329
+ pass
330
+
331
+ def __enter__(self):
332
+ """Context manager entry."""
333
+ # Ensure context is entered when used as context manager
334
+ async def enter_async():
335
+ await self._ensure_context()
336
+
337
+ try:
338
+ loop = asyncio.get_running_loop()
339
+ import concurrent.futures
340
+ with concurrent.futures.ThreadPoolExecutor() as executor:
341
+ future = executor.submit(asyncio.run, enter_async())
342
+ future.result()
343
+ except RuntimeError:
344
+ asyncio.run(enter_async())
345
+
346
+ return self
347
+
348
+ def __exit__(self, exc_type, exc_val, exc_tb):
349
+ """Context manager exit."""
350
+ self.close()
351
+
352
+ def __del__(self):
353
+ """Destructor to ensure session is closed when object is garbage collected."""
354
+ if not self._closed:
355
+ try:
356
+ self._cleanup()
357
+ except Exception:
358
+ # If we can't close gracefully, ignore the error during cleanup
359
+ pass
360
+
361
+ # Initialize endpoint managers (these can be overridden by subclasses)
362
+ def _init_endpoints(self):
363
+ """Initialize endpoint managers. Can be overridden by subclasses."""
364
+ self.datasets = SyncWrapper(self._async_client.datasets, self)
365
+ self.users = SyncWrapper(self._async_client.users, self)
366
+ self.mass_stats = SyncWrapper(self._async_client.mass_stats, self)
367
+ self.groups = SyncWrapper(self._async_client.groups, self)
368
+ self.space = SyncWrapper(self._async_client.space, self)
369
+ self.model = SyncWrapper(self._async_client.model, self)
370
+ self.auth = SyncWrapper(self._async_client.auth, self)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: terrakio-core
3
- Version: 0.3.3
3
+ Version: 0.3.6
4
4
  Summary: Core components for Terrakio API clients
5
5
  Author-email: Yupeng Chao <yupeng@haizea.com.au>
6
6
  Project-URL: Homepage, https://github.com/HaizeaAnalytics/terrakio-python-api
@@ -0,0 +1,21 @@
1
+ terrakio_core/__init__.py,sha256=3MTDxMAQZCgXE5_gREyxTGTNK4WDZHlW91Syr0axr1M,242
2
+ terrakio_core/async_client.py,sha256=0Zz-g5X4B2NZHRJrTuJAwNBUfO596Zvn1Nga3J-PiaE,13092
3
+ terrakio_core/client.py,sha256=h8GW88g6RlGwNFW6MW48c_3BnaeT9nSd19LI1jCn1GU,1008
4
+ terrakio_core/config.py,sha256=r8NARVYOca4AuM88VP_j-8wQxOk1s7VcRdyEdseBlLE,4193
5
+ terrakio_core/exceptions.py,sha256=9S-I20-QiDRj1qgjFyYUwYM7BLic_bxurcDOIm2Fu_0,410
6
+ terrakio_core/sync_client.py,sha256=v1mcBtUaKWACqZgw8dTTVPMxUfKfiY0kjtBKzDwtGTU,13634
7
+ terrakio_core/convenience_functions/convenience_functions.py,sha256=U7bLGwfBF-FUYc0nv49pAViPsBQ6LgPlV6c6b-zeKo8,10616
8
+ terrakio_core/endpoints/auth.py,sha256=e_hdNE6JOGhRVlQMFdEoOmoMHp5EzK6CclOEnc_AmZw,5863
9
+ terrakio_core/endpoints/dataset_management.py,sha256=8uf6cxlSSevqnQWcldtA9Cd24D5VrmWyxkE7Ngx3IEw,13084
10
+ terrakio_core/endpoints/group_management.py,sha256=VFl3jakjQa9OPi351D3DZvLU9M7fHdfjCzGhmyJsx3U,6309
11
+ terrakio_core/endpoints/mass_stats.py,sha256=KDmIlMYy4nkehPU5Ejtb_WN9Cz5mkt_rIsyDZkTWOLA,21351
12
+ terrakio_core/endpoints/model_management.py,sha256=1ZYymaTQ7IY191sLSS7MWvhrHLmy2VeAM2A1Ty5NhU0,15346
13
+ terrakio_core/endpoints/space_management.py,sha256=YWb55nkJnFJGlALJ520DvurxDqVqwYtsvqQPWzxzhDs,2266
14
+ terrakio_core/endpoints/user_management.py,sha256=x0JW6VET7eokngmkhZPukegxoJNR1X09BVehJt2nIdI,3781
15
+ terrakio_core/helper/bounded_taskgroup.py,sha256=wiTH10jhKZgrsgrFUNG6gig8bFkUEPHkGRT2XY7Rgmo,677
16
+ terrakio_core/helper/decorators.py,sha256=L6om7wmWNgCei3Wy5U0aZ-70OzsCwclkjIf7SfQuhCg,2289
17
+ terrakio_core/helper/tiles.py,sha256=xNtp3oDD912PN_FQV5fb6uQYhwfHANuXyIcxoVCCfZU,2632
18
+ terrakio_core-0.3.6.dist-info/METADATA,sha256=b0a6IvGiQAjaN-iBUu7gmE4-oxaZLIhfw8KQ_xV8tOs,1476
19
+ terrakio_core-0.3.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
+ terrakio_core-0.3.6.dist-info/top_level.txt,sha256=5cBj6O7rNWyn97ND4YuvvXm0Crv4RxttT4JZvNdOG6Q,14
21
+ terrakio_core-0.3.6.dist-info/RECORD,,
terrakio_core/auth.py DELETED
@@ -1,223 +0,0 @@
1
- import requests
2
- from typing import Optional, Dict, Any
3
- from .exceptions import APIError, ConfigurationError
4
-
5
- class AuthClient:
6
- def __init__(self, base_url: str = "https://dev-au.terrak.io",
7
- verify: bool = True, timeout: int = 60):
8
- """
9
- Initialize the Authentication Client for Terrakio API.
10
-
11
- Args:
12
- base_url: Authentication API base URL
13
- verify: Verify SSL certificates
14
- timeout: Request timeout in seconds
15
- """
16
- self.base_url = base_url.rstrip('/')
17
- self.verify = verify
18
- self.timeout = timeout
19
- self.session = requests.Session()
20
- self.session.headers.update({
21
- 'Content-Type': 'application/json'
22
- })
23
- self.token = None
24
- self.api_key = None
25
-
26
- def signup(self, email: str, password: str) -> Dict[str, Any]:
27
- """
28
- Register a new user account.
29
-
30
- Args:
31
- email: User email address
32
- password: User password
33
-
34
- Returns:
35
- API response data
36
-
37
- Raises:
38
- APIError: If signup fails
39
- """
40
- endpoint = f"{self.base_url}/users/signup"
41
-
42
- payload = {
43
- "email": email,
44
- "password": password
45
- }
46
- print("the payload is ", payload)
47
- print("the endpoint is ", endpoint)
48
- try:
49
- response = self.session.post(
50
- endpoint,
51
- json=payload,
52
- verify=self.verify,
53
- timeout=self.timeout
54
- )
55
- print("the response is ", response)
56
- if not response.ok:
57
- error_msg = f"Signup failed: {response.status_code} {response.reason}"
58
- try:
59
- error_data = response.json()
60
- if "detail" in error_data:
61
- error_msg += f" - {error_data['detail']}"
62
- except:
63
- pass
64
- raise APIError(error_msg)
65
-
66
- return response.json()
67
- except requests.RequestException as e:
68
- raise APIError(f"Signup request failed: {str(e)}")
69
-
70
- def login(self, email: str, password: str) -> str:
71
- """
72
- Log in and obtain authentication token.
73
-
74
- Args:
75
- email: User email address
76
- password: User password
77
-
78
- Returns:
79
- Authentication token
80
-
81
- Raises:
82
- APIError: If login fails
83
- """
84
- endpoint = f"{self.base_url}/users/login"
85
-
86
- payload = {
87
- "email": email,
88
- "password": password
89
- }
90
-
91
- try:
92
- response = self.session.post(
93
- endpoint,
94
- json=payload,
95
- verify=self.verify,
96
- timeout=self.timeout
97
- )
98
-
99
- if not response.ok:
100
- error_msg = f"Login failed: {response.status_code} {response.reason}"
101
- try:
102
- error_data = response.json()
103
- if "detail" in error_data:
104
- error_msg += f" - {error_data['detail']}"
105
- except:
106
- pass
107
- raise APIError(error_msg)
108
-
109
- result = response.json()
110
- self.token = result.get("token")
111
-
112
- # Update session with authorization header
113
- if self.token:
114
- self.session.headers.update({
115
- "Authorization": self.token
116
- })
117
-
118
- return self.token
119
- except requests.RequestException as e:
120
- raise APIError(f"Login request failed: {str(e)}")
121
-
122
- def refresh_api_key(self) -> str:
123
- """
124
- Generate or refresh API key.
125
-
126
- Returns:
127
- API key
128
-
129
- Raises:
130
- APIError: If refresh fails
131
- """
132
- endpoint = f"{self.base_url}/users/refresh_key"
133
-
134
- try:
135
- # Use session with updated headers from login
136
- response = self.session.post(
137
- endpoint,
138
- verify=self.verify,
139
- timeout=self.timeout
140
- )
141
-
142
- if not response.ok:
143
- error_msg = f"API key generation failed: {response.status_code} {response.reason}"
144
- try:
145
- error_data = response.json()
146
- if "detail" in error_data:
147
- error_msg += f" - {error_data['detail']}"
148
- except:
149
- pass
150
- raise APIError(error_msg)
151
-
152
- result = response.json()
153
- self.api_key = result.get("apiKey")
154
- return self.api_key
155
- except requests.RequestException as e:
156
- raise APIError(f"API key refresh request failed: {str(e)}")
157
-
158
- def view_api_key(self) -> str:
159
- """
160
- Retrieve current API key.
161
-
162
- Returns:
163
- API key
164
-
165
- Raises:
166
- APIError: If retrieval fails
167
- """
168
- endpoint = f"{self.base_url}/users/key"
169
- try:
170
- # Use session with updated headers from login
171
- response = self.session.get(
172
- endpoint,
173
- verify=self.verify,
174
- timeout=self.timeout
175
- )
176
-
177
- if not response.ok:
178
- error_msg = f"Failed to retrieve API key: {response.status_code} {response.reason}"
179
- try:
180
- error_data = response.json()
181
- if "detail" in error_data:
182
- error_msg += f" - {error_data['detail']}"
183
- except:
184
- pass
185
- raise APIError(error_msg)
186
-
187
- result = response.json()
188
- self.api_key = result.get("apiKey")
189
- return self.api_key
190
- except requests.RequestException as e:
191
- raise APIError(f"API key retrieval request failed: {str(e)}")
192
-
193
- def get_user_info(self) -> Dict[str, Any]:
194
- """
195
- Retrieve the current user's information.
196
-
197
- Returns:
198
- User information data
199
-
200
- Raises:
201
- APIError: If retrieval fails
202
- """
203
- endpoint = f"{self.base_url}/users/info"
204
- try:
205
- # Use session with updated headers from login
206
- response = self.session.get(
207
- endpoint,
208
- verify=self.verify,
209
- timeout=self.timeout
210
- )
211
- if not response.ok:
212
- error_msg = f"Failed to retrieve user info: {response.status_code} {response.reason}"
213
- try:
214
- error_data = response.json()
215
- if "detail" in error_data:
216
- error_msg += f" - {error_data['detail']}"
217
- except:
218
- pass
219
- raise APIError(error_msg)
220
-
221
- return response.json()
222
- except requests.RequestException as e:
223
- raise APIError(f"User info retrieval request failed: {str(e)}")