Skip to content

Commit 7fd17c8

Browse files
committed
do the things
Signed-off-by: Max Chesterfield <max.chesterfield@zepben.com> tests pass Signed-off-by: Max Chesterfield <max.chesterfield@zepben.com>
1 parent 02b8da8 commit 7fd17c8

File tree

15 files changed

+1626
-1433
lines changed

15 files changed

+1626
-1433
lines changed

README.md

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from zepben.eas.client.auth_method import TokenAuth
2+
13
# Evolve App Server Python Client #
24

35
This library provides a wrapper to the Evolve App Server's API, allowing users of the evolve SDK to authenticate with
@@ -7,16 +9,18 @@ the Evolve App Server and upload studies.
79

810
```python
911
from geojson import FeatureCollection
10-
from zepben.eas import EasClient, Study, Result, Section, GeoJsonOverlay
12+
from zepben.eas import EasClient, Study, Result, Section, GeoJsonOverlay, TokenAuth
1113

1214
eas_client = EasClient(
13-
host="<host>",
14-
port=1234,
15-
access_token="<access_token>",
16-
client_id="<client_id>",
17-
username="<username>",
18-
password="<password>",
19-
client_secret="<client_secret>"
15+
TokenAuth(
16+
host="<host>",
17+
port=1234,
18+
access_token="<access_token>",
19+
client_id="<client_id>",
20+
username="<username>",
21+
password="<password>",
22+
client_secret="<client_secret>"
23+
)
2024
)
2125

2226
eas_client.upload_study(
@@ -66,17 +70,19 @@ To use the asyncio API use `async_upload_study` like so:
6670
```python
6771
from aiohttp import ClientSession
6872
from geojson import FeatureCollection
69-
from zepben.eas import EasClient, Study, Result, Section, GeoJsonOverlay
73+
from zepben.eas import EasClient, Study, Result, Section, GeoJsonOverlay, TokenAuth
7074

7175
async def upload():
7276
eas_client = EasClient(
73-
host="<host>",
74-
port=1234,
75-
access_token="<access_token>",
76-
client_id="<client_id>",
77-
username="<username>",
78-
password="<password>",
79-
client_secret="<client_secret>",
77+
TokenAuth(
78+
host="<host>",
79+
port=1234,
80+
access_token="<access_token>",
81+
client_id="<client_id>",
82+
username="<username>",
83+
password="<password>",
84+
client_secret="<client_secret>",
85+
),
8086
session=ClientSession(...)
8187
)
8288

changelog.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33
### Breaking Changes
44
* Renamed the parameter `calibration_id` to `calibration_name` for the following methods `get_transformer_tap_settings` and `async_get_transformer_tap_settings`. This better reflects that
55
this parameter is the user supplied calibration name rather than EAS's internal calibration run ID.
6+
* EasClient will now need `auth=` passed with an auth object. either `BaseAuthMethod` or `TokenAuth`, this allows
7+
cleaner documenting of accepted constructor arguments.
68

79
### New Features
810
* Added optional fields to `ModelConfig` to control network simplification: `simplify_network`, `collapse_negligible_impedances`, and `combine_common_impedances`.
911
* Added optional `node_level_results` field to `GeneratorConfig`. This `NodeLevelResultsConfig` allows the configuration of node level power flow results from OpenDss.
1012

1113
### Enhancements
12-
* None.
14+
* Internal: query bodys are now mostly self generating with `to_json` and `build_gql_query_object_model` methods.
15+
* All request handling logic has been refactored into a single method.
16+
* `catch_warnings` wrapper func to handle standard warning catching.
1317

1418
### Fixes
1519
* None.

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2020 Zeppelin Bend Pty Ltd
1+
# Copyright 2025 Zeppelin Bend Pty Ltd
22
#
33
# This Source Code Form is subject to the terms of the Mozilla Public
44
# License, v. 2.0. If a copy of the MPL was not distributed with this

src/zepben/eas/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@
66
#
77

88
from zepben.eas.client.eas_client import EasClient
9+
from zepben.eas.client.auth_method import *
910
from zepben.eas.client.study import *
1011
from zepben.eas.client.work_package import *
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
# Copyright 2025 Zeppelin Bend Pty Ltd
2+
#
3+
# This Source Code Form is subject to the terms of the Mozilla Public
4+
# License, v. 2.0. If a copy of the MPL was not distributed with this
5+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
7+
__all__ = ['BaseAuthMethod', 'TokenAuth']
8+
9+
10+
from hashlib import sha256
11+
from typing import Optional, overload
12+
13+
from aiohttp import ClientSession
14+
from zepben.auth import ZepbenTokenFetcher, create_token_fetcher, AuthMethod, create_token_fetcher_managed_identity
15+
16+
17+
class EasClient:
18+
protocol: str = "https",
19+
verify_certificate: bool = True,
20+
ca_filename: Optional[str] = None,
21+
session: ClientSession = None,
22+
json_serialiser=None
23+
24+
25+
class BaseAuthMethod:
26+
def __init__(self, host, port, protocol='https', verify_certificate=True):
27+
"""
28+
29+
:param host: The domain of the Evolve App Server, e.g. "evolve.local"
30+
:param port: The port on which to make requests to the Evolve App Server, e.g. 7624
31+
:param protocol: The protocol of the Evolve App Server. Should be either "http" or "https". Must be "https" if
32+
auth is configured. (Defaults to "https")
33+
:param verify_certificate: Set this to "False" to disable certificate verification. This will also apply to the
34+
auth provider if auth is initialised via client id + username + password or
35+
client_id + client_secret. (Defaults to True)
36+
"""
37+
self._host = host
38+
self._port = port
39+
self.protocol = protocol
40+
self.verify_certificate = verify_certificate
41+
42+
@property
43+
def base_url_args(self) -> dict:
44+
return dict(host=self._host, port=self._port, protocol=self.protocol)
45+
46+
47+
class TokenAuth(BaseAuthMethod):
48+
"""
49+
Token Auth Method for Evolve App Server python client when connecting to HTTPS servers.
50+
51+
Token Authentication may be configured in one of three ways:
52+
- Providing an access token via the access_token parameter
53+
- Specifying the client ID of the Auth0 application via the client_id parameter, plus one of the following:
54+
- A username and password pair via the username and password parameters (account authentication)
55+
- The client secret via the client_secret parameter (M2M authentication)
56+
If this method is used, the auth configuration will be fetched from the Evolve App Server at the path
57+
"/api/config/auth".
58+
- Specifying a ZepbenTokenFetcher directly via the token_fetcher parameter
59+
60+
..code-block:: python::
61+
62+
TokenAuth(access_token='...')
63+
TokenAuth(token_fetcher='...')
64+
TokenAuth(client_id='...' username='...' password='...')
65+
TokenAuth(client_id='...', client_secret='...')
66+
67+
"""
68+
@overload
69+
def __init__(self, host, port, protocol='https', verify_certificate=True, *, access_token: str):
70+
"""
71+
:param access_token: The access token used for authentication, generated by Evolve App Server.
72+
"""
73+
...
74+
75+
@overload
76+
def __init__(self, host, port, protocol='https', verify_certificate=True, *, token_fetcher: ZepbenTokenFetcher):
77+
"""
78+
:param token_fetcher: A ZepbenTokenFetcher used to fetch auth tokens for access to the Evolve App Server.
79+
"""
80+
...
81+
82+
@overload
83+
def __init__(self, host, port, protocol='https', verify_certificate=True, *, client_id: str, username: str, password: str, client_secret: Optional[str]):
84+
"""
85+
:param client_id: The Auth0 client ID used to specify to the auth server which application to request a token for.
86+
:param username: The username used for account authentication.
87+
:param password: The password used for account authentication.
88+
:param client_secret: The Auth0 client secret used for M2M authentication. (Optional)
89+
"""
90+
...
91+
92+
@overload
93+
def __init__(self, host, port, protocol='https', verify_certificate=True, *, client_id: str, client_secret: str):
94+
"""
95+
:param client_id: The Auth0 client ID used to specify to the auth server which application to request a token for.
96+
:param client_secret: The Auth0 client secret used for M2M authentication.
97+
"""
98+
...
99+
100+
def __init__(self, host, port, protocol='https', verify_certificate=True, **kwargs):
101+
if protocol != 'https': # TODO: this exists because of an existing test, but given we can force it, we should
102+
raise ValueError(
103+
"Incompatible arguments passed to connect to secured Evolve App Server. "
104+
"Authentication tokens must be sent via https. "
105+
"To resolve this issue, exclude the \"protocol\" argument when initialising the EasClient.")
106+
107+
super().__init__(host, port, protocol, verify_certificate)
108+
self._token_fetcher = None
109+
self._access_token = None
110+
self._init_func = None
111+
self._configure(kwargs)
112+
113+
@property
114+
def token(self) -> Optional[str]:
115+
if self._access_token:
116+
return f"Bearer {self._access_token}"
117+
elif self._token_fetcher:
118+
return self._token_fetcher.fetch_token()
119+
raise AttributeError("access_token or token_fetcher method not configured")
120+
121+
def _configure(self, kwargs: dict):
122+
"""
123+
Validates that the kwargs that end up being passed to the non-overloaded `__init__` method are of a valid
124+
combination.
125+
"""
126+
match list(kwargs.keys()):
127+
case ['access_token']:
128+
self._access_token = kwargs['access_token']
129+
case ['token_fetcher']:
130+
self._token_fetcher = kwargs['token_fetcher']
131+
case ['client_id', 'client_secret', 'username', 'password']:
132+
self._configure_client_id(**kwargs)
133+
case ['client_id', 'username', 'password']:
134+
self._configure_client_id(**kwargs)
135+
case ['client_id', 'client_secret']:
136+
self._configure_client_id(**kwargs)
137+
case _:
138+
raise ValueError("Incompatible arguments passed to connect to secured Evolve App Server.")
139+
140+
if kwargs.get('client_id'):
141+
self._token_fetcher = create_token_fetcher(
142+
conf_address=f"{self.protocol}://{self._host}:{self._port}/api/config/auth",
143+
verify_conf=self.verify_certificate,
144+
)
145+
146+
def _configure_client_id(
147+
self, client_id: str = None,
148+
username: str = None,
149+
password: str = None,
150+
client_secret: str = None
151+
):
152+
self._token_fetcher = create_token_fetcher(
153+
conf_address=f"{self.protocol}://{self._host}:{self._port}/api/config/auth",
154+
verify_conf=self.verify_certificate,
155+
)
156+
if self._token_fetcher:
157+
scope = (
158+
'trusted' if self._token_fetcher.auth_method is AuthMethod.SELF else 'offline_access openid profile email0'
159+
)
160+
161+
self._token_fetcher.token_request_data.update({
162+
'client_id': client_id,
163+
'scope': scope
164+
})
165+
self._token_fetcher.refresh_request_data.update({
166+
"grant_type": "refresh_token",
167+
'client_id': client_id,
168+
'scope': scope
169+
})
170+
if username and password:
171+
self._token_fetcher.token_request_data.update({
172+
'grant_type': 'password',
173+
'username': username,
174+
'password':
175+
sha256(password.encode('utf-8')).hexdigest()
176+
if self._token_fetcher.auth_method is AuthMethod.SELF
177+
else password
178+
})
179+
if client_secret:
180+
self._token_fetcher.token_request_data.update({'client_secret': client_secret})
181+
182+
elif client_secret:
183+
self._token_fetcher.token_request_data.update({
184+
'grant_type': 'client_credentials',
185+
'client_secret': client_secret
186+
})
187+
else:
188+
# Attempt azure managed identity (what a hack)
189+
url = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01"
190+
self._token_fetcher = create_token_fetcher_managed_identity(
191+
identity_url=f"{url}&resource={client_id}",
192+
verify_auth=self.verify_certificate
193+
)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright 2025 Zeppelin Bend Pty Ltd
2+
#
3+
# This Source Code Form is subject to the terms of the Mozilla Public
4+
# License, v. 2.0. If a copy of the MPL was not distributed with this
5+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
7+
__all__ = ['catch_warnings']
8+
9+
import functools
10+
import warnings
11+
from typing import Callable
12+
13+
14+
def catch_warnings(func: Callable) -> Callable:
15+
@functools.wraps(func)
16+
def wrapper(*args, **kwargs):
17+
with warnings.catch_warnings():
18+
return func(*args, **kwargs)
19+
return wrapper

0 commit comments

Comments
 (0)