Skip to content

Commit abca9b2

Browse files
api
1 parent d72e5f4 commit abca9b2

File tree

9 files changed

+489
-19
lines changed

9 files changed

+489
-19
lines changed

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
This package allows easy access to Geo Engine functionality from Python environments.
44

5+
## Test
6+
7+
Create a virtual environment (e.g., `python3 -m venv env`).
8+
Then, install the dependencies with `python3 -m pip install -e .`
9+
510
## Build
611

712
You can build the package with `python3 -m build`.

geoengine/__init__.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1-
from .auth import Session
2-
from .lib import initialize, Accessor
1+
from .auth import Session, get_session, initialize, reset
2+
from .lib import Accessor, geopandas_by_workflow, geopandas_by_workflow_id
3+
from .types import Bbox
4+
from .error import GeoEngineException, InputException, UninitializedException

geoengine/auth.py

+49-8
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,65 @@
1+
from typing import Dict
2+
from geoengine.error import GeoEngineException, UninitializedException
13
import requests as req
2-
from uuid import UUID
34

45

56
class Session:
67
'''
78
A Geo Engine session
89
'''
910

10-
def __init__(self, id: UUID) -> None:
11-
self.id = id
11+
def __init__(self, server_url: str) -> None:
12+
'''
13+
Initialize communication between this library and a Geo Engine instance
14+
'''
1215

16+
# TODO: username and password for Pro version
1317

14-
def initialize_session(server_url: str) -> Session:
18+
session = req.post(f'{server_url}/anonymous').json()
19+
20+
if 'error' in session:
21+
raise GeoEngineException(session)
22+
23+
self.__id = session['id']
24+
self.__valid_until = session['validUntil']
25+
self.__server_url = server_url
26+
27+
def auth_headers(self) -> Dict[str, str]:
28+
return {'Authorization': 'Bearer ' + self.__id}
29+
30+
def server_url(self) -> str:
31+
return self.__server_url
32+
33+
34+
session: Session = None
35+
36+
37+
def get_session() -> Session:
38+
global session
39+
40+
if session is None:
41+
raise UninitializedException()
42+
43+
return session
44+
45+
46+
def initialize(server_url: str) -> None:
1547
'''
1648
Initialize communication between this library and a Geo Engine instance
1749
'''
1850

19-
# TODO: username and password for Pro version
51+
global session
52+
53+
session = Session(server_url)
54+
55+
56+
def reset() -> None:
57+
'''
58+
Resets the current session
59+
'''
60+
61+
global session
2062

21-
session = req.post(f'{server_url}/anonymous').json()
22-
session_id = session['id']
63+
# TODO: active logout for Pro version
2364

24-
Session(session_id)
65+
session = None

geoengine/error.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from typing import Dict
2+
3+
4+
class GeoEngineException(Exception):
5+
def __init__(self, reponse: Dict[str, str]) -> None:
6+
self.error = reponse['error'] if 'error' in reponse else '?'
7+
self.message = reponse['message'] if 'message' in reponse else '?'
8+
9+
def __str__(self) -> str:
10+
return f"{self.error}: {self.message}"
11+
12+
13+
class InputException(Exception):
14+
def __init__(self, message: str) -> None:
15+
self.__message = message
16+
17+
def __str__(self) -> str:
18+
return f"{self.__message}"
19+
20+
21+
class UninitializedException(Exception):
22+
def __str__(self) -> str:
23+
return "You have to call `initialize` before using other functionality"

geoengine/lib.py

+78-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11

2-
from geoengine.auth import Session, initialize_session
3-
from typing import Any
2+
from logging import debug
3+
from geoengine.types import Bbox
4+
from geoengine.error import GeoEngineException
5+
from geoengine.auth import Session, get_session
6+
from typing import Any, Dict
47
import geopandas as gpd
58
import requests as req
69
from owslib.wfs import WebFeatureService
@@ -15,9 +18,77 @@ def get_features(self, workflow_id: Any, bbox: Any) -> gpd.GeoDataFrame:
1518
pass
1619

1720

18-
def initialize(server_url: str) -> Accessor:
19-
'''
20-
Initialize communication between this library and a Geo Engine instance
21-
'''
21+
class WorkflowId:
22+
def __init__(self, response: Dict[str, str]) -> None:
23+
if not 'id' in response:
24+
raise GeoEngineException(response)
2225

23-
Accessor(initialize_session(server_url))
26+
self.__id = response['id']
27+
28+
def __str__(self) -> str:
29+
return self.__id
30+
31+
32+
def register_workflow(workflow: str) -> WorkflowId:
33+
session = get_session()
34+
35+
workflow_response = req.post(
36+
f'{session.server_url()}/workflow',
37+
json=workflow,
38+
headers=session.auth_headers()
39+
).json()
40+
41+
return WorkflowId(workflow_response)
42+
43+
44+
def geopandas_by_workflow_id(workflow_id: WorkflowId, bbox: Bbox) -> gpd.GeoDataFrame:
45+
session = get_session()
46+
47+
# wfs = WebFeatureService(url=f'{session.server_url()}/wfs', version='2.0.0')
48+
49+
# TODO: add resolution
50+
# TODO: customize SRS
51+
params = dict(
52+
service='WFS',
53+
version="2.0.0",
54+
request='GetFeature',
55+
outputFormat='application/json',
56+
typeNames=f'registry:{workflow_id}',
57+
bbox=bbox.bbox_str(),
58+
time=bbox.time_str(),
59+
srsName='EPSG:4326',
60+
)
61+
62+
wfs_url = req.Request(
63+
'GET', url=f'{session.server_url()}/wfs', params=params).prepare().url
64+
65+
debug(f'WFS URL:\n{wfs_url}')
66+
print(f'WFS URL:\n{wfs_url}')
67+
68+
data_response = req.get(wfs_url, headers=session.auth_headers())
69+
70+
def geo_json_with_time_to_geopandas(data_response):
71+
'''
72+
GeoJson has no standard for time, so we parse the when field
73+
separately and attach it to the data frame as columns `start`
74+
and `end`.
75+
'''
76+
77+
data = gpd.read_file(StringIO(data_response.text))
78+
79+
geo_json = data_response.json()
80+
start = [f['when']['start'] for f in geo_json['features']]
81+
end = [f['when']['end'] for f in geo_json['features']]
82+
83+
data['start'] = gpd.pd.to_datetime(start)
84+
data['end'] = gpd.pd.to_datetime(end)
85+
86+
return data
87+
88+
return geo_json_with_time_to_geopandas(data_response)
89+
90+
91+
def geopandas_by_workflow(workflow: str, bbox: Bbox) -> gpd.GeoDataFrame:
92+
workflow_id = register_workflow(workflow)
93+
94+
return geopandas_by_workflow_id(workflow_id, bbox)

geoengine/types.py

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from geoengine.error import InputException
2+
from typing import Tuple
3+
from datetime import datetime
4+
5+
6+
class Bbox:
7+
def __init__(self, spatial_bbox: Tuple[float, float, float, float], time_interval: Tuple[datetime, datetime], resolution=0.1) -> None:
8+
xmin = spatial_bbox[0]
9+
ymin = spatial_bbox[1]
10+
xmax = spatial_bbox[2]
11+
ymax = spatial_bbox[3]
12+
13+
if (xmin > xmax) or (ymin > ymax):
14+
raise InputException("Bbox: Malformed since min must be <= max")
15+
16+
self.__spatial_bbox = spatial_bbox
17+
18+
if time_interval[0] > time_interval[1]:
19+
raise InputException("Time inverval: Start must be <= End")
20+
21+
self.__time_interval = time_interval
22+
23+
if resolution <= 0:
24+
raise InputException("Resoultion: Must be positive")
25+
26+
self.__resolution = resolution
27+
28+
def bbox_str(self) -> str:
29+
return ','.join(map(str, self.__spatial_bbox))
30+
31+
def time_str(self) -> str:
32+
if self.__time_interval[0] == self.__time_interval[1]:
33+
return self.__time_interval[0].isoformat(timespec='milliseconds')
34+
35+
return '/'.join(map(str, self.__time_interval))
36+
37+
def resolution(self) -> float:
38+
return self.__resolution

setup.cfg

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ install_requires =
2121
requests
2222
geopandas
2323
owslib
24+
requests_mock # actually a dev-dependency
2425

2526
[options.packages.find]
2627
where = .

setup.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from setuptools import setup
2+
setup()

0 commit comments

Comments
 (0)