upstream-sdk 1.0.0__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.
- upstream/__init__.py +51 -0
- upstream/auth.py +153 -0
- upstream/campaigns.py +223 -0
- upstream/client.py +481 -0
- upstream/data.py +693 -0
- upstream/exceptions.py +277 -0
- upstream/measurements.py +389 -0
- upstream/py.typed +0 -0
- upstream/sensors.py +379 -0
- upstream/stations.py +282 -0
- upstream/utils.py +386 -0
- upstream_sdk-1.0.0.dist-info/METADATA +489 -0
- upstream_sdk-1.0.0.dist-info/RECORD +17 -0
- upstream_sdk-1.0.0.dist-info/WHEEL +5 -0
- upstream_sdk-1.0.0.dist-info/entry_points.txt +2 -0
- upstream_sdk-1.0.0.dist-info/licenses/LICENSE +21 -0
- upstream_sdk-1.0.0.dist-info/top_level.txt +1 -0
upstream/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Upstream Python SDK for environmental sensor data platform and CKAN integration.
|
|
3
|
+
|
|
4
|
+
This package provides a standardized toolkit for environmental researchers and organizations
|
|
5
|
+
to interact with the Upstream API and CKAN data portals.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .auth import AuthManager
|
|
9
|
+
from .campaigns import CampaignManager
|
|
10
|
+
from .client import UpstreamClient
|
|
11
|
+
from .data import DataUploader, DataValidator
|
|
12
|
+
from .exceptions import (
|
|
13
|
+
APIError,
|
|
14
|
+
AuthenticationError,
|
|
15
|
+
UploadError,
|
|
16
|
+
UpstreamError,
|
|
17
|
+
ValidationError,
|
|
18
|
+
)
|
|
19
|
+
from .stations import StationManager
|
|
20
|
+
|
|
21
|
+
__version__ = "1.0.0"
|
|
22
|
+
__author__ = "In-For-Disaster-Analytics Team"
|
|
23
|
+
__email__ = "info@tacc.utexas.edu"
|
|
24
|
+
__license__ = "MIT"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
# Core client
|
|
28
|
+
"UpstreamClient",
|
|
29
|
+
# Authentication
|
|
30
|
+
"AuthManager",
|
|
31
|
+
# Campaign management
|
|
32
|
+
"CampaignManager",
|
|
33
|
+
# Station management
|
|
34
|
+
"StationManager",
|
|
35
|
+
# Data handling
|
|
36
|
+
"DataUploader",
|
|
37
|
+
"DataValidator",
|
|
38
|
+
# CKAN integration
|
|
39
|
+
"CKANIntegration",
|
|
40
|
+
# Exceptions
|
|
41
|
+
"UpstreamError",
|
|
42
|
+
"AuthenticationError",
|
|
43
|
+
"ValidationError",
|
|
44
|
+
"UploadError",
|
|
45
|
+
"APIError",
|
|
46
|
+
# Metadata
|
|
47
|
+
"__version__",
|
|
48
|
+
"__author__",
|
|
49
|
+
"__email__",
|
|
50
|
+
"__license__",
|
|
51
|
+
]
|
upstream/auth.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication manager for Upstream SDK using OpenAPI client.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
from upstream_api_client import ApiClient, Configuration
|
|
10
|
+
from upstream_api_client.api import AuthApi
|
|
11
|
+
from upstream_api_client.rest import ApiException
|
|
12
|
+
|
|
13
|
+
from .exceptions import AuthenticationError, ConfigurationError, NetworkError
|
|
14
|
+
from .utils import ConfigManager
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AuthManager:
|
|
20
|
+
"""
|
|
21
|
+
Manages authentication with the Upstream API using OpenAPI client.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, config: ConfigManager) -> None:
|
|
25
|
+
"""
|
|
26
|
+
Initialize authentication manager.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
config: Configuration manager instance
|
|
30
|
+
"""
|
|
31
|
+
self.config = config
|
|
32
|
+
self.configuration = Configuration(host=config.base_url)
|
|
33
|
+
self.api_client: Optional[ApiClient] = None
|
|
34
|
+
self.access_token: Optional[str] = None
|
|
35
|
+
self.token_expires_at: Optional[datetime] = None
|
|
36
|
+
|
|
37
|
+
# Validate configuration
|
|
38
|
+
if not config.username or not config.password:
|
|
39
|
+
raise ConfigurationError("Username and password are required")
|
|
40
|
+
|
|
41
|
+
def authenticate(self) -> bool:
|
|
42
|
+
"""
|
|
43
|
+
Authenticate with the Upstream API.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
True if authentication successful
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
AuthenticationError: If authentication fails
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
with ApiClient(self.configuration) as api_client:
|
|
53
|
+
auth_api = AuthApi(api_client)
|
|
54
|
+
# Attempt login
|
|
55
|
+
if self.config.username is None or self.config.password is None:
|
|
56
|
+
raise AuthenticationError("Username and password are required")
|
|
57
|
+
|
|
58
|
+
response = auth_api.login_api_v1_token_post(
|
|
59
|
+
username=self.config.username,
|
|
60
|
+
password=self.config.password,
|
|
61
|
+
grant_type="password",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Store token information
|
|
65
|
+
self.access_token = response.access_token
|
|
66
|
+
self.configuration.access_token = response.access_token
|
|
67
|
+
|
|
68
|
+
# Calculate expiration time (default to 1 hour if not provided)
|
|
69
|
+
expires_in = getattr(response, "expires_in", 3600)
|
|
70
|
+
self.token_expires_at = datetime.now() + timedelta(seconds=expires_in)
|
|
71
|
+
|
|
72
|
+
logger.info("Successfully authenticated with Upstream API")
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
except ApiException as e:
|
|
76
|
+
if e.status == 401:
|
|
77
|
+
raise AuthenticationError("Invalid username or password")
|
|
78
|
+
elif e.status == 422:
|
|
79
|
+
raise AuthenticationError("Authentication request validation failed")
|
|
80
|
+
else:
|
|
81
|
+
raise AuthenticationError(f"Authentication failed: {e}")
|
|
82
|
+
except Exception as e:
|
|
83
|
+
raise NetworkError(f"Authentication request failed: {e}")
|
|
84
|
+
|
|
85
|
+
def is_authenticated(self) -> bool:
|
|
86
|
+
"""
|
|
87
|
+
Check if currently authenticated with a valid token.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
True if authenticated with valid token
|
|
91
|
+
"""
|
|
92
|
+
if not self.access_token or not self.token_expires_at:
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
# Consider token expired if it expires within 5 minutes
|
|
96
|
+
buffer_time = timedelta(minutes=5)
|
|
97
|
+
return datetime.now() < (self.token_expires_at - buffer_time)
|
|
98
|
+
|
|
99
|
+
def get_api_client(self) -> ApiClient:
|
|
100
|
+
"""
|
|
101
|
+
Get authenticated API client.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Configured API client with authentication
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
AuthenticationError: If not authenticated
|
|
108
|
+
"""
|
|
109
|
+
if not self.is_authenticated():
|
|
110
|
+
if not self.authenticate():
|
|
111
|
+
raise AuthenticationError("Failed to authenticate")
|
|
112
|
+
|
|
113
|
+
return ApiClient(self.configuration)
|
|
114
|
+
|
|
115
|
+
def get_headers(self) -> Dict[str, str]:
|
|
116
|
+
"""
|
|
117
|
+
Get authentication headers for direct requests.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Dictionary of headers including authorization
|
|
121
|
+
"""
|
|
122
|
+
if not self.is_authenticated():
|
|
123
|
+
if not self.authenticate():
|
|
124
|
+
raise AuthenticationError("Failed to authenticate")
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
"Authorization": f"Bearer {self.access_token}",
|
|
128
|
+
"Content-Type": "application/json",
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
def refresh_token(self) -> bool:
|
|
132
|
+
"""
|
|
133
|
+
Refresh authentication token.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
True if refresh successful
|
|
137
|
+
"""
|
|
138
|
+
# For now, just re-authenticate
|
|
139
|
+
# TODO: Implement proper token refresh if supported by API
|
|
140
|
+
try:
|
|
141
|
+
return self.authenticate()
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logger.warning(f"Token refresh failed: {e}")
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
def logout(self) -> None:
|
|
147
|
+
"""
|
|
148
|
+
Logout and clear authentication tokens.
|
|
149
|
+
"""
|
|
150
|
+
self.access_token = None
|
|
151
|
+
self.token_expires_at = None
|
|
152
|
+
self.configuration.access_token = None
|
|
153
|
+
logger.info("Successfully logged out")
|
upstream/campaigns.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Campaign management module for the Upstream SDK using OpenAPI client.
|
|
3
|
+
|
|
4
|
+
This module handles creation, retrieval, and management of environmental
|
|
5
|
+
monitoring campaigns using the generated OpenAPI client.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from upstream_api_client.api import CampaignsApi
|
|
11
|
+
from upstream_api_client.models import (
|
|
12
|
+
CampaignCreateResponse,
|
|
13
|
+
CampaignsIn,
|
|
14
|
+
CampaignUpdate,
|
|
15
|
+
)
|
|
16
|
+
from upstream_api_client.models.get_campaign_response import GetCampaignResponse
|
|
17
|
+
from upstream_api_client.models.list_campaigns_response_pagination import (
|
|
18
|
+
ListCampaignsResponsePagination,
|
|
19
|
+
)
|
|
20
|
+
from upstream_api_client.rest import ApiException
|
|
21
|
+
|
|
22
|
+
from .auth import AuthManager
|
|
23
|
+
from .exceptions import APIError, ValidationError
|
|
24
|
+
from .utils import get_logger
|
|
25
|
+
|
|
26
|
+
logger = get_logger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CampaignManager:
|
|
30
|
+
"""Manages campaign operations using the OpenAPI client."""
|
|
31
|
+
|
|
32
|
+
auth_manager: AuthManager
|
|
33
|
+
|
|
34
|
+
def __init__(self, auth_manager: AuthManager) -> None:
|
|
35
|
+
"""Initialize campaign manager.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
auth_manager: Authentication manager instance
|
|
39
|
+
"""
|
|
40
|
+
self.auth_manager = auth_manager
|
|
41
|
+
|
|
42
|
+
def create(self, campaign_in: CampaignsIn) -> CampaignCreateResponse:
|
|
43
|
+
"""
|
|
44
|
+
Create a new campaign.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
campaign_in: CampaignsIn model instance
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Created Campaign object
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
ValidationError: If campaign_in is not a CampaignsIn
|
|
54
|
+
APIError: If API request fails
|
|
55
|
+
"""
|
|
56
|
+
if not isinstance(campaign_in, CampaignsIn):
|
|
57
|
+
raise ValidationError(
|
|
58
|
+
"campaign_in must be a CampaignsIn instance", field="campaign_in"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
with self.auth_manager.get_api_client() as api_client:
|
|
63
|
+
campaigns_api = CampaignsApi(api_client)
|
|
64
|
+
response: CampaignCreateResponse = (
|
|
65
|
+
campaigns_api.create_campaign_api_v1_campaigns_post(
|
|
66
|
+
campaigns_in=campaign_in
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
return response
|
|
70
|
+
except ApiException as e:
|
|
71
|
+
if e.status == 422:
|
|
72
|
+
raise ValidationError(f"Campaign validation failed: {e}")
|
|
73
|
+
else:
|
|
74
|
+
raise APIError(f"Failed to create campaign: {e}", status_code=e.status)
|
|
75
|
+
except Exception as e:
|
|
76
|
+
raise APIError(f"Failed to create campaign: {e}")
|
|
77
|
+
|
|
78
|
+
def get(self, campaign_id: str) -> GetCampaignResponse:
|
|
79
|
+
"""Get campaign by ID.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
campaign_id: Campaign ID
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Campaign object
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
APIError: If API request fails or campaign not found
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
campaign_id_int = int(campaign_id)
|
|
92
|
+
|
|
93
|
+
with self.auth_manager.get_api_client() as api_client:
|
|
94
|
+
campaigns_api = CampaignsApi(api_client)
|
|
95
|
+
|
|
96
|
+
response: GetCampaignResponse = (
|
|
97
|
+
campaigns_api.get_campaign_api_v1_campaigns_campaign_id_get(
|
|
98
|
+
campaign_id=campaign_id_int
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
return response
|
|
102
|
+
|
|
103
|
+
except ValueError:
|
|
104
|
+
raise ValidationError(f"Invalid campaign ID format: {campaign_id}")
|
|
105
|
+
except ApiException as e:
|
|
106
|
+
if e.status == 404:
|
|
107
|
+
raise APIError(f"Campaign not found: {campaign_id}", status_code=404)
|
|
108
|
+
else:
|
|
109
|
+
raise APIError(f"Failed to get campaign: {e}", status_code=e.status)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
raise APIError(f"Failed to get campaign: {e}")
|
|
112
|
+
|
|
113
|
+
def list(
|
|
114
|
+
self, limit: int = 50, page: int = 1, search: Optional[str] = None
|
|
115
|
+
) -> ListCampaignsResponsePagination:
|
|
116
|
+
"""List campaigns with optional filtering.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
limit: Maximum number of campaigns to return
|
|
120
|
+
page: Page number for pagination
|
|
121
|
+
search: Search term for campaign names/descriptions
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
List of Campaign objects
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
APIError: If API request fails
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
with self.auth_manager.get_api_client() as api_client:
|
|
131
|
+
campaigns_api = CampaignsApi(api_client)
|
|
132
|
+
response: ListCampaignsResponsePagination = (
|
|
133
|
+
campaigns_api.list_campaigns_api_v1_campaigns_get(
|
|
134
|
+
limit=limit,
|
|
135
|
+
page=page,
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
logger.info(f"Retrieved {response.total} campaigns")
|
|
139
|
+
return response
|
|
140
|
+
|
|
141
|
+
except ApiException as e:
|
|
142
|
+
raise APIError(f"Failed to list campaigns: {e}", status_code=e.status)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
raise APIError(f"Failed to list campaigns: {e}")
|
|
145
|
+
|
|
146
|
+
def update(
|
|
147
|
+
self, campaign_id: str, campaign_update: CampaignUpdate
|
|
148
|
+
) -> CampaignCreateResponse:
|
|
149
|
+
"""
|
|
150
|
+
Update an existing campaign.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
campaign_id: Campaign ID
|
|
154
|
+
campaign_update: CampaignUpdate model instance
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Updated Campaign object
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
ValidationError: If campaign_update is not a CampaignUpdate
|
|
161
|
+
APIError: If API request fails
|
|
162
|
+
"""
|
|
163
|
+
if not isinstance(campaign_update, CampaignUpdate):
|
|
164
|
+
raise ValidationError(
|
|
165
|
+
"campaign_update must be a CampaignUpdate instance",
|
|
166
|
+
field="campaign_update",
|
|
167
|
+
)
|
|
168
|
+
try:
|
|
169
|
+
campaign_id_int = int(campaign_id)
|
|
170
|
+
with self.auth_manager.get_api_client() as api_client:
|
|
171
|
+
campaigns_api = CampaignsApi(api_client)
|
|
172
|
+
response: CampaignCreateResponse = (
|
|
173
|
+
campaigns_api.partial_update_campaign_api_v1_campaigns_campaign_id_patch(
|
|
174
|
+
campaign_id=campaign_id_int, campaign_update=campaign_update
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
return response
|
|
178
|
+
except ValueError:
|
|
179
|
+
raise ValidationError(f"Invalid campaign ID format: {campaign_id}")
|
|
180
|
+
except ApiException as e:
|
|
181
|
+
if e.status == 404:
|
|
182
|
+
raise APIError(f"Campaign not found: {campaign_id}", status_code=404)
|
|
183
|
+
elif e.status == 422:
|
|
184
|
+
raise ValidationError(f"Campaign validation failed: {e}")
|
|
185
|
+
else:
|
|
186
|
+
raise APIError(f"Failed to update campaign: {e}", status_code=e.status)
|
|
187
|
+
except Exception as e:
|
|
188
|
+
raise APIError(f"Failed to update campaign: {e}")
|
|
189
|
+
|
|
190
|
+
def delete(self, campaign_id: str) -> bool:
|
|
191
|
+
"""Delete a campaign.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
campaign_id: Campaign ID
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
True if successful
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
APIError: If API request fails
|
|
201
|
+
"""
|
|
202
|
+
try:
|
|
203
|
+
campaign_id_int = int(campaign_id)
|
|
204
|
+
|
|
205
|
+
with self.auth_manager.get_api_client() as api_client:
|
|
206
|
+
campaigns_api = CampaignsApi(api_client)
|
|
207
|
+
|
|
208
|
+
campaigns_api.delete_sensor_api_v1_campaigns_campaign_id_delete(
|
|
209
|
+
campaign_id=campaign_id_int
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
logger.info(f"Deleted campaign: {campaign_id}")
|
|
213
|
+
return True
|
|
214
|
+
|
|
215
|
+
except ValueError:
|
|
216
|
+
raise ValidationError(f"Invalid campaign ID format: {campaign_id}")
|
|
217
|
+
except ApiException as e:
|
|
218
|
+
if e.status == 404:
|
|
219
|
+
raise APIError(f"Campaign not found: {campaign_id}", status_code=404)
|
|
220
|
+
else:
|
|
221
|
+
raise APIError(f"Failed to delete campaign: {e}", status_code=e.status)
|
|
222
|
+
except Exception as e:
|
|
223
|
+
raise APIError(f"Failed to delete campaign: {e}")
|