maxar-platform 1.0.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Maxar Intelligence
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.
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.1
2
+ Name: maxar-platform
3
+ Version: 1.0.0
4
+ Summary: Maxar Geospatial Platform SDK
5
+ License: MIT
6
+ Author: Marc Pfister
7
+ Author-email: marc.pfister@maxar.com
8
+ Requires-Python: >=3.9,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Provides-Extra: rasterio
16
+ Requires-Dist: OWSLib (>=0.29.3,<0.30.0)
17
+ Requires-Dist: affine (>=2.4.0,<3.0.0)
18
+ Requires-Dist: pendulum (>=3.0.0,<4.0.0)
19
+ Requires-Dist: pygeofilter (>=0.2.1,<0.3.0)
20
+ Requires-Dist: pyproj (>=3.6.1,<4.0.0)
21
+ Requires-Dist: pystac-client (>=0.7.5,<0.8.0)
22
+ Requires-Dist: pytest-coverage (>=0.0,<0.1)
23
+ Requires-Dist: rasterio (>=1.3.9,<2.0.0) ; extra == "rasterio"
24
+ Requires-Dist: requests (>=2.31.0,<3.0.0)
25
+ Requires-Dist: requests-oauthlib (>=2.0.0,<3.0.0)
26
+ Project-URL: Documentation, https://developers.maxar.com/docs/developer-tools/python-sdk
27
+ Description-Content-Type: text/markdown
28
+
29
+ # maxar-platform
30
+
31
+ A Python SDK for working with [Maxar Geospatial Platform](https://pro.maxar.com/)
32
+
33
+ Version: 1.0.0
34
+
35
+ ## Introduction
36
+
37
+ `maxar-platform` provides wrappers around the MGP APIs with a focus on simplicity and productivity. It wraps popular and proven Python geospatial packages whenever possible, but also offers a core HTTP session object to use as a basis for more custom functionality.
38
+
39
+ ### APIs supported:
40
+
41
+ #### Discovery
42
+
43
+ Discovery is Maxar's catalog API. `maxar_platform` extends `pystac-client` to provide STAC search to the user.
44
+
45
+ #### Streaming APIs (OGC)
46
+
47
+ [`owslib`](https://owslib.readthedocs.io/en/latest/) methods are available for OGC access to imagery and image footprint OGC endpoints. In addition, `maxar-platform` provides a search client with a similar interface to the Discovery module that queries the images available from these endpoints
48
+
49
+ #### Ordering, Monitoring, and Tasking APIs
50
+
51
+ These endpoints can be accessed through matching modules and functions.
52
+
53
+ #### Raster Analytics
54
+
55
+ Open any image available in Raster Analytics with `rasterio` as if the image was a Cloud Optimized Geotiff. Pixel data is generated on-the-fly.
56
+
57
+ #### Authentication
58
+
59
+ An underlying `session` object handles authentication and communication with the APIs. Also included is a module for managing API Keys.
60
+
61
+ ## Installation
62
+
63
+ `pip install maxar-platform`
64
+
65
+ To use the Raster Analytics module, you will also need to install [`rasterio`](https://rasterio.readthedocs.io/en/stable/).
66
+
67
+ ## Documentation
68
+
69
+ See the [MGP Developer Site](https://developers.maxar.com/docs/developer-tools/python-sdk)
70
+
@@ -0,0 +1,41 @@
1
+ # maxar-platform
2
+
3
+ A Python SDK for working with [Maxar Geospatial Platform](https://pro.maxar.com/)
4
+
5
+ Version: 1.0.0
6
+
7
+ ## Introduction
8
+
9
+ `maxar-platform` provides wrappers around the MGP APIs with a focus on simplicity and productivity. It wraps popular and proven Python geospatial packages whenever possible, but also offers a core HTTP session object to use as a basis for more custom functionality.
10
+
11
+ ### APIs supported:
12
+
13
+ #### Discovery
14
+
15
+ Discovery is Maxar's catalog API. `maxar_platform` extends `pystac-client` to provide STAC search to the user.
16
+
17
+ #### Streaming APIs (OGC)
18
+
19
+ [`owslib`](https://owslib.readthedocs.io/en/latest/) methods are available for OGC access to imagery and image footprint OGC endpoints. In addition, `maxar-platform` provides a search client with a similar interface to the Discovery module that queries the images available from these endpoints
20
+
21
+ #### Ordering, Monitoring, and Tasking APIs
22
+
23
+ These endpoints can be accessed through matching modules and functions.
24
+
25
+ #### Raster Analytics
26
+
27
+ Open any image available in Raster Analytics with `rasterio` as if the image was a Cloud Optimized Geotiff. Pixel data is generated on-the-fly.
28
+
29
+ #### Authentication
30
+
31
+ An underlying `session` object handles authentication and communication with the APIs. Also included is a module for managing API Keys.
32
+
33
+ ## Installation
34
+
35
+ `pip install maxar-platform`
36
+
37
+ To use the Raster Analytics module, you will also need to install [`rasterio`](https://rasterio.readthedocs.io/en/stable/).
38
+
39
+ ## Documentation
40
+
41
+ See the [MGP Developer Site](https://developers.maxar.com/docs/developer-tools/python-sdk)
@@ -0,0 +1,32 @@
1
+ """
2
+ Provides
3
+ --------
4
+
5
+ Streamlined access to these Maxar Geospatial Platform APIs:
6
+
7
+ - Authentication
8
+ - Discovery Catalog
9
+ - OGC Image streaming, vectors, and metadata
10
+ - Ordering
11
+ - Monitoring
12
+ - Tasking
13
+ - Raster Analytics
14
+
15
+ Coming soon:
16
+
17
+ - 3D Analytics
18
+ - Change Monitoring
19
+
20
+ Reference
21
+ ---------
22
+
23
+ https://developers.maxar.com/
24
+ """
25
+
26
+ __version__ = "1.0.0"
27
+
28
+ import os
29
+
30
+ BASE_URL = os.environ.get("MGP_BASE_URL") or "https://api.maxar.com/"
31
+ CLIENT_ID = os.environ.get("MAXAR_CLIENT_ID") or "mgp"
32
+ PACKAGE = "maxar_platform"
@@ -0,0 +1,179 @@
1
+ """
2
+ API keys are an alternative to OAuth2 authentication.
3
+
4
+ They are more compact than OAuth2 bearer tokens which makes them easier to send as URL parameters.
5
+
6
+ Unlike bearer tokens which expire in two hours, API keys have a default 6 month lifespan. It is also possible to
7
+ set the expiration date as short as needed. API keys can not be renewed or extended, once they expire a new one must
8
+ be created.
9
+
10
+ API keys have the same roles and scopes as your account but can not be used on any of the administrative endpoints:
11
+
12
+ - Admin API
13
+ - API Key Service
14
+ - Oauth2 Authentication
15
+
16
+ API Key dictionaries use a custom representations that mask out the API key string when they are displayed/printed.
17
+
18
+ >>> key = generate_key("my key")
19
+ >>> print(key)
20
+
21
+ {'id': '5431dd15-3efe-418a-b04d-acdca3a4d281',
22
+ 'name': 'my key',
23
+ 'description': '',
24
+ 'api_key': '*******',
25
+ 'last_modified_date': '2023-07-20 19:44:07',
26
+ 'created_date': '2023-07-20 19:44:07',
27
+ 'expiration_date': '2024-03-12 18:33:23'}
28
+
29
+ The `api_key` value is not otherwise masked, be careful with:
30
+
31
+ >>> print(key['api_key'])
32
+ <your key will print out!>
33
+
34
+
35
+ Reference
36
+ ---------
37
+
38
+ https://developers.maxar.com/docs/authentication/guides/api-key
39
+
40
+ """
41
+
42
+ import pprint
43
+ from posixpath import join
44
+
45
+ import pendulum
46
+
47
+ from maxar_platform import BASE_URL
48
+ from maxar_platform.exceptions import MissingAPIResource
49
+ from maxar_platform.session import session
50
+
51
+ __all__ = ["generate_key", "list_keys", "delete_key", "get_key"]
52
+
53
+
54
+ KEYS_URL = join(BASE_URL, "api-key/api/v2/api-key")
55
+ KEYS_KEY = join(KEYS_URL, "id/:id")
56
+
57
+
58
+ def generate_key(name="", description="", expiration=None, lifespan=None):
59
+ """Generate a new API key
60
+
61
+ Keywords
62
+ ----------
63
+ name: str, optional
64
+ Name of key, while optional highly recommended.
65
+ description: str, optional
66
+ Description of key.
67
+ expiration: datelike object
68
+ Expiration date. Can be Date or Datetime objects, or strings of dates in common formats. Maximum lifespan is 6 months.
69
+ lifespan: int
70
+ Lifespan of keys in days if you don't want to compute an expiration date.
71
+
72
+ Returns
73
+ -------
74
+ KeyDict
75
+ Key as Python dictionary with special `str` and `repr` methods to mask keys from accidental display.
76
+
77
+ """
78
+
79
+ expires_format = "YYYY-MM-DD HH:mm:ss"
80
+
81
+ payload = {"name": name, "description": description}
82
+
83
+ if expiration and lifespan:
84
+ raise ValueError("Tokens can be given an `expiration` OR a `lifespan`, not both")
85
+
86
+ if expiration:
87
+ expires = pendulum.parse(expiration)
88
+ payload["expiration_date"] = expires.format(expires_format)
89
+
90
+ if lifespan:
91
+ expires = pendulum.now().add(days=lifespan)
92
+ payload["expiration_date"] = expires.format(expires_format)
93
+
94
+ return KeyDict(session.oauth_session.post(KEYS_URL, json=payload).json())
95
+
96
+
97
+ def list_keys():
98
+ """List keys associated with your user
99
+
100
+ Returns
101
+ -------
102
+ list of KeyDict
103
+ All of user's keys, including expired ones
104
+
105
+ """
106
+ return [KeyDict(k) for k in session.oauth_session.get(KEYS_URL).json()]
107
+
108
+
109
+ def delete_key(key_or_id):
110
+ """Delete an API key
111
+
112
+ Parameters
113
+ ----------
114
+ key: KeyDict or str
115
+ Either a key dictionary, or a key's ID string
116
+
117
+ Returns
118
+ -------
119
+ None
120
+
121
+ Note
122
+ ----
123
+ A key's ID is the 'id' value, not the 'api_key' value that is used for authentication.
124
+
125
+
126
+ """
127
+ if isinstance(key_or_id, dict):
128
+ key_id = key_or_id["id"]
129
+ else:
130
+ key_id = key_or_id
131
+ url = KEYS_KEY.replace(":id", key_id)
132
+ try:
133
+ session.oauth_session.delete(url)
134
+ except MissingAPIResource:
135
+ raise MissingAPIResource(f"API Key with ID {key_id} was not found")
136
+
137
+
138
+ def get_key(**kwargs):
139
+ """Get a key
140
+
141
+ Keywords
142
+ ----------
143
+ id: str, optional
144
+ name: str, optional
145
+ description: str, optional
146
+ api_key: str, optional
147
+
148
+ Returns
149
+ -------
150
+ KeyDict
151
+ The first key found matching the supplied keyword arguments.
152
+
153
+ Note
154
+ ----
155
+ You can match to any ONE of the KeyDict fields, however in most cases you would use ID or name.
156
+ Matching is exact, there is no support for wildcards or other regex magic at this time.
157
+
158
+ """
159
+ if len(kwargs) > 1:
160
+ raise ValueError("API keys can only be retrieved on a single property")
161
+ for k, v in kwargs.items():
162
+ for key in list_keys():
163
+ if key[k] == v:
164
+ return KeyDict(key)
165
+ raise MissingAPIResource(f"API Key with {k} `{v}` was not found")
166
+
167
+
168
+ class KeyDict(dict):
169
+ """A dictionary class for keys that masks the key value when printed or displayed"""
170
+
171
+ def __str__(self):
172
+ masked = self.copy()
173
+ masked["api_key"] = "**************"
174
+ return pprint.pformat(masked, sort_dicts=False)
175
+
176
+ def __repr__(self):
177
+ masked = self.copy()
178
+ masked["api_key"] = "*******"
179
+ return str(masked)
@@ -0,0 +1,163 @@
1
+ """ Discovery API
2
+
3
+ Discovery is Maxar's data catalog. Since it implements STAC API, this SDK uses `pystac_client` for access.
4
+
5
+ For search, open a catalog or subcatalog to get a STAC client.
6
+
7
+
8
+ from maxar_platform.discovery import open_catalog
9
+ open_catalog() # returns the root catalog, or pass a subcatalog name like "imagery"
10
+ results = stac_api_client.search(...)
11
+
12
+
13
+ Reference
14
+ ---------
15
+ https://developers.maxar.com/docs/discovery/
16
+
17
+ https://pystac-client.readthedocs.io/en/latest/usage.html#itemsearch
18
+
19
+ """
20
+
21
+ import json
22
+ import logging
23
+ from copy import deepcopy
24
+ from posixpath import join
25
+
26
+ from pygeofilter.backends.cql2_json import to_cql2
27
+ from pygeofilter.parsers.cql2_text import parse as cql2_parse
28
+ from pystac_client import Client
29
+ from pystac_client.exceptions import APIError
30
+ from pystac_client.stac_api_io import StacApiIO
31
+
32
+ from maxar_platform import BASE_URL
33
+ from maxar_platform.session import session
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+ from typing import Any, Dict, Optional
38
+
39
+ DISCOVERY_URL = join(BASE_URL, "discovery/v1/")
40
+ DISCOVERY_CATALOG = join(DISCOVERY_URL, "catalogs/:subCatalogId")
41
+
42
+ __all__ = ["stac_api_client", "open_catalog"]
43
+
44
+
45
+ class MaxarStacApiIO(StacApiIO):
46
+
47
+ area_based_calc = False
48
+
49
+ def request(
50
+ self,
51
+ href: str,
52
+ method: Optional[str] = None,
53
+ headers: Optional[Dict[str, str]] = None,
54
+ parameters: Optional[Dict[str, Any]] = None,
55
+ ) -> str:
56
+ """Makes a request to an http endpoint
57
+
58
+ Args:
59
+ href (str): The request URL
60
+ method (Optional[str], optional): The http method to use, 'GET' or 'POST'.
61
+ Defaults to None, which will result in 'GET' being used.
62
+ headers (Optional[Dict[str, str]], optional): Additional headers to include
63
+ in request. Defaults to None.
64
+ parameters (Optional[Dict[str, Any]], optional): parameters to send with
65
+ request. Defaults to None.
66
+
67
+ Raises:
68
+ APIError: raised if the server returns an error response
69
+
70
+ Return:
71
+ str: The decoded response from the endpoint
72
+
73
+ Note:
74
+ This follows the default StacApiIO call but uses the SDK's session object. It also converts
75
+ cql2 text queries to json since the Discovery API currently doesn't accept text queries in
76
+ POST queries.
77
+ """
78
+ if method == "POST":
79
+ if parameters.get("filter-lang") == "cql2-text":
80
+ ast = cql2_parse(parameters["filter"])
81
+ parameters["filter"] = json.loads(to_cql2(ast))
82
+ parameters["filter-lang"] = "cql2-json"
83
+ if self.area_based_calc and parameters:
84
+ if "bbox" in parameters or "intersects" in parameters:
85
+ parameters.update({"area-based-calc": True})
86
+ resp = session.post(url=href, headers=headers, json=parameters)
87
+ else:
88
+ params = deepcopy(parameters) or {}
89
+ if self.area_based_calc and parameters:
90
+ if "bbox" in parameters or "intersects" in parameters:
91
+ params.update({"area-based-calc": 1})
92
+ resp = session.get(url=href, headers=headers, params=params)
93
+ try:
94
+ return resp.content.decode("utf-8")
95
+ except Exception as err:
96
+ raise APIError(str(err))
97
+
98
+
99
+ def open_catalog(catalog=None):
100
+ """Open a pystac_client connection to a Discovery catalog
101
+
102
+ Parameters
103
+ ----------
104
+ catalog : str, default None
105
+ Catalog or subcatalog name to open, i.e. "imagery". If omitted the root catalog is used.
106
+
107
+ Returns
108
+ -------
109
+ pystac_client.Client
110
+ A Client object, configured to use Maxar authentication and area-based calculations
111
+
112
+ Reference
113
+ ---------
114
+ https://pystac-client.readthedocs.io/en/latest/api.html#collection-client"""
115
+
116
+ if not catalog:
117
+ url = DISCOVERY_URL
118
+ else:
119
+ url = DISCOVERY_CATALOG.replace(":subCatalogId", catalog)
120
+
121
+ client = Client.open(url, stac_io=MaxarStacApiIO())
122
+
123
+ def abc_getter(self):
124
+ return self._stac_io.area_based_calc
125
+
126
+ def abc_setter(self, val):
127
+ if type(val) is not bool:
128
+ raise ValueError("Value must be a boolean")
129
+ self._stac_io.area_based_calc = val
130
+
131
+ setattr(
132
+ client.__class__,
133
+ "area_based_calc",
134
+ property(abc_getter, abc_setter, doc="Return metadata with area-based calculations"),
135
+ )
136
+
137
+ return client
138
+
139
+
140
+ stac_api_client = open_catalog()
141
+ """ A pre-configured `pystac-client` instance ready to search the root catalog.
142
+
143
+
144
+ Attributes
145
+ ----------
146
+ area_based_calc : Bool
147
+ Toggles area-based calculations, default is False
148
+
149
+ Reference
150
+ ---------
151
+ https://pystac-client.readthedocs.io/en/latest/api.html#collection-client """
152
+
153
+
154
+ # This is handy for certain searches
155
+ CATID_TO_COLLECTION = {
156
+ "1010": "qb01",
157
+ "1020": "wv01",
158
+ "1030": "wv02",
159
+ "1040": "wv03-vnir",
160
+ "104A": "wv03-swir",
161
+ "1050": "ge01",
162
+ "1060": "ik02",
163
+ }
@@ -0,0 +1,124 @@
1
+ """ SDK Exceptions """
2
+
3
+ from datetime import datetime
4
+
5
+ __all__ = [
6
+ "MissingDependency",
7
+ "APIServerException",
8
+ "BadAPIRequest",
9
+ "MissingAPIResource",
10
+ "OversizeRequestException",
11
+ "UnAuthorizedException",
12
+ ]
13
+
14
+ #########
15
+ # General Errors
16
+ #########
17
+
18
+
19
+ class MissingDependency(Exception):
20
+ """A dependency required for this functionality has not been installed"""
21
+
22
+ pass
23
+
24
+
25
+ #########
26
+ # API Errors
27
+ #########
28
+
29
+
30
+ class APIServerException(Exception):
31
+ """The API service encountered an error"""
32
+
33
+ def __init__(self, msg, *args, **kwargs):
34
+ msg = f"{msg} ({datetime.now()})"
35
+ super().__init__(msg, *args, **kwargs)
36
+
37
+
38
+ class BadAPIRequest(Exception):
39
+ """The API request sent was incorrectly formatted"""
40
+
41
+ pass
42
+
43
+
44
+ class MissingAPIResource(Exception):
45
+ """An API request returned a 404"""
46
+
47
+ pass
48
+
49
+
50
+ class OversizeRequestException(Exception):
51
+ """The API request sent was incorrectly formatted"""
52
+
53
+ pass
54
+
55
+
56
+ class UnAuthorizedException(Exception):
57
+ """The API request's credentials were rejected"""
58
+
59
+ pass
60
+
61
+
62
+ ########
63
+ # Hook functions for Request.Session.hooks
64
+ ########
65
+
66
+
67
+ def messenger(request):
68
+ try:
69
+ payload = request.json()
70
+ message = payload.get("message")
71
+ messages = payload.get("error").get("error_messages")
72
+ if messages:
73
+ messages = "; ".join(messages)
74
+ return message or messages or request.text
75
+ except:
76
+ return request.text
77
+
78
+
79
+ def oversize_request_hook(r, *args, **kwargs):
80
+ """
81
+ Hook for handling oversize request errors from API.
82
+ """
83
+ if r.status_code == 413 and "Request Entity Too Large" in r.json()["message"]:
84
+ raise OversizeRequestException(f"{messenger(r)}, Reduce size of attributes and try again.")
85
+
86
+ return r
87
+
88
+
89
+ def bad_api_request_hook(r, *args, **kwargs):
90
+ """
91
+ Hook for handling 400 errors from API.
92
+ """
93
+ if r.status_code == 400:
94
+ raise BadAPIRequest(messenger(r))
95
+ return r
96
+
97
+
98
+ def unauth_request_hook(r, *args, **kwargs):
99
+ """
100
+ Hook for handling 401 errors from API.
101
+ """
102
+ if r.status_code in [401, 403]:
103
+ raise UnAuthorizedException(
104
+ f"Server responded with {r.status_code} '{messenger(r)}', check your credentials and try again."
105
+ )
106
+
107
+ return r
108
+
109
+
110
+ def missing_resource_hook(r, *args, **kwargs):
111
+ if r.status_code == 404:
112
+ raise MissingAPIResource(messenger(r))
113
+
114
+ return r
115
+
116
+
117
+ def api_server_request_hook(r, *args, **kwargs):
118
+ """
119
+ Hook for handling non-20X errors from API.
120
+ """
121
+ if not r.ok:
122
+ raise APIServerException(messenger(r))
123
+
124
+ return r