hakai_api 1.5.2__py3-none-any.whl → 2.0.0__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.
hakai_api/auth/web.py ADDED
@@ -0,0 +1,94 @@
1
+ """Web authentication strategy using copy/paste credentials."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ from loguru import logger
8
+
9
+ from .base import AuthStrategy
10
+
11
+
12
+ class WebAuthStrategy(AuthStrategy):
13
+ """Web authentication strategy using copy/paste from login page.
14
+
15
+ This is the traditional authentication flow where users visit a login page,
16
+ authenticate, and copy/paste the resulting credentials string.
17
+ """
18
+
19
+ def __init__(self, api_root: str, login_page: str, **kwargs: object) -> None:
20
+ """Initialize the authentication strategy.
21
+
22
+ Args:
23
+ api_root: The base url of the hakai api.
24
+ login_page: The url of the login page to direct users to.
25
+ **kwargs: Additional parameters passed to the base authentication strategy.
26
+ """
27
+ super().__init__(api_root, **kwargs)
28
+ self.login_page = login_page
29
+
30
+ def get_credentials(self) -> dict:
31
+ """Get user credentials from web sign-in flow.
32
+
33
+ First checks for cached credentials, environment variables, or prompts
34
+ for login if none are available.
35
+
36
+ Returns:
37
+ A dict containing the credentials parsed from user input or cache.
38
+ """
39
+ # Try environment variable first
40
+ env_credentials = os.getenv("HAKAI_API_CREDENTIALS")
41
+ if env_credentials is not None:
42
+ logger.trace("Loading credentials from environment variable")
43
+ try:
44
+ parsed_creds = self.parse_credentials_string(env_credentials)
45
+ # Check if environment credentials are expired
46
+ if self._are_credentials_expired(parsed_creds):
47
+ logger.warning("Environment variable credentials have expired")
48
+ else:
49
+ return parsed_creds
50
+ except (ValueError, KeyError) as e:
51
+ logger.warning(f"Invalid environment variable credentials: {e}")
52
+
53
+ # Try cached credentials
54
+ if self.file_credentials_are_valid():
55
+ logger.trace("Loading cached credentials from file")
56
+ return self.get_credentials_from_file()
57
+
58
+ # Prompt user for new credentials
59
+ logger.info("No valid cached credentials found, starting web authentication flow")
60
+ return self._get_credentials_from_web_input()
61
+
62
+ def _get_credentials_from_web_input(self) -> dict:
63
+ """Get user credentials from web sign-in with user input prompt.
64
+
65
+ Prompts the user to copy and paste credentials from the login page.
66
+
67
+ Returns:
68
+ A dict containing the credentials parsed from user input.
69
+
70
+ Raises:
71
+ ValueError: If the input format is invalid or cannot be split properly.
72
+ AttributeError: If the input string lacks expected string methods.
73
+ """
74
+ logger.info(f"Please go here and authorize: {self.login_page}")
75
+ print(f"Please go here and authorize: {self.login_page}")
76
+ response = input("\nCopy and paste your credentials from the login page here and press <enter>:\n")
77
+
78
+ logger.trace("Parsing credentials from user input")
79
+ try:
80
+ # Reformat response to dict
81
+ credentials = dict(map(lambda x: x.split("="), response.split("&")))
82
+ credentials = self._check_keys_convert_types(credentials)
83
+ logger.trace("Successfully parsed web credentials")
84
+ return credentials
85
+ except (ValueError, AttributeError) as e:
86
+ logger.error(f"Failed to parse credentials from input: {e}")
87
+ raise
88
+
89
+ @property
90
+ def client_type(self) -> str:
91
+ """Get the client type for web authentication strategy."""
92
+ return "web"
93
+
94
+ # refresh_token method is now inherited from AuthStrategy base class
hakai_api/client.py ADDED
@@ -0,0 +1,390 @@
1
+ """Hakai API Python Client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Any, Literal
8
+
9
+ from loguru import logger
10
+ from requests_oauthlib import OAuth2Session
11
+
12
+ from .auth import DesktopAuthStrategy, WebAuthStrategy
13
+
14
+ if TYPE_CHECKING:
15
+ from requests import Response
16
+
17
+
18
+ class Client(OAuth2Session):
19
+ """Hakai API client for authenticated HTTP requests.
20
+
21
+ Extends OAuth2Session to provide authenticated access to the Hakai API
22
+ resource server. Handles OAuth2 credential management, caching, and
23
+ automatic token refresh for seamless API interactions.
24
+
25
+ The client supports two authentication flows:
26
+ - Web flow: Copy/paste credentials from a web login page (default)
27
+ - Desktop flow: OAuth2 with PKCE for native applications
28
+
29
+ Credentials are automatically cached to the credentials_file for reuse
30
+ across sessions until expiry.
31
+
32
+ Attributes:
33
+ DEFAULT_API_ROOT: Default production API base URL.
34
+ DEFAULT_LOGIN_PAGE: Default production login page URL.
35
+ CREDENTIALS_ENV_VAR: Environment variable name for credentials.
36
+ USER_AGENT_ENV_VAR: Environment variable name for User-Agent header.
37
+
38
+ Example:
39
+ Basic usage with default settings:
40
+
41
+ >>> client = Client()
42
+ >>> response = client.get("/eims/views/output/stations")
43
+
44
+ Desktop OAuth flow:
45
+
46
+ >>> client = Client(auth_flow="desktop")
47
+ >>> response = client.get("/eims/views/output/stations")
48
+
49
+ Custom API endpoint:
50
+
51
+ >>> client = Client(api_root="https://custom.api.endpoint")
52
+ >>> response = client.get("/custom/endpoint")
53
+ """
54
+
55
+ DEFAULT_API_ROOT = "https://hecate.hakai.org/api"
56
+ DEFAULT_LOGIN_PAGE = "https://hecate.hakai.org/api-client-login"
57
+ CREDENTIALS_ENV_VAR = "HAKAI_API_CREDENTIALS"
58
+ USER_AGENT_ENV_VAR = "HAKAI_API_USER_AGENT"
59
+
60
+ def __init__(
61
+ self,
62
+ api_root: str = DEFAULT_API_ROOT,
63
+ login_page: str = DEFAULT_LOGIN_PAGE,
64
+ credentials: str | dict | None = None,
65
+ credentials_file: str | Path | None = None,
66
+ user_agent: str | None = None,
67
+ auth_flow: Literal["web", "desktop"] = "web",
68
+ local_port: int = 65500,
69
+ use_refresh: bool = True,
70
+ callback_timeout: int = 120,
71
+ ) -> None:
72
+ """Create a new Client class with credentials.
73
+
74
+ Args:
75
+ api_root: The base url of the hakai api you want to call.
76
+ Defaults to the production server.
77
+ login_page: The url of the login page to direct users to.
78
+ Defaults to the production login page.
79
+ credentials: Credentials token retrieved from the hakai api
80
+ login page. If `None`, loads cached credentials or prompts for log in.
81
+ credentials_file: The path to the file where credentials are saved. This will default to the path given by
82
+ environment variable `HAKAI_API_CREDENTIALS`, if defined, else to `~/.hakai-api-auth`.
83
+ user_agent: A user-agent string to use when requesting the hakai api to identify your application.
84
+ This will default to the value given by environment variable `HAKAI_API_USER_AGENT`, if defined, else
85
+ to `hakai-api-client-py`.
86
+ auth_flow: Authentication flow type - "web" (default, copy/paste) or "desktop" (OAuth with PKCE).
87
+ Only used if credentials are not provided.
88
+ local_port: Port for local callback server in desktop flow (default 65500).
89
+ Only used when auth_flow="desktop".
90
+ use_refresh: Whether to use the refresh token to automatically extend user sessions.
91
+ Currently only works for credentials obtained with the "desktop" authentication flow.
92
+ callback_timeout: Timeout for detecting credentials in browser in seconds.
93
+
94
+ Raises:
95
+ ValueError: If credentials are unable to be set.
96
+ """
97
+ self._api_root = api_root
98
+ self._login_page = login_page
99
+ self._auth_flow = auth_flow
100
+ self._local_port = local_port
101
+ self._use_refresh = use_refresh
102
+
103
+ if credentials_file is None:
104
+ credentials_file = os.getenv(self.CREDENTIALS_ENV_VAR, Path.home() / ".hakai-api-auth")
105
+ credentials_file = Path(credentials_file)
106
+
107
+ if user_agent is None:
108
+ user_agent = os.getenv(self.USER_AGENT_ENV_VAR, "hakai-api-client-py")
109
+
110
+ # Create authentication strategy
111
+ if auth_flow == "desktop":
112
+ self._auth_strategy = DesktopAuthStrategy(
113
+ api_root, local_port=local_port, credentials_file=credentials_file, callback_timeout=callback_timeout
114
+ )
115
+ else:
116
+ self._auth_strategy = WebAuthStrategy(api_root, login_page=login_page, credentials_file=credentials_file)
117
+
118
+ # Get credentials using strategy or provided values
119
+ logger.trace(f"Initializing Hakai API client with auth_flow={auth_flow}")
120
+
121
+ if isinstance(credentials, dict):
122
+ logger.trace("Using provided credentials dictionary")
123
+ # Validate and type-convert the provided credentials
124
+ self._credentials = self._auth_strategy._check_keys_convert_types(credentials)
125
+ elif isinstance(credentials, str):
126
+ logger.trace("Parsing credentials from provided string")
127
+ self._credentials = self._auth_strategy.parse_credentials_string(credentials)
128
+ else:
129
+ # Use strategy to get credentials (handles env vars and cached credentials properly)
130
+ logger.trace(f"No credentials provided, using {auth_flow} authentication strategy")
131
+ try:
132
+ self._credentials = self._auth_strategy.get_credentials()
133
+ except Exception as e:
134
+ logger.error(f"Failed to get credentials from strategy: {e}")
135
+ self._credentials = None
136
+
137
+ if self._credentials is None:
138
+ logger.error("Failed to obtain valid credentials from any source")
139
+ raise ValueError("Credentials could not be set.")
140
+
141
+ # Cache the credentials
142
+ logger.trace(f"Caching credentials to file {self.credentials_file}")
143
+ self._auth_strategy.save_credentials_to_file(self._credentials)
144
+
145
+ # Init the OAuth2Session parent class with credentials
146
+ super().__init__(token=self._credentials)
147
+
148
+ # Set User-Agent header
149
+ self.headers.update({"User-Agent": user_agent})
150
+ logger.debug(f"Hakai API client initialized successfully with User-Agent: {user_agent}")
151
+
152
+ @property
153
+ def api_root(self) -> str:
154
+ """Return the api base url.
155
+
156
+ Returns:
157
+ The base URL of the Hakai API.
158
+ """
159
+ return self._api_root
160
+
161
+ @property
162
+ def login_page(self) -> str:
163
+ """Return the login page url.
164
+
165
+ Returns:
166
+ The URL of the login page.
167
+ """
168
+ return self._login_page
169
+
170
+ @property
171
+ def credentials(self) -> dict:
172
+ """Return the credentials object.
173
+
174
+ Returns:
175
+ Credentials object.
176
+
177
+ Raises:
178
+ ValueError: If credentials are not provided.
179
+ """
180
+ if self._credentials is None:
181
+ raise ValueError("Credentials have not been set.")
182
+ return self._credentials
183
+
184
+ def reset_credentials(self) -> None:
185
+ """Remove the cached credentials file.
186
+
187
+ Deletes the credentials file from the filesystem if it exists.
188
+ """
189
+ self._auth_strategy.reset_credentials()
190
+
191
+ def file_credentials_are_valid(self) -> bool:
192
+ """Check if the cached credentials exist and are valid.
193
+
194
+ Validates that the credentials file exists, can be parsed,
195
+ contains required fields, and has not expired.
196
+
197
+ Returns:
198
+ True if the credentials are valid, False otherwise.
199
+ """
200
+ return self._auth_strategy.file_credentials_are_valid()
201
+
202
+ def refresh_token(self) -> bool:
203
+ """Refresh the access token using the refresh token.
204
+
205
+ Uses the stored refresh token to obtain a new access token from
206
+ the API. Updates the stored credentials and OAuth2Session token
207
+ if successful.
208
+
209
+ Returns:
210
+ True if refresh successful, False otherwise.
211
+ """
212
+ if "refresh_token" not in self._credentials:
213
+ logger.trace("No refresh token available, cannot refresh")
214
+ return False
215
+
216
+ logger.trace("Attempting to refresh access token using auth strategy")
217
+
218
+ # All strategies that support refresh tokens should have a refresh_token method
219
+ if self._use_refresh:
220
+ updated_credentials = self._auth_strategy.refresh_token(self._credentials)
221
+ if updated_credentials:
222
+ self._credentials = updated_credentials
223
+ self._auth_strategy.save_credentials_to_file(self._credentials)
224
+ self.token = self._credentials
225
+ logger.trace("Access token refreshed successfully")
226
+ return True
227
+
228
+ logger.warning("Token refresh failed or is not supported by the current auth strategy")
229
+ return False
230
+
231
+ # Factory methods for easy client creation
232
+ @classmethod
233
+ def create_web_client(
234
+ cls,
235
+ api_root: str = DEFAULT_API_ROOT,
236
+ login_page: str = DEFAULT_LOGIN_PAGE,
237
+ credentials: str | dict | None = None,
238
+ ) -> Client:
239
+ """Create a client using web authentication flow.
240
+
241
+ Args:
242
+ api_root: The base url of the hakai api.
243
+ login_page: The url of the login page to direct users to.
244
+ credentials: Optional credentials to use.
245
+
246
+ Returns:
247
+ A Client configured for web authentication.
248
+ """
249
+ return cls(
250
+ api_root=api_root,
251
+ login_page=login_page,
252
+ credentials=credentials,
253
+ auth_flow="web",
254
+ )
255
+
256
+ @classmethod
257
+ def create_desktop_client(
258
+ cls,
259
+ api_root: str = DEFAULT_API_ROOT,
260
+ local_port: int = 65500,
261
+ credentials: str | dict | None = None,
262
+ callback_timeout: int = 120,
263
+ ) -> Client:
264
+ """Create a client using desktop OAuth authentication flow.
265
+
266
+ Args:
267
+ api_root: The base url of the hakai api.
268
+ local_port: Port for local callback server.
269
+ credentials: Optional credentials to use.
270
+ callback_timeout: Timeout for detecting credentials in browser in seconds.
271
+
272
+ Returns:
273
+ A Client configured for desktop OAuth authentication.
274
+ """
275
+ return cls(
276
+ api_root=api_root,
277
+ credentials=credentials,
278
+ auth_flow="desktop",
279
+ local_port=local_port,
280
+ callback_timeout=callback_timeout,
281
+ )
282
+
283
+ # Backward compatibility methods
284
+ def _get_credentials_from_web(self) -> dict:
285
+ """Backward compatibility method for getting credentials from web prompt.
286
+
287
+ Returns:
288
+ dict: Credentials dictionary from web authentication.
289
+
290
+ Raises:
291
+ TypeError: If auth_flow is not 'web'.
292
+ """
293
+ if not isinstance(self._auth_strategy, WebAuthStrategy):
294
+ raise TypeError("_get_credentials_from_web is only available for 'web' auth_flow")
295
+ return self._auth_strategy._get_credentials_from_web_input()
296
+
297
+ def _save_credentials_to_file(self, credentials: dict) -> None:
298
+ """Backward compatibility method for saving credentials to file."""
299
+ self._auth_strategy.save_credentials_to_file(credentials)
300
+
301
+ def _parse_credentials_string(self, credentials: str) -> dict:
302
+ """Backward compatibility for parsing credentials string.
303
+
304
+ Returns:
305
+ dict: Parsed credentials dictionary.
306
+ """
307
+ return self._auth_strategy.parse_credentials_string(credentials)
308
+
309
+ def _check_keys_convert_types(self, credentials: dict) -> dict:
310
+ """Backward compatibility for checking keys and converting types.
311
+
312
+ Returns:
313
+ dict: Validated and type-converted credentials dictionary.
314
+ """
315
+ return self._auth_strategy._check_keys_convert_types(credentials)
316
+
317
+ @property
318
+ def credentials_file(self) -> str:
319
+ """Backward compatibility property for credentials file path.
320
+
321
+ Returns:
322
+ The path to the credentials file.
323
+ """
324
+ return str(self._auth_strategy.credentials_file)
325
+
326
+ def _get_credentials_from_file(self) -> dict:
327
+ """Backward compatibility method for getting credentials from file.
328
+
329
+ Returns:
330
+ The contents of the credentials file as a dict.
331
+ """
332
+ return self._auth_strategy.get_credentials_from_file()
333
+
334
+ def request(self, method: str, uri: str, **kwargs: dict[str, Any]) -> Response:
335
+ """Override request method to handle 401 responses by re-authenticating.
336
+
337
+ Args:
338
+ method: HTTP method (GET, POST, etc.)
339
+ uri: URI to request
340
+ **kwargs: Additional arguments forwarded to requests.request().
341
+
342
+ Returns:
343
+ Response object
344
+ """
345
+ # Test for relative urls and prepend API root if needed
346
+ if uri.startswith("/"):
347
+ uri = f"{self.api_root}{uri}"
348
+
349
+ # First attempt
350
+ response = super().request(method, uri, **kwargs)
351
+
352
+ # If we get a 401, try to re-authenticate once
353
+ if response.status_code == 401:
354
+ logger.warning("Received 401 Unauthorized, attempting to re-authenticate")
355
+
356
+ # First try refresh token if available
357
+ if "refresh_token" in self._credentials:
358
+ logger.trace("Attempting token refresh")
359
+ if self.refresh_token():
360
+ logger.trace("Token refresh successful, retrying request")
361
+ response = super().request(method, uri, **kwargs)
362
+ return response
363
+ else:
364
+ logger.warning("Token refresh failed, falling back to full re-authentication")
365
+
366
+ # If no refresh token or refresh failed, do full re-authentication
367
+ logger.debug("Starting full re-authentication flow")
368
+
369
+ # Clear cached credentials and get new ones
370
+ self._auth_strategy.reset_credentials()
371
+
372
+ try:
373
+ # Get new credentials using the strategy
374
+ self._credentials = self._auth_strategy.get_credentials()
375
+
376
+ # Update the token for this session
377
+ self.token = self._credentials
378
+
379
+ # Cache the new credentials
380
+ self._auth_strategy.save_credentials_to_file(self._credentials)
381
+
382
+ # Retry the request with new credentials
383
+ logger.trace("Re-authentication successful, retrying request")
384
+ response = super().request(method, uri, **kwargs)
385
+
386
+ except Exception as e:
387
+ logger.error(f"Re-authentication failed: {e}")
388
+ # Return the original 401 response if re-auth fails
389
+
390
+ return response
@@ -1,11 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hakai_api
3
- Version: 1.5.2
3
+ Version: 2.0.0
4
4
  Summary: Get Hakai database resources using http calls
5
- Author-email: Taylor Denouden <taylor.denouden@hakai.org>
5
+ Author-email: Taylor Denouden <taylor.denouden@hakai.org>, Chris Davis <chris.davis@hakai.org>, Nate Rosenstock <nate.rosenstock@hakai.org>, Sam Albers <sam.albers@hakai.org>
6
+ Maintainer-email: Taylor Denouden <taylor.denouden@hakai.org>, Sam Albers <sam.albers@hakai.org>
6
7
  License-Expression: MIT
7
8
  License-File: LICENSE
8
9
  Requires-Python: >=3.8
10
+ Requires-Dist: loguru>=0.7.3
11
+ Requires-Dist: pkce>=1.0.3
9
12
  Requires-Dist: pytz>=2025.2
10
13
  Requires-Dist: requests-oauthlib>=2.0.0
11
14
  Requires-Dist: requests>=2.32.3
@@ -28,12 +31,17 @@ OAuth2 credentials with url requests.
28
31
  [Installation](#installation)
29
32
 
30
33
  [Quickstart](#quickstart)
34
+ - [Desktop OAuth Flow](#desktop-oauth-flow)
35
+ - [User Agent Configuration](#user-agent-configuration)
31
36
 
32
37
  [Methods](#methods)
33
38
 
34
39
  [API endpoints](#api-endpoints)
35
40
 
36
41
  [Advanced usage](#advanced-usage)
42
+ - [Custom API Endpoints](#custom-api-endpoints)
43
+ - [Relative Endpoint Support](#relative-endpoint-support)
44
+ - [Credentials Configuration](#credentials-configuration)
37
45
 
38
46
  [Contributing](#contributing)
39
47
 
@@ -55,15 +63,46 @@ from hakai_api import Client
55
63
  # Get the api request client
56
64
  client = Client() # Follow stdout prompts to get an API token
57
65
 
58
- # Make a data request for chlorophyll data
59
- url = '%s/%s' % (client.api_root, 'eims/views/output/chlorophyll?limit=50')
60
- response = client.get(url)
66
+ # Make a data request for chlorophyll data (using relative endpoint)
67
+ response = client.get('/eims/views/output/chlorophyll?limit=50')
61
68
 
62
- print(url) # https://hecate.hakai.org/api/eims/views/output/chlorophyll...
63
69
  print(response.json())
64
70
  # [{'action': '', 'event_pk': 7064, 'rn': '1', 'date': '2012-05-17', 'work_area': 'CALVERT'...
65
71
  ```
66
72
 
73
+ ## Desktop OAuth Flow
74
+
75
+ For native applications and automated scripts, use the desktop OAuth flow with PKCE:
76
+
77
+ ```python
78
+ from hakai_api import Client
79
+
80
+ # Use desktop OAuth flow (opens browser, more secure)
81
+ client = Client(auth_flow="desktop")
82
+
83
+ # Or use the factory method
84
+ client = Client.create_desktop_client()
85
+
86
+ # Make requests using relative endpoints
87
+ response = client.get('/eims/views/output/stations')
88
+ print(response.json())
89
+ ```
90
+
91
+ ## User Agent Configuration
92
+
93
+ **Important**: Set a descriptive user agent to help identify your application on the backend. Often the repository url is a good way to identify yourself:
94
+
95
+ ```python
96
+ from hakai_api import Client
97
+ import os
98
+
99
+ # Set user agent during initialization
100
+ client = Client(user_agent="MyApp/1.0 (contact@example.com)")
101
+
102
+ # Or set via environment variable
103
+ os.environ['HAKAI_API_USER_AGENT'] = "MyApp/1.0 (contact@example.com)"
104
+ client = Client()
105
+
67
106
  # Methods
68
107
 
69
108
  This library exports a single client name `Client`. Instantiating this class produces
@@ -84,6 +123,8 @@ from, see the [Hakai API documentation](https://github.com/HakaiInstitute/hakai-
84
123
 
85
124
  # Advanced usage
86
125
 
126
+ ## Custom API Endpoints
127
+
87
128
  You can specify which API to access when instantiating the Client. By default, the API
88
129
  uses `https://hecate.hakai.org/api` as the API root. It may be useful to use this
89
130
  library to access a locally running API instance or to access the Goose API for testing
@@ -99,7 +140,25 @@ client = Client("http://localhost:8666")
99
140
  print(client.api_root) # http://localhost:8666
100
141
  ```
101
142
 
102
- You can also pass in the credentials string retrieved from the hakai API login page
143
+ ## Relative Endpoint Support
144
+
145
+ The client supports relative endpoints that automatically prepend the API root:
146
+
147
+ ```python
148
+ from hakai_api import Client
149
+
150
+ client = Client()
151
+
152
+ # These are equivalent:
153
+ response1 = client.get('/eims/views/output/stations')
154
+ response2 = client.get('https://hecate.hakai.org/api/eims/views/output/stations')
155
+ ```
156
+
157
+ ## Credentials Configuration
158
+
159
+ ### Direct Credentials
160
+
161
+ You can pass in the credentials string retrieved from the hakai API login page
103
162
  while initiating the Client class.
104
163
 
105
164
  ```python
@@ -109,10 +168,30 @@ from hakai_api import Client
109
168
  client = Client(credentials="CREDENTIAL_TOKEN")
110
169
  ```
111
170
 
112
- Finally, you can set credentials for the client class using the `HAKAI_API_CREDENTIALS`
113
- environment variable. This is useful for e.g. setting credentials in a docker container.
114
- The value of the environment variable should be the credentials token retrieved from the
115
- Hakai API login page.
171
+ ### Environment Variables
172
+
173
+ Set credentials using the `HAKAI_API_CREDENTIALS` environment variable. This is useful
174
+ for e.g. setting credentials in a docker container. The value of the environment variable
175
+ should be the credentials token retrieved from the Hakai API login page.
176
+
177
+ ```bash
178
+ export HAKAI_API_CREDENTIALS="your_credential_token_here"
179
+ ```
180
+
181
+ ### Custom Credentials File Location
182
+
183
+ By default, credentials are saved to `~/.hakai-api-auth`. You can customize this location:
184
+
185
+ ```python
186
+ from hakai_api import Client
187
+
188
+ # Set custom credentials file path
189
+ client = Client(credentials_file="/path/to/my/credentials")
190
+
191
+ # Or use environment variable
192
+ # export HAKAI_API_CREDENTIALS="/path/to/my/credentials"
193
+ client = Client()
194
+ ```
116
195
 
117
196
  # Contributing
118
197
 
@@ -0,0 +1,11 @@
1
+ hakai_api/__init__.py,sha256=MkCIFgEAySfdofoAldNIP0pcM6AGAhNDxkBXjwceTrk,693
2
+ hakai_api/client.py,sha256=oPRcbCtzbdYH-w55Gjv_9uXcK6KYzUY6ARtoGGiyHu8,15034
3
+ hakai_api/auth/__init__.py,sha256=LfCI6mqeU2TNEw1lx6dX38-VnEyk28em6safdej3ceM,234
4
+ hakai_api/auth/base.py,sha256=M25bJ8Y0ho3j6FtDO8lknxsGqjiOnxed2rVOFn4SCcE,9349
5
+ hakai_api/auth/desktop.py,sha256=D-TbQUcAArAxgh4FVogDH4SaPlJp_1SYV-YLiCCd8L0,9996
6
+ hakai_api/auth/desktop_callback.html,sha256=T7bMbgEGPyQC2erJKN0KrcRIxmBl0bFhAWp5m60mPTc,2545
7
+ hakai_api/auth/web.py,sha256=8fBKBFaI_wN-iqjF1dH7y-pOE26i7009xjmQvVzroaU,3755
8
+ hakai_api-2.0.0.dist-info/METADATA,sha256=KAXPdzIte9GxnK1v_sPGbGzy-vJYvdtVVEr5Z7UFFjY,6207
9
+ hakai_api-2.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
+ hakai_api-2.0.0.dist-info/licenses/LICENSE,sha256=i8v9xjsJeSpF9B1I7sOroqgw2-ZKY3-hQF3i27Ap6Ns,1072
11
+ hakai_api-2.0.0.dist-info/RECORD,,
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2023 Hakai Institute
3
+ Copyright (c) 2025 Hakai Institute
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal