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 +168 -0
- hakai_api/__init__.py +3 -0
- hakai_api-1.5.2.dist-info/METADATA +123 -0
- hakai_api-1.5.2.dist-info/RECORD +6 -0
- hakai_api-1.5.2.dist-info/WHEEL +4 -0
- hakai_api-1.5.2.dist-info/licenses/LICENSE +21 -0
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,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
|
+
 [](https://github.com/HakaiInstitute/hakai-api-client-py/actions/workflows/test.yaml) [](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,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.
|