hakai_api 1.5.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.
hakai_api/Client.py ADDED
@@ -0,0 +1,168 @@
1
+ """Get authorized requests to the Hakai API using the requests library.
2
+
3
+ Written by: Taylor Denouden, Chris Davis, and Nate Rosenstock
4
+ Last updated: April 2021
5
+ """
6
+
7
+ import json
8
+ import os
9
+ from datetime import datetime
10
+ from time import mktime
11
+ from typing import Dict, Union
12
+
13
+ from requests_oauthlib import OAuth2Session
14
+
15
+
16
+ class Client(OAuth2Session):
17
+ _credentials_file = os.path.expanduser("~/.hakai-api-auth")
18
+ DEFAULT_API_ROOT = "https://hecate.hakai.org/api"
19
+ DEFAULT_LOGIN_PAGE = "https://hecate.hakai.org/api-client-login"
20
+ CREDENTIALS_ENV_VAR = "HAKAI_API_CREDENTIALS"
21
+ USER_AGENT_ENV_VAR = "HAKAI_API_USER_AGENT"
22
+
23
+ def __init__(
24
+ self,
25
+ api_root: str = DEFAULT_API_ROOT,
26
+ login_page: str = DEFAULT_LOGIN_PAGE,
27
+ credentials: Union[str, Dict] = None,
28
+ ):
29
+ """Create a new Client class with credentials.
30
+
31
+ Params:
32
+ api_root: The base url of the hakai api you want to call.
33
+ Defaults to the production server.
34
+ login_page: The url of the login page to direct users to.
35
+ Defaults to the production login page.
36
+ credentials (str, Dict): Credentials token retrieved from the hakai api
37
+ login page. If `None`, loads cached credentials or prompts for log in.
38
+ """
39
+ self._api_root = api_root
40
+ self._login_page = login_page
41
+ self._credentials = None
42
+
43
+ env_credentials = os.getenv(self.CREDENTIALS_ENV_VAR, None)
44
+ if isinstance(credentials, dict):
45
+ self._credentials = credentials
46
+ elif isinstance(credentials, str):
47
+ # Parse credentials from string
48
+ self._credentials = self._parse_credentials_string(credentials)
49
+ elif env_credentials is not None:
50
+ self._credentials = self._parse_credentials_string(env_credentials)
51
+ elif self.file_credentials_are_valid():
52
+ self._credentials = self._get_credentials_from_file()
53
+ else:
54
+ self._credentials = self._get_credentials_from_web()
55
+
56
+ if self._credentials is None:
57
+ raise ValueError("Credentials could not be set.")
58
+
59
+ # Cache the credentials
60
+ self._save_credentials_to_file(self._credentials)
61
+
62
+ # Init the OAuth2Session parent class with credentials
63
+ super(Client, self).__init__(token=self._credentials)
64
+
65
+ # Set User-Agent header
66
+ user_agent = os.getenv(self.USER_AGENT_ENV_VAR, "hakai-api-client-py")
67
+ self.headers.update({"User-Agent": user_agent})
68
+
69
+ @property
70
+ def api_root(self) -> str:
71
+ """Return the api base url."""
72
+ return self._api_root
73
+
74
+ @property
75
+ def login_page(self) -> str:
76
+ """Return the login page url."""
77
+ return self._login_page
78
+
79
+ @property
80
+ def credentials(self) -> Dict:
81
+ """Return the credentials object."""
82
+ if self._credentials is None:
83
+ raise ValueError("Credentials have not been set.")
84
+ return self._credentials
85
+
86
+ @classmethod
87
+ def reset_credentials(cls):
88
+ """Remove the cached credentials file."""
89
+ if os.path.isfile(cls._credentials_file):
90
+ os.remove(cls._credentials_file)
91
+
92
+ def _save_credentials_to_file(self, credentials: Dict):
93
+ """Save the credentials object to a file."""
94
+ with open(self._credentials_file, "w") as outfile:
95
+ json.dump(credentials, outfile)
96
+
97
+ @classmethod
98
+ def file_credentials_are_valid(cls) -> bool:
99
+ """Check if the cached credentials exist and are valid."""
100
+ if not os.path.isfile(cls._credentials_file):
101
+ return False
102
+ with open(cls._credentials_file, "r"):
103
+ try:
104
+ credentials = cls._get_credentials_from_file()
105
+ expires_at = credentials["expires_at"]
106
+ except (KeyError, ValueError):
107
+ os.remove(cls._credentials_file)
108
+ return False
109
+
110
+ now = int(
111
+ (
112
+ mktime(datetime.now().timetuple())
113
+ + datetime.now().microsecond / 1000000.0
114
+ )
115
+ ) # utc timestamp
116
+
117
+ if now > expires_at:
118
+ cls.reset_credentials()
119
+ return False
120
+
121
+ return True
122
+
123
+ @classmethod
124
+ def _get_credentials_from_file(cls) -> Dict:
125
+ """Get user credentials from a cached file."""
126
+ with open(cls._credentials_file, "r") as infile:
127
+ result = json.load(infile)
128
+ result = Client._check_keys_convert_types(result)
129
+ return result
130
+
131
+ def _get_credentials_from_web(self) -> Dict:
132
+ """Get user credentials from a web sign-in."""
133
+ print("Please go here and authorize:")
134
+ print(self.login_page, flush=True)
135
+ response = input("\nCopy and past your credentials from the login page:\n")
136
+
137
+ # Reformat response to dict
138
+ credentials = dict(map(lambda x: x.split("="), response.split("&")))
139
+ return credentials
140
+
141
+ @staticmethod
142
+ def _parse_credentials_string(credentials: str) -> Dict:
143
+ """Parse a credentials string into a dictionary."""
144
+ result = dict(map(lambda x: x.split("="), credentials.split("&")))
145
+ result = Client._check_keys_convert_types(result)
146
+ return result
147
+
148
+ @staticmethod
149
+ def _check_keys_convert_types(credentials: dict) -> dict:
150
+ """Check that the credentials dict has the required keys and convert types."""
151
+ missing_keys = [
152
+ key
153
+ for key in ["access_token", "token_type", "expires_at"]
154
+ if key not in credentials
155
+ ]
156
+ if len(missing_keys) > 0:
157
+ raise ValueError(
158
+ f"Credentials string is missing required keys: {str(missing_keys)}."
159
+ )
160
+
161
+ # Convert expires_at to int
162
+ credentials["expires_at"] = int(float(credentials["expires_at"]))
163
+
164
+ # If expires_in is present, convert to int
165
+ if "expires_in" in credentials:
166
+ credentials["expires_in"] = int(float(credentials["expires_in"]))
167
+
168
+ return credentials
hakai_api/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from hakai_api.Client import Client
2
+
3
+ __all__ = [Client]
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: hakai_api
3
+ Version: 1.5.2
4
+ Summary: Get Hakai database resources using http calls
5
+ Author-email: Taylor Denouden <taylor.denouden@hakai.org>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.8
9
+ Requires-Dist: pytz>=2025.2
10
+ Requires-Dist: requests-oauthlib>=2.0.0
11
+ Requires-Dist: requests>=2.32.3
12
+ Description-Content-Type: text/markdown
13
+
14
+ # Hakai Api Python Client
15
+
16
+ This project exports a single Python class that can be used to make HTTP requests to the
17
+ Hakai API resource server.
18
+ The exported `Client` class extends the functionality of the
19
+ Python [requests library](https://docs.python-requests.org/en/master/) to supply Hakai
20
+ OAuth2 credentials with url requests.
21
+
22
+ ![PyPI](https://img.shields.io/pypi/v/hakai-api) [![tests](https://github.com/HakaiInstitute/hakai-api-client-py/actions/workflows/test.yaml/badge.svg)](https://github.com/HakaiInstitute/hakai-api-client-py/actions/workflows/test.yaml) [![License: MIT](https://img.shields.io/badge/License-MIT-black.svg)](https://opensource.org/licenses/MIT)
23
+
24
+ <details>
25
+
26
+ <summary>Table of Contents</summary>
27
+
28
+ [Installation](#installation)
29
+
30
+ [Quickstart](#quickstart)
31
+
32
+ [Methods](#methods)
33
+
34
+ [API endpoints](#api-endpoints)
35
+
36
+ [Advanced usage](#advanced-usage)
37
+
38
+ [Contributing](#contributing)
39
+
40
+ </details>
41
+
42
+ # Installation
43
+
44
+ Python 3.8 or higher is required. Install with pip:
45
+
46
+ ```bash
47
+ pip install hakai-api
48
+ ```
49
+
50
+ # Quickstart
51
+
52
+ ```python
53
+ from hakai_api import Client
54
+
55
+ # Get the api request client
56
+ client = Client() # Follow stdout prompts to get an API token
57
+
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)
61
+
62
+ print(url) # https://hecate.hakai.org/api/eims/views/output/chlorophyll...
63
+ print(response.json())
64
+ # [{'action': '', 'event_pk': 7064, 'rn': '1', 'date': '2012-05-17', 'work_area': 'CALVERT'...
65
+ ```
66
+
67
+ # Methods
68
+
69
+ This library exports a single client name `Client`. Instantiating this class produces
70
+ a `requests.Session` client from the Python requests library. The Hakai API Python
71
+ Client inherits directly from `requests.Session` thus all methods available on that
72
+ parent class are available. For details see
73
+ the [requests documentation](http://docs.python-requests.org/).
74
+
75
+ The hakai_api `Client` class also contains a property `api_root` which is useful for
76
+ constructing urls to access data from the API. The
77
+ above [Quickstart example](#quickstart) demonstrates using this property to construct a
78
+ url to access project names.
79
+
80
+ # API endpoints
81
+
82
+ For details about the API, including available endpoints where data can be requested
83
+ from, see the [Hakai API documentation](https://github.com/HakaiInstitute/hakai-api).
84
+
85
+ # Advanced usage
86
+
87
+ You can specify which API to access when instantiating the Client. By default, the API
88
+ uses `https://hecate.hakai.org/api` as the API root. It may be useful to use this
89
+ library to access a locally running API instance or to access the Goose API for testing
90
+ purposes. If you are always going to be accessing data from a locally running API
91
+ instance, you are better off using the requests.py library directly since Authorization
92
+ is not required for local requests.
93
+
94
+ ```python
95
+ from hakai_api import Client
96
+
97
+ # Get a client for a locally running API instance
98
+ client = Client("http://localhost:8666")
99
+ print(client.api_root) # http://localhost:8666
100
+ ```
101
+
102
+ You can also pass in the credentials string retrieved from the hakai API login page
103
+ while initiating the Client class.
104
+
105
+ ```python
106
+ from hakai_api import Client
107
+
108
+ # Pass a credentials token as the Client Class is initiated
109
+ client = Client(credentials="CREDENTIAL_TOKEN")
110
+ ```
111
+
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.
116
+
117
+ # Contributing
118
+
119
+ See [CONTRIBUTING](CONTRIBUTING.md)
120
+
121
+ # License
122
+
123
+ See [LICENSE](LICENSE.md)
@@ -0,0 +1,6 @@
1
+ hakai_api/Client.py,sha256=L4K-Hhb-IGZJ3wydi7hMEdENM_h7duPaL4RtsACCQvw,6062
2
+ hakai_api/__init__.py,sha256=6jSdZtw1AZRFEsPxiYB7jpQ1B_xlBCho6IPZdGarOxA,56
3
+ hakai_api-1.5.2.dist-info/METADATA,sha256=e219ERceF9DFeR_eWzxmCB9vudXSGviKAXIr8PCJFA4,4023
4
+ hakai_api-1.5.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
+ hakai_api-1.5.2.dist-info/licenses/LICENSE,sha256=Ag7Q1MVBG7kruFExfcgV9OajxdpvGDbo0n4lft2yMWs,1072
6
+ hakai_api-1.5.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Hakai Institute
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.