hydroserverpy 0.2.5__py3-none-any.whl → 0.4.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.
Potentially problematic release.
This version of hydroserverpy might be problematic. Click here for more details.
- hydroserverpy/__init__.py +6 -15
- hydroserverpy/core/endpoints/__init__.py +9 -0
- hydroserverpy/core/endpoints/base.py +146 -0
- hydroserverpy/core/endpoints/data_loaders.py +93 -0
- hydroserverpy/core/endpoints/data_sources.py +93 -0
- hydroserverpy/core/endpoints/datastreams.py +225 -0
- hydroserverpy/core/endpoints/observed_properties.py +111 -0
- hydroserverpy/core/endpoints/processing_levels.py +111 -0
- hydroserverpy/core/endpoints/result_qualifiers.py +111 -0
- hydroserverpy/core/endpoints/sensors.py +111 -0
- hydroserverpy/core/endpoints/things.py +261 -0
- hydroserverpy/core/endpoints/units.py +111 -0
- hydroserverpy/{components → core/schemas}/__init__.py +1 -2
- hydroserverpy/core/schemas/base.py +124 -0
- hydroserverpy/core/schemas/data_loaders.py +73 -0
- hydroserverpy/core/schemas/data_sources.py +223 -0
- hydroserverpy/core/schemas/datastreams.py +330 -0
- hydroserverpy/core/schemas/observed_properties.py +43 -0
- hydroserverpy/core/schemas/processing_levels.py +31 -0
- hydroserverpy/core/schemas/result_qualifiers.py +26 -0
- hydroserverpy/core/schemas/sensors.py +68 -0
- hydroserverpy/core/schemas/things.py +346 -0
- hydroserverpy/core/schemas/units.py +29 -0
- hydroserverpy/core/service.py +200 -0
- hydroserverpy/etl/__init__.py +21 -0
- hydroserverpy/etl/extractors/__init__.py +0 -0
- hydroserverpy/etl/extractors/base.py +13 -0
- hydroserverpy/etl/extractors/ftp_extractor.py +50 -0
- hydroserverpy/etl/extractors/http_extractor.py +84 -0
- hydroserverpy/etl/extractors/local_file_extractor.py +25 -0
- hydroserverpy/etl/hydroserver_etl.py +40 -0
- hydroserverpy/etl/loaders/__init__.py +0 -0
- hydroserverpy/etl/loaders/base.py +13 -0
- hydroserverpy/etl/loaders/hydroserver_loader.py +68 -0
- hydroserverpy/etl/transformers/__init__.py +0 -0
- hydroserverpy/etl/transformers/base.py +52 -0
- hydroserverpy/etl/transformers/csv_transformer.py +88 -0
- hydroserverpy/etl/transformers/json_transformer.py +62 -0
- hydroserverpy/etl/types.py +7 -0
- hydroserverpy/etl_csv/__init__.py +0 -0
- hydroserverpy/{etl.py → etl_csv/hydroserver_etl_csv.py} +118 -95
- hydroserverpy/quality/__init__.py +1 -0
- hydroserverpy/quality/service.py +405 -0
- hydroserverpy-0.4.0.dist-info/METADATA +18 -0
- hydroserverpy-0.4.0.dist-info/RECORD +51 -0
- {hydroserverpy-0.2.5.dist-info → hydroserverpy-0.4.0.dist-info}/WHEEL +1 -1
- hydroserverpy/components/data_loaders.py +0 -67
- hydroserverpy/components/data_sources.py +0 -98
- hydroserverpy/components/datastreams.py +0 -47
- hydroserverpy/components/observed_properties.py +0 -48
- hydroserverpy/components/processing_levels.py +0 -48
- hydroserverpy/components/result_qualifiers.py +0 -48
- hydroserverpy/components/sensors.py +0 -48
- hydroserverpy/components/things.py +0 -48
- hydroserverpy/components/units.py +0 -48
- hydroserverpy/components/users.py +0 -28
- hydroserverpy/main.py +0 -62
- hydroserverpy/models.py +0 -218
- hydroserverpy/schemas/data_loaders.py +0 -27
- hydroserverpy/schemas/data_sources.py +0 -58
- hydroserverpy/schemas/datastreams.py +0 -56
- hydroserverpy/schemas/observed_properties.py +0 -33
- hydroserverpy/schemas/processing_levels.py +0 -33
- hydroserverpy/schemas/result_qualifiers.py +0 -32
- hydroserverpy/schemas/sensors.py +0 -39
- hydroserverpy/schemas/things.py +0 -107
- hydroserverpy/schemas/units.py +0 -32
- hydroserverpy/schemas/users.py +0 -28
- hydroserverpy/service.py +0 -170
- hydroserverpy/utils.py +0 -37
- hydroserverpy-0.2.5.dist-info/METADATA +0 -15
- hydroserverpy-0.2.5.dist-info/RECORD +0 -35
- /hydroserverpy/{schemas → core}/__init__.py +0 -0
- /hydroserverpy/{exceptions.py → etl_csv/exceptions.py} +0 -0
- {hydroserverpy-0.2.5.dist-info → hydroserverpy-0.4.0.dist-info}/LICENSE +0 -0
- {hydroserverpy-0.2.5.dist-info → hydroserverpy-0.4.0.dist-info}/top_level.txt +0 -0
- {hydroserverpy-0.2.5.dist-info → hydroserverpy-0.4.0.dist-info}/zip-safe +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from typing import List, Union, TYPE_CHECKING
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
from hydroserverpy.core.endpoints.base import HydroServerEndpoint, expand_docstring
|
|
4
|
+
from hydroserverpy.core.schemas import Unit
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from hydroserverpy.core.service import HydroServer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class UnitEndpoint(HydroServerEndpoint):
|
|
11
|
+
"""
|
|
12
|
+
An endpoint for interacting with unit entities in the HydroServer service.
|
|
13
|
+
|
|
14
|
+
:ivar _model: The model class associated with this endpoint, set to `Unit`.
|
|
15
|
+
:ivar _api_route: The base route of the API, derived from the service.
|
|
16
|
+
:ivar _endpoint_route: The specific route of the endpoint, set to `'units'`.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, service: "HydroServer"):
|
|
20
|
+
"""
|
|
21
|
+
Initialize the UnitEndpoint.
|
|
22
|
+
|
|
23
|
+
:param service: The HydroServer service instance to use for requests.
|
|
24
|
+
:type service: HydroServer
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
super().__init__(service)
|
|
28
|
+
self._model = Unit
|
|
29
|
+
self._api_route = self._service.api_route
|
|
30
|
+
self._endpoint_route = "units"
|
|
31
|
+
|
|
32
|
+
def list(
|
|
33
|
+
self,
|
|
34
|
+
include_owned: bool = True,
|
|
35
|
+
include_unowned: bool = True,
|
|
36
|
+
include_templates: bool = True,
|
|
37
|
+
) -> List[Unit]:
|
|
38
|
+
"""
|
|
39
|
+
Retrieve a collection of units.
|
|
40
|
+
|
|
41
|
+
:param include_owned: Whether to include owned observed properties.
|
|
42
|
+
:param include_unowned: Whether to include unowned observed properties.
|
|
43
|
+
:param include_templates: Whether to include template observed properties.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
if (
|
|
47
|
+
include_owned is True
|
|
48
|
+
and include_unowned is True
|
|
49
|
+
and include_templates is True
|
|
50
|
+
):
|
|
51
|
+
owner = "anyUserOrNoUser"
|
|
52
|
+
elif (
|
|
53
|
+
include_owned is True
|
|
54
|
+
and include_unowned is True
|
|
55
|
+
and include_templates is False
|
|
56
|
+
):
|
|
57
|
+
owner = "anyUser"
|
|
58
|
+
elif (
|
|
59
|
+
include_owned is True
|
|
60
|
+
and include_unowned is False
|
|
61
|
+
and include_templates is True
|
|
62
|
+
):
|
|
63
|
+
owner = "currentUserOrNoUser"
|
|
64
|
+
elif (
|
|
65
|
+
include_owned is True
|
|
66
|
+
and include_unowned is False
|
|
67
|
+
and include_templates is False
|
|
68
|
+
):
|
|
69
|
+
owner = "currentUser"
|
|
70
|
+
elif (
|
|
71
|
+
include_owned is False
|
|
72
|
+
and include_unowned is False
|
|
73
|
+
and include_templates is True
|
|
74
|
+
):
|
|
75
|
+
owner = "noUser"
|
|
76
|
+
else:
|
|
77
|
+
return []
|
|
78
|
+
|
|
79
|
+
return super()._get(params={"owner": owner})
|
|
80
|
+
|
|
81
|
+
@expand_docstring(include_uid=True)
|
|
82
|
+
def get(self, uid: Union[UUID, str]) -> Unit:
|
|
83
|
+
"""
|
|
84
|
+
Retrieve a unit owned by the logged-in user.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
return super()._get(uid)
|
|
88
|
+
|
|
89
|
+
@expand_docstring(model=Unit)
|
|
90
|
+
def create(self, **kwargs) -> Unit:
|
|
91
|
+
"""
|
|
92
|
+
Create a new unit in HydroServer.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
return super()._post(**kwargs)
|
|
96
|
+
|
|
97
|
+
@expand_docstring(model=Unit, include_uid=True)
|
|
98
|
+
def update(self, uid: Union[UUID, str], **kwargs) -> Unit:
|
|
99
|
+
"""
|
|
100
|
+
Update an existing unit in HydroServer.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
return super()._patch(uid=uid, **kwargs)
|
|
104
|
+
|
|
105
|
+
@expand_docstring(include_uid=True)
|
|
106
|
+
def delete(self, uid: Union[UUID, str]) -> None:
|
|
107
|
+
"""
|
|
108
|
+
Delete an existing unit in HydroServer.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
super()._delete(uid=uid)
|
|
@@ -5,6 +5,5 @@ from .observed_properties import ObservedProperty
|
|
|
5
5
|
from .processing_levels import ProcessingLevel
|
|
6
6
|
from .result_qualifiers import ResultQualifier
|
|
7
7
|
from .sensors import Sensor
|
|
8
|
-
from .things import Thing
|
|
8
|
+
from .things import Thing, Tag, Photo, Archive
|
|
9
9
|
from .units import Unit
|
|
10
|
-
from .users import User
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from pydantic import (
|
|
2
|
+
BaseModel,
|
|
3
|
+
PrivateAttr,
|
|
4
|
+
AliasGenerator,
|
|
5
|
+
AliasChoices,
|
|
6
|
+
computed_field,
|
|
7
|
+
)
|
|
8
|
+
from pydantic.alias_generators import to_camel
|
|
9
|
+
from uuid import UUID
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
base_alias_generator = AliasGenerator(
|
|
14
|
+
serialization_alias=lambda field_name: to_camel(field_name),
|
|
15
|
+
validation_alias=lambda field_name: AliasChoices(to_camel(field_name), field_name),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HydroServerBaseModel(BaseModel):
|
|
20
|
+
"""
|
|
21
|
+
A base model for HydroServer entities that provides common attributes and functionality for HydroServer data.
|
|
22
|
+
|
|
23
|
+
:ivar _uid: A private attribute for storing the unique identifier (UUID) of the model.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
_uid: Optional[UUID] = PrivateAttr()
|
|
27
|
+
|
|
28
|
+
def __init__(self, _uid: Optional[UUID] = None, **data):
|
|
29
|
+
"""
|
|
30
|
+
Initialize a HydroServerBaseModel instance.
|
|
31
|
+
|
|
32
|
+
:param _uid: The unique identifier for the model.
|
|
33
|
+
:type _uid: Optional[UUID]
|
|
34
|
+
:param data: Additional attributes for the model.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
super().__init__(**data)
|
|
38
|
+
self._uid = _uid
|
|
39
|
+
|
|
40
|
+
@computed_field
|
|
41
|
+
@property
|
|
42
|
+
def uid(self) -> Optional[UUID]:
|
|
43
|
+
"""
|
|
44
|
+
The unique identifier (UUID) of the model.
|
|
45
|
+
|
|
46
|
+
:return: The UUID of the model.
|
|
47
|
+
:rtype: Optional[UUID]
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
return self._uid
|
|
51
|
+
|
|
52
|
+
class Config:
|
|
53
|
+
alias_generator = base_alias_generator
|
|
54
|
+
validate_assignment = True
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class HydroServerCoreModel(HydroServerBaseModel):
|
|
58
|
+
"""
|
|
59
|
+
A core model for HydroServer entities that includes methods for data manipulation and persistence.
|
|
60
|
+
|
|
61
|
+
:ivar _original_data: A private attribute storing the original data used to initialize the model.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
_original_data: Optional[dict] = PrivateAttr()
|
|
65
|
+
|
|
66
|
+
def __init__(self, _endpoint, _uid: Optional[UUID] = None, **data):
|
|
67
|
+
"""
|
|
68
|
+
Initialize a HydroServerCoreModel instance.
|
|
69
|
+
|
|
70
|
+
:param _endpoint: The endpoint associated with the model.
|
|
71
|
+
:param _uid: The unique identifier for the model.
|
|
72
|
+
:type _uid: Optional[UUID]
|
|
73
|
+
:param data: Additional attributes for the model.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
super().__init__(_uid=_uid, **data)
|
|
77
|
+
self._endpoint = _endpoint
|
|
78
|
+
self._original_data = self.dict(by_alias=False).copy()
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def _patch_data(self) -> dict:
|
|
82
|
+
"""
|
|
83
|
+
Generate a dictionary of modified data that needs to be patched on the server.
|
|
84
|
+
|
|
85
|
+
:return: A dictionary of modified attributes.
|
|
86
|
+
:rtype: dict
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
key: getattr(self, key)
|
|
91
|
+
for key, value in self._original_data.items()
|
|
92
|
+
if hasattr(self, key) and getattr(self, key) != value
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
def refresh(self) -> None:
|
|
96
|
+
"""
|
|
97
|
+
Refresh the model with the latest data from the server.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
entity = self._endpoint.get(uid=self.uid).model_dump(exclude=["uid"])
|
|
101
|
+
self._original_data = entity.dict(by_alias=False, exclude=["uid"])
|
|
102
|
+
self.__dict__.update(self._original_data)
|
|
103
|
+
|
|
104
|
+
def save(self) -> None:
|
|
105
|
+
"""
|
|
106
|
+
Save the current state of the model to the server by updating modified attributes.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
if self._patch_data:
|
|
110
|
+
entity = self._endpoint.update(uid=self.uid, **self._patch_data)
|
|
111
|
+
self._original_data = entity.dict(by_alias=False, exclude=["uid"])
|
|
112
|
+
self.__dict__.update(self._original_data)
|
|
113
|
+
|
|
114
|
+
def delete(self) -> None:
|
|
115
|
+
"""
|
|
116
|
+
Delete the model from the server.
|
|
117
|
+
|
|
118
|
+
:raises AttributeError: If the model's UID is not set.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
if not self._uid:
|
|
122
|
+
raise AttributeError("This resource cannot be deleted: UID is not set.")
|
|
123
|
+
self._endpoint.delete(uid=self._uid)
|
|
124
|
+
self._uid = None
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
from typing import Optional, List, TYPE_CHECKING
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
from hydroserverpy.core.schemas.base import HydroServerCoreModel
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from hydroserverpy.core.schemas.data_sources import DataSource
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DataLoaderFields(BaseModel):
|
|
11
|
+
name: str = Field(
|
|
12
|
+
...,
|
|
13
|
+
strip_whitespace=True,
|
|
14
|
+
max_length=255,
|
|
15
|
+
description="The name of the data loader.",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DataLoader(HydroServerCoreModel, DataLoaderFields):
|
|
20
|
+
"""
|
|
21
|
+
A model representing a DataLoader, extending the core functionality of HydroServerCoreModel with additional
|
|
22
|
+
properties and methods.
|
|
23
|
+
|
|
24
|
+
:ivar _data_sources: A private attribute to cache the list of data sources associated with the DataLoader.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, _endpoint, _uid: Optional[UUID] = None, **data):
|
|
28
|
+
"""
|
|
29
|
+
Initialize a DataLoader instance.
|
|
30
|
+
|
|
31
|
+
:param _endpoint: The endpoint associated with the data loader.
|
|
32
|
+
:type _endpoint: str
|
|
33
|
+
:param _uid: The unique identifier for the data loader.
|
|
34
|
+
:type _uid: Optional[UUID]
|
|
35
|
+
:param data: Additional attributes for the data loader.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
super().__init__(_endpoint=_endpoint, _uid=_uid, **data)
|
|
39
|
+
self._data_sources = None
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def data_sources(self) -> List["DataSource"]:
|
|
43
|
+
"""
|
|
44
|
+
The data sources associated with the data loader. If not already cached, fetch the data sources from the server.
|
|
45
|
+
|
|
46
|
+
:return: A list of data sources associated with the data loader.
|
|
47
|
+
:rtype: List[DataSource]
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
if self._data_sources is None:
|
|
51
|
+
self._data_sources = self._endpoint.list_data_sources(uid=self.uid)
|
|
52
|
+
|
|
53
|
+
return self._data_sources
|
|
54
|
+
|
|
55
|
+
def refresh(self) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Refresh the data loader with the latest data from the server and update cached data sources.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
entity = self._endpoint.get(uid=self.uid).model_dump(exclude=["uid"])
|
|
61
|
+
self._original_data = entity
|
|
62
|
+
self.__dict__.update(entity)
|
|
63
|
+
if self._data_sources is not None:
|
|
64
|
+
self._data_sources = self._endpoint.list_data_sources(uid=self.uid)
|
|
65
|
+
|
|
66
|
+
def load_observations(self) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Load observations data from a local file or a remote URL into HydroServer using all data sources associated with
|
|
69
|
+
this data loader.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
for data_source in self.data_sources:
|
|
73
|
+
data_source.load_observations()
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
import io
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
from typing import Optional, Literal, Union, List, TYPE_CHECKING
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
from urllib.request import urlopen
|
|
8
|
+
from hydroserverpy.core.schemas.base import HydroServerCoreModel
|
|
9
|
+
from hydroserverpy.etl_csv.hydroserver_etl_csv import HydroServerETLCSV
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from hydroserverpy.core.schemas.data_loaders import DataLoader
|
|
13
|
+
from hydroserverpy.core.schemas.datastreams import Datastream
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DataSourceFields(BaseModel):
|
|
17
|
+
name: str = Field(
|
|
18
|
+
...,
|
|
19
|
+
strip_whitespace=True,
|
|
20
|
+
max_length=255,
|
|
21
|
+
description="The name of the data source.",
|
|
22
|
+
)
|
|
23
|
+
path: Optional[str] = Field(
|
|
24
|
+
None,
|
|
25
|
+
strip_whitespace=True,
|
|
26
|
+
max_length=255,
|
|
27
|
+
description="The path to a local data source file.",
|
|
28
|
+
)
|
|
29
|
+
link: Optional[str] = Field(
|
|
30
|
+
None,
|
|
31
|
+
strip_whitespace=True,
|
|
32
|
+
max_length=255,
|
|
33
|
+
description="The link to a remote data source file.",
|
|
34
|
+
)
|
|
35
|
+
header_row: Optional[int] = Field(
|
|
36
|
+
None, gt=0, lt=9999, description="The row number where the data begins."
|
|
37
|
+
)
|
|
38
|
+
data_start_row: Optional[int] = Field(
|
|
39
|
+
None, gt=0, lt=9999, description="The row number where the data begins."
|
|
40
|
+
)
|
|
41
|
+
delimiter: Optional[str] = Field(
|
|
42
|
+
",",
|
|
43
|
+
strip_whitespace=True,
|
|
44
|
+
max_length=1,
|
|
45
|
+
description="The delimiter used by the data source file.",
|
|
46
|
+
)
|
|
47
|
+
quote_char: Optional[str] = Field(
|
|
48
|
+
'"',
|
|
49
|
+
strip_whitespace=True,
|
|
50
|
+
max_length=1,
|
|
51
|
+
description="The quote delimiter character used by the data source file.",
|
|
52
|
+
)
|
|
53
|
+
interval: Optional[int] = Field(
|
|
54
|
+
None,
|
|
55
|
+
gt=0,
|
|
56
|
+
lt=9999,
|
|
57
|
+
description="The time interval at which the data source should be loaded.",
|
|
58
|
+
)
|
|
59
|
+
interval_units: Optional[Literal["minutes", "hours", "days", "weeks", "months"]] = (
|
|
60
|
+
Field(None, description="The interval units used by the data source file.")
|
|
61
|
+
)
|
|
62
|
+
crontab: Optional[str] = Field(
|
|
63
|
+
None,
|
|
64
|
+
strip_whitespace=True,
|
|
65
|
+
max_length=255,
|
|
66
|
+
description="The crontab used to schedule when the data source should be loaded.",
|
|
67
|
+
)
|
|
68
|
+
start_time: Optional[datetime] = Field(
|
|
69
|
+
None, description="When the data source should begin being loaded."
|
|
70
|
+
)
|
|
71
|
+
end_time: Optional[datetime] = Field(
|
|
72
|
+
None, description="When the data source should stop being loaded."
|
|
73
|
+
)
|
|
74
|
+
paused: Optional[bool] = Field(
|
|
75
|
+
False, description="Whether loading the data source should be paused or not."
|
|
76
|
+
)
|
|
77
|
+
timestamp_column: Union[int, str] = Field(
|
|
78
|
+
...,
|
|
79
|
+
strip_whitespace=True,
|
|
80
|
+
max_length=255,
|
|
81
|
+
description="The column of the data source file containing the timestamps.",
|
|
82
|
+
)
|
|
83
|
+
timestamp_format: Optional[str] = Field(
|
|
84
|
+
"%Y-%m-%dT%H:%M:%S%Z",
|
|
85
|
+
strip_whitespace=True,
|
|
86
|
+
max_length=255,
|
|
87
|
+
description="The format of the timestamps, using Python's datetime strftime codes.",
|
|
88
|
+
)
|
|
89
|
+
timestamp_offset: Optional[str] = Field(
|
|
90
|
+
"+0000",
|
|
91
|
+
strip_whitespace=True,
|
|
92
|
+
max_length=255,
|
|
93
|
+
description="An ISO 8601 time zone offset designator code to be applied to timestamps in the data source file.",
|
|
94
|
+
)
|
|
95
|
+
data_loader_id: UUID = Field(
|
|
96
|
+
...,
|
|
97
|
+
description="The ID of the data loader responsible for loading this data source.",
|
|
98
|
+
)
|
|
99
|
+
data_source_thru: Optional[datetime] = Field(
|
|
100
|
+
None, description="The timestamp through which the data source contains data."
|
|
101
|
+
)
|
|
102
|
+
last_sync_successful: Optional[bool] = Field(
|
|
103
|
+
None, description="Whether the last data loading attempt was successful of not."
|
|
104
|
+
)
|
|
105
|
+
last_sync_message: Optional[str] = Field(
|
|
106
|
+
None,
|
|
107
|
+
strip_whitespace=True,
|
|
108
|
+
description="A message generated by the data loader it attempted to load data from this data source.",
|
|
109
|
+
)
|
|
110
|
+
last_synced: Optional[datetime] = Field(
|
|
111
|
+
None,
|
|
112
|
+
description="The last time the data loader attempted to load data from this data source.",
|
|
113
|
+
)
|
|
114
|
+
next_sync: Optional[datetime] = Field(
|
|
115
|
+
None,
|
|
116
|
+
description="The next time the data loader will attempt to load data from this data source.",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class DataSource(HydroServerCoreModel, DataSourceFields):
|
|
121
|
+
"""
|
|
122
|
+
A model representing a data source, extending the core functionality of HydroServerCoreModel with additional
|
|
123
|
+
properties and methods.
|
|
124
|
+
|
|
125
|
+
:ivar _datastreams: A private attribute to cache the list of datastreams associated with the data source.
|
|
126
|
+
:ivar _data_loader: A private attribute to cache the data loader associated with the data source.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(self, _endpoint, _uid: Optional[UUID] = None, **data):
|
|
130
|
+
"""
|
|
131
|
+
Initialize a DataSource instance.
|
|
132
|
+
|
|
133
|
+
:param _endpoint: The endpoint associated with the DataSource.
|
|
134
|
+
:param _uid: The unique identifier for the DataSource.
|
|
135
|
+
:type _uid: Optional[UUID]
|
|
136
|
+
:param data: Additional attributes for the DataSource.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
super().__init__(_endpoint=_endpoint, _uid=_uid, **data)
|
|
140
|
+
self._datastreams = None
|
|
141
|
+
self._data_loader = None
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def datastreams(self) -> List["Datastream"]:
|
|
145
|
+
"""
|
|
146
|
+
Retrieve the datastreams associated with the DataSource. If not already cached, fetch the datastreams from the
|
|
147
|
+
server.
|
|
148
|
+
|
|
149
|
+
:return: A list of datastreams associated with the data source.
|
|
150
|
+
:rtype: List[Datastream]
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
if self._datastreams is None:
|
|
154
|
+
self._datastreams = self._endpoint.list_datastreams(uid=self.uid)
|
|
155
|
+
|
|
156
|
+
return self._datastreams
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def data_loader(self) -> "DataLoader":
|
|
160
|
+
"""
|
|
161
|
+
Retrieve the data loader associated with the data source. If not already cached, fetch the data loader from the
|
|
162
|
+
server.
|
|
163
|
+
|
|
164
|
+
:return: The data loader associated with the data source.
|
|
165
|
+
:rtype: DataLoader
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
if self._data_loader is None:
|
|
169
|
+
self._data_loader = self._endpoint._service.dataloaders.get(
|
|
170
|
+
uid=self.data_loader_id
|
|
171
|
+
) # noqa
|
|
172
|
+
|
|
173
|
+
return self._data_loader
|
|
174
|
+
|
|
175
|
+
def refresh(self) -> None:
|
|
176
|
+
"""
|
|
177
|
+
Refresh the data source with the latest data from the server and update cached datastreams and data loader if
|
|
178
|
+
they were previously loaded.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
entity = self._endpoint.get(uid=self.uid).model_dump(exclude=["uid"])
|
|
182
|
+
self._original_data = entity
|
|
183
|
+
self.__dict__.update(entity)
|
|
184
|
+
if self._datastreams is not None:
|
|
185
|
+
self._datastreams = self._endpoint.list_datastreams(uid=self.uid)
|
|
186
|
+
if self._data_loader is not None:
|
|
187
|
+
self._data_loader = self._endpoint._service.dataloaders.get(
|
|
188
|
+
uid=self.data_loader_id
|
|
189
|
+
) # noqa
|
|
190
|
+
|
|
191
|
+
def load_observations(self) -> None:
|
|
192
|
+
"""
|
|
193
|
+
Load observations data from a local file or a remote URL into HydroServer using this data source configuration.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
if self.path:
|
|
197
|
+
with open(self.path, "rb") as f:
|
|
198
|
+
with io.TextIOWrapper(f, encoding="utf-8") as data_file:
|
|
199
|
+
hs_etl = HydroServerETLCSV(
|
|
200
|
+
service=getattr(self._endpoint, "_service"),
|
|
201
|
+
data_file=data_file,
|
|
202
|
+
data_source=self,
|
|
203
|
+
)
|
|
204
|
+
hs_etl.run()
|
|
205
|
+
elif self.link:
|
|
206
|
+
with tempfile.NamedTemporaryFile(mode="w+b") as temp_file:
|
|
207
|
+
with urlopen(self.link) as response:
|
|
208
|
+
chunk_size = 1024 * 1024 * 10 # Use a 10mb chunk size.
|
|
209
|
+
while True:
|
|
210
|
+
chunk = response.read(chunk_size)
|
|
211
|
+
if not chunk:
|
|
212
|
+
break
|
|
213
|
+
temp_file.write(chunk)
|
|
214
|
+
temp_file.seek(0)
|
|
215
|
+
with io.TextIOWrapper(temp_file, encoding="utf-8") as data_file:
|
|
216
|
+
hs_etl = HydroServerETLCSV(
|
|
217
|
+
service=getattr(self._endpoint, "_service"),
|
|
218
|
+
data_file=data_file,
|
|
219
|
+
data_source=self,
|
|
220
|
+
)
|
|
221
|
+
hs_etl.run()
|
|
222
|
+
else:
|
|
223
|
+
return None
|