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 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}")