d2spy 0.2.1__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.
Files changed (38) hide show
  1. d2spy-0.2.1/LICENSE +21 -0
  2. d2spy-0.2.1/PKG-INFO +52 -0
  3. d2spy-0.2.1/README.md +26 -0
  4. d2spy-0.2.1/d2spy/__init__.py +0 -0
  5. d2spy-0.2.1/d2spy/api_client.py +89 -0
  6. d2spy-0.2.1/d2spy/auth.py +127 -0
  7. d2spy-0.2.1/d2spy/extras/__init__.py +0 -0
  8. d2spy-0.2.1/d2spy/extras/third_party/__init__.py +0 -0
  9. d2spy-0.2.1/d2spy/extras/third_party/tusclient/__init__.py +1 -0
  10. d2spy-0.2.1/d2spy/extras/third_party/tusclient/client.py +77 -0
  11. d2spy-0.2.1/d2spy/extras/third_party/tusclient/exceptions.py +35 -0
  12. d2spy-0.2.1/d2spy/extras/third_party/tusclient/py.typed +0 -0
  13. d2spy-0.2.1/d2spy/extras/third_party/tusclient/request.py +96 -0
  14. d2spy-0.2.1/d2spy/extras/third_party/tusclient/uploader/__init__.py +1 -0
  15. d2spy-0.2.1/d2spy/extras/third_party/tusclient/uploader/baseuploader.py +246 -0
  16. d2spy-0.2.1/d2spy/extras/third_party/tusclient/uploader/uploader.py +105 -0
  17. d2spy-0.2.1/d2spy/extras/utils.py +335 -0
  18. d2spy-0.2.1/d2spy/models/__init__.py +4 -0
  19. d2spy-0.2.1/d2spy/models/data_product.py +257 -0
  20. d2spy-0.2.1/d2spy/models/data_product_collection.py +35 -0
  21. d2spy-0.2.1/d2spy/models/flight.py +308 -0
  22. d2spy-0.2.1/d2spy/models/flight_collection.py +98 -0
  23. d2spy-0.2.1/d2spy/models/job.py +31 -0
  24. d2spy-0.2.1/d2spy/models/location.py +8 -0
  25. d2spy-0.2.1/d2spy/models/project.py +200 -0
  26. d2spy-0.2.1/d2spy/models/project_collection.py +53 -0
  27. d2spy-0.2.1/d2spy/models/raw_data.py +16 -0
  28. d2spy-0.2.1/d2spy/models/user.py +30 -0
  29. d2spy-0.2.1/d2spy/schemas/__init__.py +4 -0
  30. d2spy-0.2.1/d2spy/schemas/data_product.py +37 -0
  31. d2spy-0.2.1/d2spy/schemas/flight.py +41 -0
  32. d2spy-0.2.1/d2spy/schemas/geojson.py +34 -0
  33. d2spy-0.2.1/d2spy/schemas/project.py +97 -0
  34. d2spy-0.2.1/d2spy/schemas/raw_data.py +29 -0
  35. d2spy-0.2.1/d2spy/schemas/session.py +7 -0
  36. d2spy-0.2.1/d2spy/schemas/stac_properties.py +28 -0
  37. d2spy-0.2.1/d2spy/workspace.py +162 -0
  38. d2spy-0.2.1/pyproject.toml +51 -0
d2spy-0.2.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Geospatial Data Science Lab
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.
d2spy-0.2.1/PKG-INFO ADDED
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.1
2
+ Name: d2spy
3
+ Version: 0.2.1
4
+ Summary: Python package for interacting with a Data to Science instance.
5
+ License: MIT
6
+ Keywords: python,data to science,uas
7
+ Author: Ben Hancock
8
+ Author-email: hancocb@purdue.edu
9
+ Requires-Python: >=3.9,<4.0
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Dist: exifread (>=3.0.0,<4.0.0)
20
+ Requires-Dist: geojson-pydantic (>=1.1.1,<2.0.0)
21
+ Requires-Dist: geopandas (>=1.0.1,<2.0.0)
22
+ Requires-Dist: rasterio (>=1.3.11,<2.0.0)
23
+ Requires-Dist: requests (>=2.31.0,<3.0.0)
24
+ Description-Content-Type: text/markdown
25
+
26
+ # Welcome to Data to Science Python (d2spy)
27
+
28
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
29
+
30
+ <p align="center">
31
+ <img
32
+ src="https://py.d2s.org/assets/d2s-logo.png"
33
+ alt="Data to Science logo"
34
+ width="128"
35
+ >
36
+ </p>
37
+
38
+ D2spy is a Python package for interacting with Data to Science instances.
39
+
40
+ ## Features
41
+
42
+ - Sign in to your Data to Science instance
43
+ - Access your projects, flights, and data products
44
+ - Create new projects and flights
45
+ - Upload data products to flights
46
+
47
+ ## Documentation
48
+
49
+ - [Homepage](https://py.d2s.org/)
50
+ - [Guides](https://py.d2s.org/guides)
51
+ - [API Reference](https://py.d2s.org/api_client/)
52
+
d2spy-0.2.1/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # Welcome to Data to Science Python (d2spy)
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+
5
+ <p align="center">
6
+ <img
7
+ src="https://py.d2s.org/assets/d2s-logo.png"
8
+ alt="Data to Science logo"
9
+ width="128"
10
+ >
11
+ </p>
12
+
13
+ D2spy is a Python package for interacting with Data to Science instances.
14
+
15
+ ## Features
16
+
17
+ - Sign in to your Data to Science instance
18
+ - Access your projects, flights, and data products
19
+ - Create new projects and flights
20
+ - Upload data products to flights
21
+
22
+ ## Documentation
23
+
24
+ - [Homepage](https://py.d2s.org/)
25
+ - [Guides](https://py.d2s.org/guides)
26
+ - [API Reference](https://py.d2s.org/api_client/)
File without changes
@@ -0,0 +1,89 @@
1
+ from typing import Any, Dict, List, Union
2
+
3
+ from requests import Session
4
+
5
+ from d2spy.extras.utils import pretty_print_response
6
+
7
+
8
+ class APIClient:
9
+ """Makes API requests to D2S API."""
10
+
11
+ def __init__(self, base_url: str, session: Session):
12
+ """Constructor for APIClient class.
13
+
14
+ Args:
15
+ base_url (str): Base URL for D2S instance.
16
+ session (Session): Session set by Auth.
17
+
18
+ Raises:
19
+ ValueError: Raised if access token missing from session.
20
+ """
21
+ self.base_url = base_url
22
+ self.session = session
23
+
24
+ # Check if access token in session cookies
25
+ if not self.session.cookies.get("access_token"):
26
+ raise ValueError("Session missing access token. Must sign in first.")
27
+
28
+ def make_get_request(
29
+ self, endpoint: str, **kwargs
30
+ ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
31
+ """Makes GET request to D2S API.
32
+
33
+ Args:
34
+ endpoint (str): D2S endpoint for request.
35
+
36
+ Returns:
37
+ Union[Dict, List]: JSON response from request.
38
+ """
39
+ url = self.base_url + endpoint
40
+ response = self.session.get(url, **kwargs)
41
+
42
+ if response.status_code != 200:
43
+ pretty_print_response(response)
44
+ response.raise_for_status()
45
+
46
+ return response.json()
47
+
48
+ def make_post_request(self, endpoint: str, **kwargs) -> Dict[Any, Any]:
49
+ """Make POST request to D2S API.
50
+
51
+ Args:
52
+ endpoint (str): D2S endpoint for request.
53
+
54
+ Returns:
55
+ Dict: JSON response from request.
56
+ """
57
+ url = self.base_url + endpoint
58
+ response = self.session.post(url, **kwargs)
59
+
60
+ if (
61
+ response.status_code != 200
62
+ and response.status_code != 201
63
+ and response.status_code != 202
64
+ ):
65
+ pretty_print_response(response)
66
+ response.raise_for_status()
67
+
68
+ if response.status_code == 202:
69
+ return {"status": "accepted"}
70
+
71
+ return response.json()
72
+
73
+ def make_put_request(self, endpoint: str, **kwargs) -> Dict[Any, Any]:
74
+ """Make PUT request to D2S API.
75
+
76
+ Args:
77
+ endpoint (str): D2S endpoint for request.
78
+
79
+ Returns:
80
+ Dict: JSON response from request.
81
+ """
82
+ url = self.base_url + endpoint
83
+ response = self.session.put(url, **kwargs)
84
+
85
+ if response.status_code != 200:
86
+ pretty_print_response(response)
87
+ response.raise_for_status()
88
+
89
+ return response.json()
@@ -0,0 +1,127 @@
1
+ import getpass
2
+ import os
3
+ import requests
4
+ from typing import Optional
5
+
6
+ from d2spy.extras.utils import pretty_print_response
7
+ from d2spy.models.user import User
8
+ from d2spy.schemas.session import D2SpySession
9
+
10
+
11
+ class Auth:
12
+ """Authenticates with D2S."""
13
+
14
+ def __init__(self, base_url: str) -> None:
15
+ """Constructor for Auth class.
16
+
17
+ Args:
18
+ base_url (str): Base URL for D2S instance.
19
+
20
+ Raises:
21
+ ValueError: Raised if unable to communicate with host.
22
+ """
23
+ self.base_url: str = base_url
24
+
25
+ if is_valid_base_url(self.base_url) is False:
26
+ raise ValueError("unable to connect to provided host")
27
+
28
+ self.session: D2SpySession = D2SpySession()
29
+
30
+ def login(
31
+ self, email: Optional[str] = None, password: Optional[str] = None
32
+ ) -> Optional[D2SpySession]:
33
+ """Login to D2S platform with email and password. Alternatively, use
34
+ environment variables `D2S_EMAIL` and `D2S_PASSWORD` to set email and password.
35
+ If the password is not passed as an argument and `D2S_PASSWORD` is not set,
36
+ `getpass` will be used to prompt user for password.
37
+
38
+ Args:
39
+ email Optional[str]: Email address used to sign in to D2S.
40
+ password Optional[str]: Password used to sign in to D2S.
41
+
42
+ Returns:
43
+ Optional[D2SpySession]: Session with user access cookie.
44
+ """
45
+ # Check for email environment variable if not provided as argument
46
+ if not email:
47
+ email = os.environ.get("D2S_EMAIL")
48
+ if not email:
49
+ raise ValueError(
50
+ "Must provide 'email' to login method as argument or set email "
51
+ "as environment variable 'D2S_EMAIL'"
52
+ )
53
+ # Check for password environment variable if not provided as argument
54
+ if not password:
55
+ password = os.environ.get("D2S_PASSWORD")
56
+ # Request password from user if not set as environment variable
57
+ if not password:
58
+ password = getpass.getpass(prompt="Enter your D2S password:")
59
+ # Credentials that will be sent to D2S auth API
60
+ credentials = {"username": email, "password": password}
61
+ # URL for D2S access-token endpoint
62
+ url = f"{self.base_url}/api/v1/auth/access-token"
63
+ # Post credentials to access-token endpoint
64
+ response = requests.post(url, data=credentials)
65
+ # JWT access token returned for successful request
66
+ if response.status_code == 200 and "access_token" in response.cookies:
67
+ # Add JWT access token to session cookies
68
+ self.session.cookies.set("access_token", response.cookies["access_token"])
69
+ # Fetch user object associated with access token
70
+ user = self.get_current_user()
71
+ # Return dictionary of user attributes and values
72
+ if user:
73
+ # Check if user has api key and set it to session header if so
74
+ if hasattr(user, "api_access_token") and user.api_access_token:
75
+ self.session.d2s_data = {"API_KEY": user.api_access_token}
76
+ return self.session
77
+ else:
78
+ return None
79
+ else:
80
+ # Print response if request fails
81
+ pretty_print_response(response)
82
+ return None
83
+
84
+ def logout(self) -> None:
85
+ """Logout of D2S platform."""
86
+ # Delete access-token cookie from session and end session
87
+ self.session.cookies.clear(domain="", path="/", name="access_token")
88
+ self.session.close()
89
+ print("session ended")
90
+
91
+ def get_current_user(self) -> Optional[User]:
92
+ """Get user object for logged in user.
93
+
94
+ Returns:
95
+ Optional[User]: User object or None.
96
+ """
97
+ # D2S endpoint for fetching user object for signed in user
98
+ url = f"{self.base_url}/api/v1/users/current"
99
+ # Request user object from D2S instance
100
+ response = self.session.get(url)
101
+ # Return user object if request successful
102
+ if response.status_code == 200:
103
+ return User.from_dict(response.json())
104
+ else:
105
+ # Print response if request fails
106
+ pretty_print_response(response)
107
+ return None
108
+
109
+
110
+ def is_valid_base_url(base_url: str) -> bool:
111
+ """Return true if base_url returns HTTP 200 else false.
112
+
113
+ Args:
114
+ base_url (str): Base URL for D2S instance.
115
+
116
+ Returns:
117
+ bool: Returns True if D2S instance returns status OK, otherwise False
118
+ """
119
+ response: Optional[requests.Response] = None
120
+ try:
121
+ response = requests.get(f"{base_url}/api/v1/health")
122
+ except requests.exceptions.ConnectionError:
123
+ response = None
124
+ finally:
125
+ if response and response.status_code == 200:
126
+ return True
127
+ return False
File without changes
File without changes
@@ -0,0 +1 @@
1
+ __version__ = "1.0.3"
@@ -0,0 +1,77 @@
1
+ from typing import Dict, Optional
2
+
3
+ from d2spy.extras.third_party.tusclient.uploader import Uploader
4
+
5
+
6
+ class TusClient:
7
+ """
8
+ Object representation of Tus client.
9
+
10
+ :Attributes:
11
+ - url (str):
12
+ represents the tus server's create extension url. On instantiation this argument
13
+ must be passed to the constructor.
14
+ - headers (dict):
15
+ This can be used to set the server specific headers. These headers would be sent
16
+ along with every request made by the client to the server. This may be used to set
17
+ authentication headers. These headers should not include headers required by tus
18
+ protocol. If not set this defaults to an empty dictionary.
19
+ - cookies (dict):
20
+ This can be used to set the server specific cookies. These cookies would be sent
21
+ along with every request made by the client to the server. This may be used to set
22
+ authorization cookies.
23
+
24
+ :Constructor Args:
25
+ - url (str)
26
+ - headers (Optional[dict])
27
+ - cookies (Optional[dict])
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ url: str,
33
+ headers: Optional[Dict[str, str]] = None,
34
+ cookies: Optional[Dict[str, str]] = None,
35
+ ):
36
+ self.url = url
37
+ self.headers = headers or {}
38
+ self.cookies = cookies or {}
39
+
40
+ def set_headers(self, headers: Dict[str, str]):
41
+ """
42
+ Set tus client headers.
43
+
44
+ Update and/or set new headers that would be sent along with every request made
45
+ to the server.
46
+
47
+ :Args:
48
+ - headers (dict):
49
+ key, value pairs of the headers to be set. This argument is required.
50
+ """
51
+ self.headers.update(headers)
52
+
53
+ def set_cookies(self, cookies: Dict[str, str]):
54
+ """
55
+ Set tus client cookies.
56
+
57
+ Update and/or set new cookies that would be sent along with every request made
58
+ to the server.
59
+
60
+ :Args:
61
+ - cookies (dict):
62
+ key, value pairs of the cookies to be set. This argument is required.
63
+ """
64
+ self.cookies.update(cookies)
65
+
66
+ def uploader(self, *args, **kwargs) -> Uploader:
67
+ """
68
+ Return uploader instance pointing at current client instance.
69
+
70
+ Return uploader instance with which you can control the upload of a specific
71
+ file. The current instance of the tus client is passed to the uploader on creation.
72
+
73
+ :Args:
74
+ see tusclient.uploader.Uploader for required and optional arguments.
75
+ """
76
+ kwargs["client"] = self
77
+ return Uploader(*args, **kwargs)
@@ -0,0 +1,35 @@
1
+ """
2
+ Global Tusclient exception and warning classes.
3
+ """
4
+
5
+
6
+ class TusCommunicationError(Exception):
7
+ """
8
+ Should be raised when communications with tus-server behaves
9
+ unexpectedly.
10
+
11
+ :Attributes:
12
+ - message (str):
13
+ Main message of the exception
14
+ - status_code (int):
15
+ Status code of response indicating an error
16
+ - response_content (str):
17
+ Content of response indicating an error
18
+ :Constructor Args:
19
+ - message (Optional[str])
20
+ - status_code (Optional[int])
21
+ - response_content (Optional[str])
22
+ """
23
+
24
+ def __init__(self, message, status_code=None, response_content=None):
25
+ default_message = "Communication with tus server failed with status {}".format(
26
+ status_code
27
+ )
28
+ message = message or default_message
29
+ super(TusCommunicationError, self).__init__(message)
30
+ self.status_code = status_code
31
+ self.response_content = response_content
32
+
33
+
34
+ class TusUploadFailed(TusCommunicationError):
35
+ """Should be raised when an attempted upload fails"""
@@ -0,0 +1,96 @@
1
+ from typing import Optional
2
+ import base64
3
+ from functools import wraps
4
+
5
+ import requests
6
+
7
+ from d2spy.extras.third_party.tusclient.exceptions import (
8
+ TusUploadFailed,
9
+ TusCommunicationError,
10
+ )
11
+
12
+
13
+ # Catches requests exceptions and throws custom tuspy errors.
14
+ def catch_requests_error(func):
15
+ """Deocrator to catch requests exceptions"""
16
+
17
+ @wraps(func)
18
+ def _wrapper(*args, **kwargs):
19
+ try:
20
+ return func(*args, **kwargs)
21
+ except requests.exceptions.RequestException as error:
22
+ raise TusCommunicationError(error)
23
+
24
+ return _wrapper
25
+
26
+
27
+ class BaseTusRequest:
28
+ """
29
+ Http Request Abstraction.
30
+
31
+ Sets up tus custom http request on instantiation.
32
+
33
+ requires argument 'uploader' an instance of tusclient.uploader.Uploader
34
+ on instantiation.
35
+
36
+ :Attributes:
37
+ - response_headers (dict)
38
+ - file (file):
39
+ The file that is being uploaded.
40
+ """
41
+
42
+ def __init__(self, uploader):
43
+ self._url = uploader.url
44
+ self.response_headers = {}
45
+ self.status_code = None
46
+ self.response_content = None
47
+ self.verify_tls_cert = bool(uploader.verify_tls_cert)
48
+ self.file = uploader.get_file_stream()
49
+ self.file.seek(uploader.offset)
50
+
51
+ self._request_headers = {
52
+ "upload-offset": str(uploader.offset),
53
+ "Content-Type": "application/offset+octet-stream",
54
+ }
55
+ self._request_cookies = {}
56
+ self._request_headers.update(uploader.get_headers())
57
+ self._request_cookies.update(uploader.get_cookies())
58
+ self._content_length = uploader.get_request_length()
59
+ self._upload_checksum = uploader.upload_checksum
60
+ self._checksum_algorithm = uploader.checksum_algorithm
61
+ self._checksum_algorithm_name = uploader.checksum_algorithm_name
62
+
63
+ def add_checksum(self, chunk: bytes):
64
+ if self._upload_checksum:
65
+ self._request_headers["upload-checksum"] = " ".join(
66
+ (
67
+ self._checksum_algorithm_name,
68
+ base64.b64encode(self._checksum_algorithm(chunk).digest()).decode(
69
+ "ascii"
70
+ ),
71
+ )
72
+ )
73
+
74
+
75
+ class TusRequest(BaseTusRequest):
76
+ """Class to handle async Tus upload requests"""
77
+
78
+ def perform(self):
79
+ """
80
+ Perform actual request.
81
+ """
82
+ try:
83
+ chunk = self.file.read(self._content_length)
84
+ self.add_checksum(chunk)
85
+ resp = requests.patch(
86
+ self._url,
87
+ data=chunk,
88
+ headers=self._request_headers,
89
+ cookies=self._request_cookies,
90
+ verify=self.verify_tls_cert,
91
+ )
92
+ self.status_code = resp.status_code
93
+ self.response_content = resp.content
94
+ self.response_headers = {k.lower(): v for k, v in resp.headers.items()}
95
+ except requests.exceptions.RequestException as error:
96
+ raise TusUploadFailed(error)
@@ -0,0 +1 @@
1
+ from d2spy.extras.third_party.tusclient.uploader.uploader import Uploader