hydroserverpy 1.2.1__tar.gz → 1.3.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.

Potentially problematic release.


This version of hydroserverpy might be problematic. Click here for more details.

Files changed (101) hide show
  1. {hydroserverpy-1.2.1/src/hydroserverpy.egg-info → hydroserverpy-1.3.0}/PKG-INFO +1 -1
  2. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/README.md +1 -1
  3. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/setup.cfg +1 -1
  4. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/__init__.py +1 -1
  5. hydroserverpy-1.2.1/src/hydroserverpy/api/main.py → hydroserverpy-1.3.0/src/hydroserverpy/api/client.py +52 -22
  6. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/api/models/__init__.py +1 -2
  7. hydroserverpy-1.3.0/src/hydroserverpy/api/models/base.py +207 -0
  8. hydroserverpy-1.3.0/src/hydroserverpy/api/models/etl/data_archive.py +77 -0
  9. hydroserverpy-1.3.0/src/hydroserverpy/api/models/etl/data_source.py +111 -0
  10. hydroserverpy-1.3.0/src/hydroserverpy/api/models/etl/orchestration_system.py +63 -0
  11. hydroserverpy-1.3.0/src/hydroserverpy/api/models/iam/apikey.py +96 -0
  12. hydroserverpy-1.3.0/src/hydroserverpy/api/models/iam/collaborator.py +70 -0
  13. hydroserverpy-1.3.0/src/hydroserverpy/api/models/iam/role.py +38 -0
  14. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/api/models/iam/workspace.py +58 -86
  15. hydroserverpy-1.3.0/src/hydroserverpy/api/models/sta/datastream.py +250 -0
  16. hydroserverpy-1.3.0/src/hydroserverpy/api/models/sta/observation.py +101 -0
  17. hydroserverpy-1.3.0/src/hydroserverpy/api/models/sta/observed_property.py +37 -0
  18. hydroserverpy-1.3.0/src/hydroserverpy/api/models/sta/processing_level.py +35 -0
  19. hydroserverpy-1.3.0/src/hydroserverpy/api/models/sta/result_qualifier.py +34 -0
  20. hydroserverpy-1.3.0/src/hydroserverpy/api/models/sta/sensor.py +44 -0
  21. hydroserverpy-1.3.0/src/hydroserverpy/api/models/sta/thing.py +113 -0
  22. hydroserverpy-1.3.0/src/hydroserverpy/api/models/sta/unit.py +36 -0
  23. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/api/services/__init__.py +1 -0
  24. hydroserverpy-1.3.0/src/hydroserverpy/api/services/base.py +118 -0
  25. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/api/services/etl/data_archive.py +42 -72
  26. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/api/services/etl/data_source.py +42 -72
  27. hydroserverpy-1.3.0/src/hydroserverpy/api/services/etl/orchestration_system.py +66 -0
  28. hydroserverpy-1.3.0/src/hydroserverpy/api/services/iam/role.py +38 -0
  29. hydroserverpy-1.3.0/src/hydroserverpy/api/services/iam/workspace.py +232 -0
  30. hydroserverpy-1.3.0/src/hydroserverpy/api/services/sta/datastream.py +293 -0
  31. hydroserverpy-1.3.0/src/hydroserverpy/api/services/sta/observed_property.py +82 -0
  32. hydroserverpy-1.3.0/src/hydroserverpy/api/services/sta/processing_level.py +72 -0
  33. hydroserverpy-1.3.0/src/hydroserverpy/api/services/sta/result_qualifier.py +64 -0
  34. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/api/services/sta/sensor.py +34 -48
  35. hydroserverpy-1.3.0/src/hydroserverpy/api/services/sta/thing.py +195 -0
  36. hydroserverpy-1.3.0/src/hydroserverpy/api/services/sta/unit.py +78 -0
  37. hydroserverpy-1.3.0/src/hydroserverpy/api/utils.py +22 -0
  38. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/etl/extractors/base.py +2 -4
  39. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/etl/loaders/hydroserver_loader.py +1 -0
  40. hydroserverpy-1.3.0/src/hydroserverpy/etl/timestamp_parser.py +109 -0
  41. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/etl/transformers/base.py +5 -9
  42. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/etl_csv/hydroserver_etl_csv.py +18 -24
  43. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0/src/hydroserverpy.egg-info}/PKG-INFO +1 -1
  44. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy.egg-info/SOURCES.txt +4 -2
  45. hydroserverpy-1.2.1/src/hydroserverpy/api/http.py +0 -22
  46. hydroserverpy-1.2.1/src/hydroserverpy/api/models/base.py +0 -74
  47. hydroserverpy-1.2.1/src/hydroserverpy/api/models/etl/data_archive.py +0 -105
  48. hydroserverpy-1.2.1/src/hydroserverpy/api/models/etl/data_source.py +0 -153
  49. hydroserverpy-1.2.1/src/hydroserverpy/api/models/etl/orchestration_system.py +0 -78
  50. hydroserverpy-1.2.1/src/hydroserverpy/api/models/iam/apikey.py +0 -77
  51. hydroserverpy-1.2.1/src/hydroserverpy/api/models/iam/collaborator.py +0 -34
  52. hydroserverpy-1.2.1/src/hydroserverpy/api/models/iam/role.py +0 -10
  53. hydroserverpy-1.2.1/src/hydroserverpy/api/models/sta/datastream.py +0 -342
  54. hydroserverpy-1.2.1/src/hydroserverpy/api/models/sta/observed_property.py +0 -72
  55. hydroserverpy-1.2.1/src/hydroserverpy/api/models/sta/processing_level.py +0 -50
  56. hydroserverpy-1.2.1/src/hydroserverpy/api/models/sta/result_qualifier.py +0 -49
  57. hydroserverpy-1.2.1/src/hydroserverpy/api/models/sta/sensor.py +0 -105
  58. hydroserverpy-1.2.1/src/hydroserverpy/api/models/sta/thing.py +0 -217
  59. hydroserverpy-1.2.1/src/hydroserverpy/api/models/sta/unit.py +0 -49
  60. hydroserverpy-1.2.1/src/hydroserverpy/api/services/base.py +0 -102
  61. hydroserverpy-1.2.1/src/hydroserverpy/api/services/etl/orchestration_system.py +0 -74
  62. hydroserverpy-1.2.1/src/hydroserverpy/api/services/iam/workspace.py +0 -235
  63. hydroserverpy-1.2.1/src/hydroserverpy/api/services/sta/datastream.py +0 -354
  64. hydroserverpy-1.2.1/src/hydroserverpy/api/services/sta/observed_property.py +0 -100
  65. hydroserverpy-1.2.1/src/hydroserverpy/api/services/sta/processing_level.py +0 -78
  66. hydroserverpy-1.2.1/src/hydroserverpy/api/services/sta/result_qualifier.py +0 -74
  67. hydroserverpy-1.2.1/src/hydroserverpy/api/services/sta/thing.py +0 -188
  68. hydroserverpy-1.2.1/src/hydroserverpy/api/services/sta/unit.py +0 -82
  69. hydroserverpy-1.2.1/src/hydroserverpy/etl/timestamp_parser.py +0 -75
  70. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/LICENSE +0 -0
  71. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/pyproject.toml +0 -0
  72. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/setup.py +0 -0
  73. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/api/__init__.py +0 -0
  74. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/api/models/etl/__init__.py +0 -0
  75. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/api/models/etl/orchestration_configuration.py +0 -0
  76. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/api/models/iam/__init__.py +0 -0
  77. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/api/models/iam/account.py +0 -0
  78. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/api/models/sta/__init__.py +0 -0
  79. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/api/services/etl/__init__.py +0 -0
  80. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/api/services/iam/__init__.py +0 -0
  81. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/api/services/sta/__init__.py +0 -0
  82. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/etl/__init__.py +0 -0
  83. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/etl/extractors/__init__.py +0 -0
  84. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/etl/extractors/ftp_extractor.py +0 -0
  85. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/etl/extractors/http_extractor.py +0 -0
  86. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/etl/extractors/local_file_extractor.py +0 -0
  87. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/etl/hydroserver_etl.py +0 -0
  88. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/etl/loaders/__init__.py +0 -0
  89. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/etl/loaders/base.py +0 -0
  90. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/etl/transformers/__init__.py +0 -0
  91. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/etl/transformers/csv_transformer.py +0 -0
  92. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/etl/transformers/json_transformer.py +0 -0
  93. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/etl/types.py +0 -0
  94. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/etl_csv/__init__.py +0 -0
  95. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/etl_csv/exceptions.py +0 -0
  96. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/quality/__init__.py +0 -0
  97. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy/quality/service.py +0 -0
  98. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy.egg-info/dependency_links.txt +0 -0
  99. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy.egg-info/requires.txt +0 -0
  100. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy.egg-info/top_level.txt +0 -0
  101. {hydroserverpy-1.2.1 → hydroserverpy-1.3.0}/src/hydroserverpy.egg-info/zip-safe +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hydroserverpy
3
- Version: 1.2.1
3
+ Version: 1.3.0
4
4
  Requires-Python: <4,>=3.9
5
5
  License-File: LICENSE
6
6
  Requires-Dist: requests>=2
@@ -33,7 +33,7 @@ from hydroserverpy import HydroServer
33
33
  # Initialize HydroServer connection with credentials.
34
34
  hs_api = HydroServer(
35
35
  host='https://playground.hydroserver.org',
36
- username='user@example.com',
36
+ email='user@example.com',
37
37
  password='******'
38
38
  )
39
39
  ```
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = hydroserverpy
3
- version = 1.2.1
3
+ version = 1.3.0
4
4
 
5
5
  [options]
6
6
  package_dir =
@@ -1,4 +1,4 @@
1
- from .api.main import HydroServer
1
+ from .api.client import HydroServer
2
2
  from .etl.hydroserver_etl import HydroServerETL
3
3
  from .quality import HydroServerQualityControl
4
4
 
@@ -1,8 +1,9 @@
1
1
  import requests
2
+ import json
2
3
  from typing import Optional, Tuple
3
- from hydroserverpy.api.http import raise_for_hs_status
4
4
  from hydroserverpy.api.services import (
5
5
  WorkspaceService,
6
+ RoleService,
6
7
  ThingService,
7
8
  ObservedPropertyService,
8
9
  UnitService,
@@ -20,11 +21,14 @@ class HydroServer:
20
21
  def __init__(
21
22
  self,
22
23
  host: str,
24
+ auth_route: str = "/api/auth",
25
+ base_route: str = "/api/data",
23
26
  email: Optional[str] = None,
24
27
  password: Optional[str] = None,
25
28
  apikey: Optional[str] = None,
26
29
  ):
27
30
  self.host = host.strip("/")
31
+ self.base_route = base_route
28
32
  self.auth = (
29
33
  (
30
34
  email or "__key__",
@@ -34,7 +38,7 @@ class HydroServer:
34
38
  else None
35
39
  )
36
40
 
37
- self._auth_url = f"{self.host}/api/auth/app/session"
41
+ self._auth_url = f"{self.host}{auth_route}/app/session"
38
42
 
39
43
  self._session = None
40
44
  self._timeout = 60
@@ -52,6 +56,30 @@ class HydroServer:
52
56
 
53
57
  self._session.delete(self._auth_url, timeout=self._timeout)
54
58
 
59
+ def request(self, method, path, *args, **kwargs) -> requests.Response:
60
+ """Sends a request to HydroServer's API."""
61
+
62
+ for attempt in range(2):
63
+ try:
64
+ response = getattr(self._session, method)(
65
+ f"{self.host}/{path.strip('/')}",
66
+ timeout=self._timeout,
67
+ *args,
68
+ **kwargs,
69
+ )
70
+ self._raise_for_hs_status(response)
71
+ except (
72
+ requests.exceptions.HTTPError,
73
+ requests.exceptions.ConnectionError,
74
+ ) as e:
75
+ if attempt == 0:
76
+ self._init_session()
77
+ continue
78
+ else:
79
+ raise e
80
+
81
+ return response
82
+
55
83
  def _init_session(self, auth: Optional[Tuple[str, str]] = None) -> None:
56
84
  if self._session is not None:
57
85
  self.logout()
@@ -82,29 +110,25 @@ class HydroServer:
82
110
 
83
111
  return session_token
84
112
 
85
- def request(self, method, path, *args, **kwargs) -> requests.Response:
86
- """Sends a request to HydroServer's API."""
87
-
88
- for attempt in range(2):
113
+ @staticmethod
114
+ def _raise_for_hs_status(response):
115
+ try:
116
+ response.raise_for_status()
117
+ except requests.HTTPError as e:
89
118
  try:
90
- response = getattr(self._session, method)(
91
- f"{self.host}/{path.strip('/')}",
92
- timeout=self._timeout,
93
- *args,
94
- **kwargs,
119
+ http_error_msg = (
120
+ f"{response.status_code} Client Error: "
121
+ f"{str(json.loads(response.content).get('detail'))}"
95
122
  )
96
- raise_for_hs_status(response)
97
123
  except (
98
- requests.exceptions.HTTPError,
99
- requests.exceptions.ConnectionError,
100
- ) as e:
101
- if attempt == 0:
102
- self._init_session()
103
- continue
104
- else:
105
- raise e
106
-
107
- return response
124
+ ValueError,
125
+ TypeError,
126
+ ):
127
+ http_error_msg = e
128
+ if 400 <= response.status_code < 500:
129
+ raise requests.HTTPError(http_error_msg, response=response)
130
+ else:
131
+ raise requests.HTTPError(str(e), response=response)
108
132
 
109
133
  @property
110
134
  def workspaces(self):
@@ -112,6 +136,12 @@ class HydroServer:
112
136
 
113
137
  return WorkspaceService(self)
114
138
 
139
+ @property
140
+ def roles(self):
141
+ """Utilities for managing HydroServer workspaces."""
142
+
143
+ return RoleService(self)
144
+
115
145
  @property
116
146
  def things(self):
117
147
  """Utilities for managing HydroServer things."""
@@ -5,6 +5,7 @@ from .iam.collaborator import Collaborator
5
5
  from .iam.apikey import APIKey
6
6
  from .iam.account import Account
7
7
  from .sta.datastream import Datastream
8
+ from .sta.observation import ObservationCollection
8
9
  from .sta.observed_property import ObservedProperty
9
10
  from .sta.processing_level import ProcessingLevel
10
11
  from .sta.result_qualifier import ResultQualifier
@@ -19,5 +20,3 @@ Workspace.model_rebuild()
19
20
  Role.model_rebuild()
20
21
  Collaborator.model_rebuild()
21
22
  APIKey.model_rebuild()
22
-
23
- Unit.model_rebuild()
@@ -0,0 +1,207 @@
1
+ import uuid
2
+ from typing import Type, List, Dict, Any, Optional, ClassVar, TYPE_CHECKING
3
+ from requests import Response
4
+ from dataclasses import dataclass, field
5
+ from pydantic import BaseModel, ConfigDict, PrivateAttr, Field
6
+ from pydantic.alias_generators import to_camel
7
+
8
+ if TYPE_CHECKING:
9
+ from hydroserverpy import HydroServer
10
+ from hydroserverpy.api.services.base import HydroServerBaseService
11
+
12
+
13
+ class HydroServerBaseModel(BaseModel):
14
+ uid: Optional[uuid.UUID] = Field(..., alias="id")
15
+
16
+ _client: "HydroServer" = PrivateAttr()
17
+ _service: "HydroServerBaseService" = PrivateAttr()
18
+ _server_data: Dict[str, Any] = PrivateAttr()
19
+ _editable_fields: ClassVar[set[str]] = set()
20
+
21
+ def __init__(self, *, client: "HydroServer", service: Optional["HydroServerBaseService"] = None, **data):
22
+ super().__init__(**data)
23
+
24
+ self._client = client
25
+ self._service = service
26
+ self._server_data = self.dict(by_alias=False).copy()
27
+
28
+ @classmethod
29
+ def get_route(cls):
30
+ raise NotImplementedError("Route not defined")
31
+
32
+ @property
33
+ def client(self) -> "HydroServer":
34
+ return self._client
35
+
36
+ @property
37
+ def service(self) -> "HydroServerBaseService":
38
+ return self._service
39
+
40
+ @property
41
+ def unsaved_changes(self) -> dict:
42
+ return {
43
+ k: v for k, v in self.__dict__.items()
44
+ if k in self._editable_fields and k in self._server_data and v != self._server_data[k]
45
+ }
46
+
47
+ def save(self):
48
+ """Saves changes to this resource to HydroServer."""
49
+
50
+ if not self.service:
51
+ raise NotImplementedError("Saving not enabled for this object.")
52
+
53
+ if not self.uid:
54
+ raise AttributeError("Data cannot be saved: UID is not set.")
55
+
56
+ if self.unsaved_changes:
57
+ saved_resource = self.service.update(
58
+ self.uid, **self.unsaved_changes
59
+ )
60
+ self._server_data = saved_resource.dict(by_alias=False).copy()
61
+ self.__dict__.update(saved_resource.__dict__)
62
+
63
+ def refresh(self):
64
+ """Refreshes this resource from HydroServer."""
65
+
66
+ if not self.service:
67
+ raise NotImplementedError("Refreshing not enabled for this object.")
68
+
69
+ if self.uid is None:
70
+ raise ValueError("Cannot refresh data without a valid ID.")
71
+
72
+ refreshed_resource = self.service.get(self.uid)
73
+ self._server_data = refreshed_resource.dict(by_alias=False).copy()
74
+ self.__dict__.update(refreshed_resource.__dict__)
75
+
76
+ def delete(self):
77
+ """Deletes this resource from HydroServer."""
78
+
79
+ if not self.service:
80
+ raise NotImplementedError("Deleting not enabled for this object.")
81
+
82
+ if self.uid is None:
83
+ raise AttributeError("Cannot delete data without a valid ID.")
84
+
85
+ self.service.delete(self.uid)
86
+ self.uid = None
87
+
88
+ model_config = ConfigDict(
89
+ validate_assignment=True,
90
+ populate_by_name=True,
91
+ str_strip_whitespace=True,
92
+ alias_generator=to_camel,
93
+ )
94
+
95
+
96
+ @dataclass
97
+ class HydroServerCollection:
98
+ items: List["HydroServerBaseModel"]
99
+ filters: Optional[dict[str, Any]] = None
100
+ order_by: Optional[List[str]] = None
101
+ page: Optional[int] = None
102
+ page_size: Optional[int] = None
103
+ total_pages: Optional[int] = None
104
+ total_count: Optional[int] = None
105
+
106
+ _service: Optional["HydroServerBaseService"] = field(init=False, repr=False)
107
+
108
+ def __init__(
109
+ self,
110
+ model: Type["HydroServerBaseModel"],
111
+ client: "HydroServer",
112
+ service: Optional["HydroServerBaseService"] = None,
113
+ response: Optional[Response] = None,
114
+ **data
115
+ ):
116
+ self._service = service
117
+
118
+ self.filters = data.get("filters")
119
+ self.order_by = data.get("order_by")
120
+ self.page = data.get("page") or (int(response.headers.get("X-Page")) if response else None)
121
+ self.page_size = data.get("page_size") or (int(response.headers.get("X-Page-Size")) if response else None)
122
+ self.total_pages = data.get("total_pages") or (int(response.headers.get("X-Total-Pages")) if response else None)
123
+ self.total_count = data.get("total_count") or (int(response.headers.get("X-Total-Count")) if response else None)
124
+
125
+ if "items" in data:
126
+ self.items = data["items"]
127
+ elif response is not None:
128
+ self.items = [model(client=client, **entity) for entity in response.json()]
129
+ else:
130
+ self.items = []
131
+
132
+ @property
133
+ def service(self) -> "HydroServerBaseService":
134
+ return self._service
135
+
136
+ def next_page(self):
137
+ """Fetches the next page of data from HydroServer."""
138
+
139
+ if not self._service:
140
+ raise NotImplementedError("Pagination not enabled for this collection.")
141
+
142
+ return self._service.list(
143
+ **(self.filters or {}),
144
+ page=(self.page or 0) + 1,
145
+ page_size=self.page_size or 100,
146
+ order_by=self.order_by or ...
147
+ )
148
+
149
+ def previous_page(self):
150
+ """Fetches the previous page of data from HydroServer."""
151
+
152
+ if not self._service:
153
+ raise NotImplementedError("Pagination not enabled for this collection.")
154
+
155
+ if not self.page or self.page <= 1:
156
+ return None
157
+
158
+ return self._service.list(
159
+ **(self.filters or {}),
160
+ page=self.page - 1,
161
+ page_size=self.page_size or 100,
162
+ order_by=self.order_by or ...
163
+ )
164
+
165
+ def fetch_all(self) -> "HydroServerCollection":
166
+ """Fetches all pages of data from HydroServer for this collection."""
167
+
168
+ if not self._service:
169
+ raise NotImplementedError("Pagination not enabled for this collection.")
170
+
171
+ all_items = []
172
+ current_page = self.page or 1
173
+ page_size = self.page_size or 100
174
+ total_pages = self.total_pages
175
+
176
+ page_num = 1
177
+ while total_pages is None or page_num <= total_pages:
178
+ if page_num == current_page:
179
+ all_items.extend(self.items)
180
+ else:
181
+ page = self._service.list(
182
+ **(self.filters or {}),
183
+ page=page_num,
184
+ page_size=page_size,
185
+ order_by=self.order_by or ...
186
+ )
187
+ if not page.items:
188
+ break
189
+ all_items.extend(page.items)
190
+
191
+ if page.total_pages is not None:
192
+ total_pages = page.total_pages
193
+
194
+ page_num += 1
195
+
196
+ return self.__class__(
197
+ model=type(self.items[0]) if self.items else None,
198
+ client=self.items[0].client if self.items else None,
199
+ service=self._service,
200
+ items=all_items,
201
+ filters=self.filters,
202
+ order_by=self.order_by,
203
+ page=1,
204
+ page_size=len(all_items),
205
+ total_pages=1,
206
+ total_count=len(all_items)
207
+ )
@@ -0,0 +1,77 @@
1
+ import uuid
2
+ from typing import Union, ClassVar, Optional, TYPE_CHECKING, List
3
+ from pydantic import Field
4
+ from .orchestration_system import OrchestrationSystem
5
+ from .orchestration_configuration import OrchestrationConfigurationFields
6
+ from ..sta.datastream import Datastream
7
+ from ..base import HydroServerBaseModel
8
+
9
+ if TYPE_CHECKING:
10
+ from hydroserverpy import HydroServer
11
+ from hydroserverpy.api.models import Workspace
12
+
13
+
14
+ class DataArchive(
15
+ HydroServerBaseModel, OrchestrationConfigurationFields
16
+ ):
17
+ name: str = Field(..., max_length=255)
18
+ settings: Optional[dict] = None
19
+ orchestration_system_id: uuid.UUID
20
+ workspace_id: uuid.UUID
21
+
22
+ _editable_fields: ClassVar[set[str]] = {
23
+ "name", "settings", "interval", "interval_units", "crontab", "start_time", "end_time", "last_run_successful",
24
+ "last_run_message", "last_run", "next_run", "paused"
25
+ }
26
+
27
+ def __init__(self, client: "HydroServer", **data):
28
+ super().__init__(client=client, service=client.dataarchives, **data)
29
+
30
+ self._workspace = None
31
+ self._orchestration_system = None
32
+ self._datastreams = None
33
+
34
+ @classmethod
35
+ def get_route(cls):
36
+ return "data-archives"
37
+
38
+ @property
39
+ def workspace(self) -> "Workspace":
40
+ """The workspace this data archive belongs to."""
41
+
42
+ if self._workspace is None:
43
+ self._workspace = self.client.workspaces.get(uid=self.workspace_id)
44
+
45
+ return self._workspace
46
+
47
+ @property
48
+ def orchestration_system(self) -> "OrchestrationSystem":
49
+ """The orchestration system that manages this data archive."""
50
+
51
+ if self._orchestration_system is None:
52
+ self._orchestration_system = self.client.orchestrationsystems.get(uid=self.orchestration_system_id)
53
+
54
+ return self._orchestration_system
55
+
56
+ @property
57
+ def datastreams(self) -> List["Datastream"]:
58
+ """The datastreams this data archive provides data for."""
59
+
60
+ if self._datastreams is None:
61
+ self._datastreams = self.client.datastreams.list(data_archive=self.uid, fetch_all=True).items
62
+
63
+ return self._datastreams
64
+
65
+ def add_datastream(self, datastream: Union["Datastream", uuid.UUID, str]):
66
+ """Add a datastream to this data archive."""
67
+
68
+ self.client.dataarchives.add_datastream(
69
+ uid=self.uid, datastream=datastream
70
+ )
71
+
72
+ def remove_datastream(self, datastream: Union["Datastream", uuid.UUID, str]):
73
+ """Remove a datastream from this data archive."""
74
+
75
+ self.client.dataarchives.remove_datastream(
76
+ uid=self.uid, datastream=datastream
77
+ )
@@ -0,0 +1,111 @@
1
+ import uuid
2
+ import tempfile
3
+ import requests
4
+ from typing import Union, ClassVar, Optional, TYPE_CHECKING, List
5
+ from pydantic import Field
6
+ from hydroserverpy.etl_csv.hydroserver_etl_csv import HydroServerETLCSV
7
+ from .orchestration_system import OrchestrationSystem
8
+ from .orchestration_configuration import OrchestrationConfigurationFields
9
+ from ..sta.datastream import Datastream
10
+ from ..base import HydroServerBaseModel
11
+
12
+ if TYPE_CHECKING:
13
+ from hydroserverpy import HydroServer
14
+ from hydroserverpy.api.models import Workspace
15
+
16
+
17
+ class DataSource(
18
+ HydroServerBaseModel, OrchestrationConfigurationFields
19
+ ):
20
+ name: str = Field(..., max_length=255)
21
+ settings: Optional[dict] = None
22
+ orchestration_system_id: uuid.UUID
23
+ workspace_id: uuid.UUID
24
+
25
+ _editable_fields: ClassVar[set[str]] = {
26
+ "name", "settings", "interval", "interval_units", "crontab", "start_time", "end_time", "last_run_successful",
27
+ "last_run_message", "last_run", "next_run", "paused"
28
+ }
29
+
30
+ def __init__(self, client: "HydroServer", **data):
31
+ super().__init__(client=client, service=client.datasources, **data)
32
+
33
+ self._workspace = None
34
+ self._orchestration_system = None
35
+ self._datastreams = None
36
+
37
+ @classmethod
38
+ def get_route(cls):
39
+ return "data-sources"
40
+
41
+ @property
42
+ def workspace(self) -> "Workspace":
43
+ """The workspace this data source belongs to."""
44
+
45
+ if self._workspace is None:
46
+ self._workspace = self.client.workspaces.get(uid=self.workspace_id)
47
+
48
+ return self._workspace
49
+
50
+ @property
51
+ def orchestration_system(self) -> "OrchestrationSystem":
52
+ """The orchestration system that manages this data source."""
53
+
54
+ if self._orchestration_system is None:
55
+ self._orchestration_system = self.client.orchestrationsystems.get(uid=self.orchestration_system_id)
56
+
57
+ return self._orchestration_system
58
+
59
+ @property
60
+ def datastreams(self) -> List["Datastream"]:
61
+ """The datastreams this data source provides data for."""
62
+
63
+ if self._datastreams is None:
64
+ self._datastreams = self.client.datastreams.list(data_source=self.uid, fetch_all=True).items
65
+
66
+ return self._datastreams
67
+
68
+ def add_datastream(self, datastream: Union["Datastream", uuid.UUID, str]):
69
+ """Add a datastream to this data source."""
70
+
71
+ self.client.datasources.add_datastream(
72
+ uid=self.uid, datastream=datastream
73
+ )
74
+
75
+ def remove_datastream(self, datastream: Union["Datastream", uuid.UUID, str]):
76
+ """Remove a datastream from this data source."""
77
+
78
+ self.client.datasources.remove_datastream(
79
+ uid=self.uid, datastream=datastream
80
+ )
81
+
82
+ # TODO: Replace with ETL module.
83
+ def load_data(self):
84
+ """Load data for this data source."""
85
+
86
+ if self.paused is True:
87
+ return
88
+
89
+ if self.settings["extractor"]["type"] == "local":
90
+ with open(self.settings["extractor"]["sourceUri"]) as data_file:
91
+ loader = HydroServerETLCSV(
92
+ self.client, data_file=data_file, data_source=self
93
+ )
94
+ loader.run()
95
+ elif self.settings["extractor"]["type"] == "HTTP":
96
+ with tempfile.NamedTemporaryFile(mode="w+") as temp_file:
97
+ response = requests.get(
98
+ self.settings["extractor"]["sourceUri"],
99
+ stream=True,
100
+ timeout=60,
101
+ )
102
+ response.raise_for_status()
103
+ chunk_size = 1024 * 1024 * 10 # Use a 10mb chunk size.
104
+ for chunk in response.iter_content(chunk_size=chunk_size):
105
+ if chunk:
106
+ temp_file.write(chunk.decode("utf-8"))
107
+ temp_file.seek(0)
108
+ loader = HydroServerETLCSV(
109
+ self.client, data_file=temp_file, data_source=self
110
+ )
111
+ loader.run()
@@ -0,0 +1,63 @@
1
+ import uuid
2
+ from typing import Optional, ClassVar, List, TYPE_CHECKING
3
+ from pydantic import BaseModel, Field
4
+ from ..base import HydroServerBaseModel
5
+
6
+ if TYPE_CHECKING:
7
+ from hydroserverpy import HydroServer
8
+ from hydroserverpy.api.models import Workspace, DataSource, DataArchive
9
+
10
+
11
+ class OrchestrationSystemFields(BaseModel):
12
+ name: str = Field(..., max_length=255)
13
+ orchestration_system_type: str = Field(..., max_length=255, alias="type")
14
+
15
+
16
+ class OrchestrationSystem(HydroServerBaseModel):
17
+ name: str = Field(..., max_length=255)
18
+ orchestration_system_type: str = Field(..., max_length=255, alias="type")
19
+ workspace_id: Optional[uuid.UUID] = None
20
+
21
+ _editable_fields: ClassVar[set[str]] = {"name", "orchestration_system_type"}
22
+
23
+ def __init__(self, client: "HydroServer", **data):
24
+ super().__init__(client=client, service=client.orchestrationsystems, **data)
25
+
26
+ self._workspace = None
27
+ self._datasources = None
28
+ self._dataarchives = None
29
+
30
+ @classmethod
31
+ def get_route(cls):
32
+ return "orchestration-systems"
33
+
34
+ @property
35
+ def workspace(self) -> "Workspace":
36
+ """The workspace this orchestration system belongs to."""
37
+
38
+ if self._workspace is None and self.workspace_id:
39
+ self._workspace = self.client.workspaces.get(uid=self.workspace_id)
40
+
41
+ return self._workspace
42
+
43
+ @property
44
+ def datasources(self) -> List["DataSource"]:
45
+ """The data sources associated with this workspace."""
46
+
47
+ if self._datasources is None:
48
+ self._datasources = self.client.datasources.list(
49
+ orchestration_system=self.uid, fetch_all=True
50
+ ).items
51
+
52
+ return self._datasources
53
+
54
+ @property
55
+ def dataarchives(self) -> List["DataArchive"]:
56
+ """The data archives associated with this workspace."""
57
+
58
+ if self._dataarchives is None:
59
+ self._dataarchives = self.client.dataarchives.list(
60
+ orchestration_system=self.uid, fetch_all=True
61
+ ).items
62
+
63
+ return self._dataarchives