-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement atlasclient - an easy-to-use Python client for the MongoDB …
…Atlas API. (#1)
- Loading branch information
1 parent
34b2897
commit fa7c5e1
Showing
8 changed files
with
510 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# Copyright 2020-present MongoDB, Inc. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
# Copyright 2020-present MongoDB, Inc. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
__version__ = '0.0.1' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# Copyright 2020-present MongoDB, Inc. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
"""Python client for the MongoDB Atlas API.""" | ||
|
||
from atlasclient.client import AtlasClient | ||
from atlasclient.exceptions import ( | ||
AtlasApiError, AtlasRateLimitError, AtlasClientError, AtlasApiBaseError) | ||
from atlasclient.utils import JSONObject |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,268 @@ | ||
# Copyright 2020-present MongoDB, Inc. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
"""Python client for the MongoDB Atlas API.""" | ||
|
||
import requests | ||
|
||
from atlasclient.configuration import ( | ||
ClientConfiguration, CONFIG_DEFAULTS as DEFAULTS) | ||
from atlasclient.exceptions import ( | ||
AtlasAuthenticationError, AtlasClientError, AtlasApiError, | ||
AtlasRateLimitError) | ||
from atlasclient.utils import enable_http_logging, JSONObject | ||
|
||
|
||
_EMPTY_PATH_ERR_MSG_TEMPLATE = ('Calling {} on an empty API path is not ' | ||
'supported.') | ||
|
||
|
||
class _ApiComponent: | ||
"""Private class for dynamically constructing resource paths.""" | ||
def __init__(self, client, path=None): | ||
self._client = client | ||
self._path = path | ||
|
||
def __repr__(self): | ||
return '<ApiComponent: %s>' % self._path | ||
|
||
def __getitem__(self, path): | ||
if self._path is not None: | ||
path = '%s/%s' % (self._path, path) | ||
return _ApiComponent(self._client, path) | ||
|
||
def __getattr__(self, path): | ||
return self[path] | ||
|
||
def get(self, **params): | ||
if self._path is None: | ||
raise TypeError(_EMPTY_PATH_ERR_MSG_TEMPLATE.format('get()')) | ||
return self._client.request('GET', self._path, **params) | ||
|
||
def patch(self, **params): | ||
if self._path is None: | ||
raise TypeError(_EMPTY_PATH_ERR_MSG_TEMPLATE.format('patch()')) | ||
return self._client.request('PATCH', self._path, **params) | ||
|
||
def post(self, **params): | ||
if self._path is None: | ||
raise TypeError(_EMPTY_PATH_ERR_MSG_TEMPLATE.format('post()')) | ||
return self._client.request('POST', self._path, **params) | ||
|
||
def delete(self, **params): | ||
if self._path is None: | ||
raise TypeError(_EMPTY_PATH_ERR_MSG_TEMPLATE.format('delete()')) | ||
return self._client.request('DELETE', self._path, **params) | ||
|
||
def get_path(self): | ||
return self._path | ||
|
||
|
||
class _ApiResponse: | ||
"""Private wrapper class for processing HTTP responses.""" | ||
def __init__(self, response, request_method, json_data): | ||
self.resource_url = response.url | ||
self.headers = response.headers | ||
self.status = response.status_code | ||
self.request_method = request_method | ||
self.data = json_data | ||
|
||
def __repr__(self): | ||
return '<{}: {} {}, [HTTP status code: {}]>'.format( | ||
self.__class__.__name__, self.request_method, | ||
self.resource_url, self.status) | ||
|
||
|
||
class AtlasClient: | ||
"""An easy-to-use MongoDB Atlas API client for Python. """ | ||
def __init__(self, *, username, password, | ||
base_url=DEFAULTS.BASE_URL, | ||
api_version=DEFAULTS.API_VERSION, | ||
timeout=DEFAULTS.HTTP_TIMEOUT, | ||
verbose=0): | ||
""" | ||
Client for the `MongoDB Atlas API | ||
<https://docs.atlas.mongodb.com/api/>`_. | ||
The client exposes a fluent interface to the Atlas API. To get started, | ||
users must first use the Atlas Web UI to `Configure API Access | ||
<https://docs.atlas.mongodb.com/configure-api-access/>`_. A client can | ||
then be instantiated using the public and private API keys:: | ||
client = AtlasClient(username=public_key, password=private_key) | ||
Use the :meth:`~atlasclient.client.AtlasClient.root` method to check | ||
that the credentials are valid:: | ||
print(client.root.get().data) | ||
GET example - Get One Cluster | ||
(GET /groups/{GROUP-ID}/clusters/{CLUSTER-NAME}) | ||
cluster_details = client.groups['5e3b3687f2a30b7ec2d220ab'].clusters['cluster1'].get() | ||
POST example - Create a Project | ||
(POST /groups) | ||
project = client.groups.post( | ||
name='new_project', orgId='5afdee7f96e8212ab7171c61') | ||
PATCH example - Disable server-side Javascript | ||
(PATCH /groups/{GROUP-ID}/clusters/{CLUSTER-NAME}/processArgs) | ||
new_process_args = client.groups['5e3b3687f2a30b7ec2d220ab'].clusters['cluster1'].processArgs.patch( | ||
javascriptEnabled=False) | ||
DELETE example - Delete a Cluster | ||
(DELETE /groups/{GROUP-ID}/clusters/{CLUSTER-NAME}) | ||
client.groups['5e3b3687f2a30b7ec2d220ab'].clusters['cluster1'].delete() | ||
.. note:: all HTTP methods (get, post, patch, delete) have support | ||
user-specified JSON input via the ``json`` keyword argument. | ||
:Parameters: | ||
- `username` (string): username to use for authenticating with the | ||
MongoDB Atlas API. This is the Public Key part of the programmatic | ||
API key generated via the Atlas Web UI. | ||
- `password` (string): password to use for authenticating with the | ||
MongoDB Atlas API. This is the Private Key part of the programmatic | ||
API key generated via the Atlas Web UI. | ||
- `base_url` (string, optional): base URL to use for | ||
communicating with the MongoDB Atlas API. | ||
Default: https://cloud.mongodb.com/api/atlas. | ||
- `api_version` (float, optional): version of the Atlas API to | ||
use while issuing requests. Default: 1.0. | ||
- `timeout` (float, optional): time, in seconds, after which an | ||
HTTP request to the Atlas API should timeout. Default: 10.0. | ||
- `verbose` (int, optional): logging level. Default: 0. | ||
""" | ||
if not username or not password: | ||
raise ValueError("Username and/or password cannot be empty.") | ||
|
||
config = ClientConfiguration( | ||
base_url=base_url, api_version=api_version, | ||
timeout=timeout, verbose=verbose, | ||
auth=requests.auth.HTTPDigestAuth( | ||
username=username, password=password)) | ||
|
||
self.config = config | ||
if config.verbose: | ||
enable_http_logging(config.verbose) | ||
|
||
def __getattr__(self, path): | ||
return _ApiComponent(self, path) | ||
|
||
@property | ||
def root(self): | ||
""" | ||
Access the root resource of the Atlas API. | ||
This needs special handling because empty paths are not otherwise | ||
supported by the Fluent API implementation. | ||
""" | ||
return _ApiComponent(self, '') | ||
|
||
def request(self, method, path, **params): | ||
""" | ||
Issue an HTTP request and process the response. | ||
:Parameters: | ||
- `method` (string): HTTP method to use for issuing the request. | ||
- `path` (string): path of the resource (relative to the API's | ||
base URL) against which to issue the request. | ||
- `params` (dict): query and body parameters to use for the request. | ||
Currently, only the "pretty", "envelope", "itemsPerPage", and | ||
"pageNum" query parameters are supported and all other parameters | ||
are passed as body parameters. Users may use the "json" kwarg to | ||
specify raw JSON input. This is useful when a user needs to send a | ||
payload that does not consist of key-value pairs (e.g. when adding | ||
a server to the IP Whitelist, a list of documents must be sent). | ||
""" | ||
method = method.upper() | ||
url = self.construct_resource_url(path) | ||
|
||
query_params = {} | ||
for param_name in ("pretty", "envelope", "itemsPerPage", "pageNum"): | ||
if param_name in params: | ||
query_params[param_name] = params.pop(param_name) | ||
|
||
raw_json = params.pop('json', None) | ||
if raw_json: | ||
params = raw_json | ||
|
||
request_kwargs = { | ||
'auth': self.config.auth, | ||
'params': query_params, | ||
'json': params, | ||
'timeout': self.config.timeout} | ||
|
||
try: | ||
response = requests.request(method, url, **request_kwargs) | ||
except requests.RequestException as e: | ||
raise AtlasClientError( | ||
str(e), | ||
resource_url=url, | ||
request_method=method | ||
) | ||
|
||
return self.handle_response(method, response) | ||
|
||
def construct_resource_url(self, path): | ||
url_template = "{base_url}/v{version}/{resource_path}" | ||
return url_template.format(base_url=self.config.base_url, | ||
version=self.config.api_version, | ||
resource_path=path) | ||
|
||
@staticmethod | ||
def handle_response(method, response): | ||
try: | ||
data = response.json(object_hook=JSONObject) | ||
except ValueError: | ||
data = None | ||
|
||
if response.status_code in (200, 201, 202): | ||
return _ApiResponse(response, method, data) | ||
|
||
if response.status_code == 429: | ||
raise AtlasRateLimitError('Too many requests', response=response, | ||
request_method=method, error_code=429) | ||
|
||
if data is None: | ||
raise AtlasApiError('Unable to decode JSON response.', | ||
response=response, request_method=method) | ||
|
||
kwargs = { | ||
'response': response, | ||
'request_method': method, | ||
'error_code': data.get('errorCode')} | ||
|
||
if response.status_code == 400: | ||
raise AtlasApiError('400: Bad Request.', **kwargs) | ||
|
||
if response.status_code == 401: | ||
raise AtlasAuthenticationError('401: Unauthorized.', **kwargs) | ||
|
||
if response.status_code == 403: | ||
raise AtlasApiError('403: Forbidden.', **kwargs) | ||
|
||
if response.status_code == 404: | ||
raise AtlasApiError('404: Not Found.', **kwargs) | ||
|
||
if response.status_code == 409: | ||
raise AtlasApiError('409: Conflict.', **kwargs) | ||
|
||
raise AtlasApiError('{}: Unknown.'.format(response.status_code), | ||
**kwargs) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# Copyright 2020-present MongoDB, Inc. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
"""Configuration options for the Python client for the MongoDB Atlas API.""" | ||
|
||
from collections import namedtuple | ||
|
||
from atlasclient.utils import JSONObject | ||
|
||
|
||
ClientConfiguration = namedtuple( | ||
"AtlasClientConfiguration", | ||
["base_url", "api_version", "auth", "timeout", "verbose"]) | ||
|
||
|
||
# Default configuration values. | ||
CONFIG_DEFAULTS = JSONObject({ | ||
"HTTP_TIMEOUT": 10.0, | ||
"API_VERSION": 1.0, | ||
"BASE_URL": "https://cloud.mongodb.com/api/atlas"}) |
Oops, something went wrong.