Skip to content

Commit 257de3d

Browse files
committed
Drafting new classes
This commit introduces a rough draft of the classes to be introduced in the object/opinionated layer. For now, the subpackage containing them is named 'objects' but that may change in the future. For discussion, see #228. - Add 'objects' subpackage with init module - Add 'objects.py' module with 5 new classes - Methods will be implemented in the following commits.
1 parent 5a26812 commit 257de3d

File tree

2 files changed

+210
-0
lines changed

2 files changed

+210
-0
lines changed

erddapy/objects/__init__.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""
2+
This module contains opinionated, higher-level objects for searching servers and accessing datasets.
3+
4+
It is named 'objects' after object-relational mapping, which is the concept of having an object-oriented
5+
layer between a database (in this case, ERDDAP), and the programming language.
6+
"""
7+
8+
9+
from objects import (
10+
ERDDAPConnection,
11+
ERDDAPDataset,
12+
ERDDAPServer,
13+
GridDataset,
14+
TableDataset,
15+
)
16+
17+
__all__ = [
18+
"ERDDAPDataset",
19+
"ERDDAPConnection",
20+
"ERDDAPServer",
21+
"TableDataset",
22+
"GridDataset",
23+
]

erddapy/objects/objects.py

+187
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
"""Main module of the 'objects' subpackage containing most classes."""
2+
3+
from pathlib import Path
4+
from typing import Dict, Union
5+
6+
StrLike = Union[str, bytes]
7+
FilePath = Union[str, Path]
8+
9+
10+
class ERDDAPConnection:
11+
"""
12+
Manages connection that will be used in ERDDAPServer instances.
13+
14+
While most ERDDAP servers allow connections via a bare url, some servers may require authentication
15+
to access data.
16+
"""
17+
18+
def __init__(self, server: str):
19+
"""Initialize instance of ERDDAPConnection."""
20+
self._server = self.to_string(server)
21+
22+
@classmethod
23+
def to_string(cls, value):
24+
"""Convert an instance of ERDDAPConnection to a string."""
25+
if isinstance(value, str):
26+
return value
27+
elif isinstance(value, cls):
28+
return value.server
29+
else:
30+
raise TypeError(
31+
f"Server must be either a string or an instance of ERDDAPConnection. '{value}' was "
32+
f"passed.",
33+
)
34+
35+
def get(self, url_part: str) -> StrLike:
36+
"""
37+
Request data from the server.
38+
39+
Uses requests by default similar to most of the current erddapy data fetching functionality.
40+
41+
Can be overridden to use httpx, and potentially aiohttp or other async functionality, which could
42+
hopefully make anything else async compatible.
43+
"""
44+
pass
45+
46+
def open(self, url_part: str) -> FilePath:
47+
"""Yield file-like object for access for file types that don't enjoy getting passed a string."""
48+
pass
49+
50+
@property
51+
def server(self) -> str:
52+
"""Access the private ._server attribute."""
53+
return self._server
54+
55+
@server.setter
56+
def server(self, value: str):
57+
"""Set private ._server attribute."""
58+
self._server = self.to_string(value)
59+
60+
61+
class ERDDAPDataset:
62+
"""Base class for more focused table or grid datasets."""
63+
64+
def __init__(
65+
self,
66+
dataset_id: str,
67+
connection: str | ERDDAPConnection,
68+
variables,
69+
constraints,
70+
):
71+
"""Initialize instance of ERDDAPDataset."""
72+
self.dataset_id = dataset_id
73+
self._connection = ERDDAPConnection(ERDDAPConnection.to_string(connection))
74+
self._variables = variables
75+
self._constraints = constraints
76+
self._meta = None
77+
78+
@property
79+
def connection(self) -> ERDDAPConnection:
80+
"""Access private ._connection variable."""
81+
return self._connection
82+
83+
@connection.setter
84+
def connection(self, value: str | ERDDAPConnection):
85+
"""Set private ._connection variable."""
86+
self._connection = ERDDAPConnection(ERDDAPConnection.to_string(value))
87+
88+
def get(self, file_type: str) -> StrLike:
89+
"""Request data using underlying connection."""
90+
return self.connection.get(file_type)
91+
92+
def open(self, file_type: str) -> FilePath:
93+
"""Download and open dataset using underlying connection."""
94+
return self.connection.open(file_type)
95+
96+
def get_meta(self):
97+
"""Request dataset metadata from the server."""
98+
self._meta = None
99+
100+
@property
101+
def meta(self):
102+
"""Access private ._meta attribute. Request metadata if ._meta is empty."""
103+
return self.get_meta() if (self._meta is None) else self._meta
104+
105+
@property
106+
def variables(self):
107+
"""Access private ._variables attribute."""
108+
return self._variables
109+
110+
@property
111+
def constraints(self):
112+
"""Access private ._constraints attribute."""
113+
return self._constraints
114+
115+
def url_segment(self, file_type: str) -> str:
116+
"""Return URL segment without the base URL (the portion after 'https://server.com/erddap/')."""
117+
pass
118+
119+
def url(self, file_type: str) -> str:
120+
"""
121+
Return a URL constructed using the underlying ERDDAPConnection.
122+
123+
The URL will contain information regarding the base class server info, the dataset ID,
124+
access method (tabledap/griddap), file type, variables, and constraints.
125+
126+
This allows ERDDAPDataset subclasses to be used as more opinionated URL constructors while still
127+
not tying users to a specific IO method.
128+
129+
Not guaranteed to capture all the specifics of formatting a request, such as if a server requires
130+
specific auth or headers.
131+
"""
132+
pass
133+
134+
def to_dataset(self):
135+
"""Open the dataset as xarray dataset by downloading a subset NetCDF."""
136+
pass
137+
138+
def opendap_dataset(self):
139+
"""Open the full dataset in xarray via OpenDAP."""
140+
pass
141+
142+
143+
class TableDataset(ERDDAPDataset):
144+
"""Subclass of ERDDAPDataset specific to TableDAP datasets."""
145+
146+
def to_dataframe(self):
147+
"""Open the dataset as a Pandas DataFrame."""
148+
149+
150+
class GridDataset(ERDDAPDataset):
151+
"""Subclass of ERDDAPDataset specific to GridDAP datasets."""
152+
153+
pass
154+
155+
156+
class ERDDAPServer:
157+
"""Instance of an ERDDAP server, with support to ERDDAP's native functionalities."""
158+
159+
def __init__(self, connection: str | ERDDAPConnection):
160+
"""Initialize instance of ERDDAPServer."""
161+
self._connection = ERDDAPConnection(ERDDAPConnection.to_string(connection))
162+
163+
@property
164+
def connection(self) -> ERDDAPConnection:
165+
"""Access private ._connection attribute."""
166+
return self._connection
167+
168+
@connection.setter
169+
def connection(self, value: str | ERDDAPConnection):
170+
"""Set private ._connection attribute."""
171+
self._connection = ERDDAPConnection(ERDDAPConnection.to_string(value))
172+
173+
def full_text_search(self, query: str) -> Dict[str, ERDDAPDataset]:
174+
"""Search the server with native ERDDAP full text search capabilities."""
175+
pass
176+
177+
def search(self, query: str) -> Dict[str, ERDDAPDataset]:
178+
"""
179+
Search the server with native ERDDAP full text search capabilities.
180+
181+
Also see ERDDAPServer.full_text_search.
182+
"""
183+
return self.full_text_search(query)
184+
185+
def advanced_search(self, **kwargs) -> Dict[str, ERDDAPDataset]:
186+
"""Search server with ERDDAP advanced search capabilities (may return pre-filtered datasets)."""
187+
pass

0 commit comments

Comments
 (0)