-
-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enode API Changes #87
base: main
Are you sure you want to change the base?
Conversation
Codecov Report
@@ Coverage Diff @@
## main #87 +/- ##
==========================================
+ Coverage 93.82% 94.60% +0.78%
==========================================
Files 10 10
Lines 405 538 +133
==========================================
+ Hits 380 509 +129
- Misses 25 29 +4
📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
Fix lint errors Finish both endpoints Fix formatting and lint errors Update pv_site_api/main.py Co-authored-by: Andrew Lester <[email protected]> Update pv_site_api/main.py Co-authored-by: Andrew Lester <[email protected]> Update pv_site_api/main.py Co-authored-by: Andrew Lester <[email protected]> Address PR comments Update pyproject.toml Fix lint errors Fix formatting Fix imports Fix bugs Remove unused import Fix formatting Fix import order Create enode_auth.py Define basic auth function Get api key from env Finish part 1
Remove test end point clean up Remove print messages Make getenv set variable to str make format lint and format Remove unecessary check Remove test files Use env var for enode token url Change get_enode_access_token to return request Refactor get_enode_access_token() into EnodeAuth class change 404 to 401 format Remove test code Remove print Address PR changes Update enode constructor to take in client_id, secret, and url Add type hints Add inverters test for site specific Fix lint errors Modularize code Fix lint errors
* Add enode link URL endpoint * Fix lint * Rename test var for clarity * Add comment to mock urls fixtuer * Use redirect response
* Bump version: 0.0.36 → 0.0.37 * TDD: add failing test, looking at forecasts in the future * add start utc filter on forecast future qery * lint * lint * isort * add freeze gun to dev dependencies * self PR comments * Bump version: 0.0.37 → 0.0.38 * Make site_uuid field on PVSiteMetadata model optional * Make pydantic field optional * Move test site with real UUID to real db test * Don't use uuid in tests * Add part of check back * Ensure autogenerated site uuid is not null in database * Add WHERE statement on query for past forecasts This doesn't change the result but speeds up the query in general. * Bump version: 0.0.38 → 0.0.39 * Bump version: 0.0.39 → 0.0.40 * add logging * lint * run blacks * add extra logs * Bump version: 0.0.40 → 0.0.41 * add structlogging * add structlog to requiremwnts * add logging * Bump version: 0.0.41 → 0.0.42 * Add structlog initialisation * blacks * isort * Bump version: 0.0.42 → 0.0.43 * Uniformize check to FAKE and fix related test * Set codecov targets * Hard-code the "now" time for all tests This means that the same tests are run now matter the time at which we run them. * add caching * fix * lint * isort * fix for routes calling routes * lint * Bump version: 0.0.43 → 0.0.44 * PR comment * add args to cache * lint * Update pv_site_api/cache.py Co-authored-by: Simon Lemieux <[email protected]> * tidy * Bump version: 0.0.44 → 0.0.45 * What I've got * Add basic authorization to the /sites* routes The caller of the routes must have a proper Bearer authorization header set. * refactor * add tests * fix error in import * update pvsite-datamodel (in line with main) * fix bug * fix incompatible types * lint and format * add site existence check * add back correct datamodel dependency * fix test name * allow tests to pass by adding fake condition * remove 404 check * use or instead of is not none * Bump version: 0.0.45 → 0.0.46 * add site existence check * format and lint * Bump version: 0.0.46 → 0.0.47 * add post endpoint * add test back in * fix errors and format * add site name to test * fix tiny error * fix another small error * run tests * fix bugs * lint and format * fix fake * :pleading-face: * yet another is_fake() addition * address comments --------- Co-authored-by: Peter Dudfield <[email protected]> Co-authored-by: BumpVersion Action <bumpversion@github-actions> Co-authored-by: peterdudfield <[email protected]> Co-authored-by: AndrewLester <[email protected]> Co-authored-by: Simon Lemieux <[email protected]> Co-authored-by: devsjc <[email protected]> Fix some functions Fix Enode link endpoint Remove duplicate fixture Rename /inverters to /enode/inverters
Move a few things Fix pyproject Fix Fix tests Fix import order Default the token URL Add some tests for other states Fix last missing coverage in main.py
|
||
|
||
@app.get("/enode/inverters") | ||
async def get_inverters( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason why some of the new routes are async
and others not?
I think we should keep everything non-async
if possible since we are not using the async
version of sqlalchemy
. This means that any query to the database will actually be blocking.
That being said I'm not sure of the implications for httpx
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason we made some of these new routes async
is ultimately because of this function:
https://github.com/openclimatefix/pv-site-api/pull/87/files#diff-d682b0ba7c0698e66b6ea6e320db5feb17c1460dbbe67aa2852801b0867f311fR16-R26
(get_inverters_list
)
The function needs to make several requests to Enode, one for each of the user's inverters to get detailed inverter data. For performance reasons, we choose to make the requests in parallel, using httpx
's AsyncClient
and aggregating the results with asyncio
. Given we use an AsyncClient
instance and we await
the asyncio
call, we must make the get_inverters_list
function async
. In turn, we then must make all routes that call get_inverters_list
async
too.
You're right that the current queries will also block the server in these async
routes, which is my mistake, as I thought FastAPI would run all routes in an external thread pool no matter what. Given that the await
on get_inverters_list
depends on the database queries in these async
routes, I think setting up the SQL queries as path dependencies rather than in the route body will prevent the blocking. It seems like any non async
dependency will run in an external thread pool even if the path function itself is async docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm now realizing the Auth path dependency might actually be a good place to grab the client from the database... the way you've implemented it now is quite abstract, so maybe it makes more sense to create a new dependency called get_client
, which depends on Auth
, and grabs the client (for now, just returns the first one), from the Auth
state.
Then, each route which needs the client can just depend on that, and if a route doesn't need the client, can depend on Auth
. Or, they can all just use get_client
since I'm not sure when a route body would use the decoded JWT payload by itself?
In any case, this fixes our blocking problem by putting the query for the client in a non async
path dependency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with you that if a route body doesn't call the DB (which might be possible here if the information about what is the user's client is coming from Auth0), then we can use async
.
This might be the case in this function but it's not the case for all the other async
functions in this PR.
Any reason why we can't simply use httpx's httpx.Client()
instead of the AsyncClient
? It would be simpler and harder to shoot ourselves in the foot in our otherwise non-async setup.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be the case in this function but it's not the case for all the other async functions in this PR.
Yes, so the other async functions which make other DB accesses will have to move those accesses to a path dependency as well, one which depends on auth
/client
.
The reason we can't use httpx.Client()
is because it blocks within the thread the route is running on when making requests, and since we're making several independent requests to Enode, we would rather make those requests in parallel, which is only possible with httpx
using httpx.AsyncClient()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK I see your point.
If those really need to run in parallel, the alternative (without async
) would be to use threads. Given that we not otherwise using async
in the API at the moment, I think that would be the cleaner solution.
This brought to mind a more general question: why do we need to go through the (OCF) API to get the Enode inverter data? Can't we call Enode's API directly from the frontend? The only thing that really needs to get to our backend is the inverter id. In other words, could we have this:
GET /sites/{site_id}/inverters
=> Returns the list of inverter ids for given site
PUT /sites/{site_id}/inverters
=> Set the inverter id for given site
And that's it. The frontend would then call Enode with those ids if it needs the full inverter data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, I've also got an implementation of the non-blocking (via path dependencies) database accesses with the async
routes, that I'll push in a second. We can definitely switch this to threads though.
Regarding accessing Enode's API directly from the frontend, since their API requires Enode secrets, and has no access control (any one with a token can access all users), it wouldn't be feasible. This would make things significantly simpler though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for taking so long on the above message @simlmx, it took me a while to figure out a bug with the way I've set it up and the new cache system, and for now I've temporarily ignored the issue by just ignoring the auth
dependency in the cache key. This is not mergable at this point since the auth
dependency must be in the cache key, but I still need to add a JSON encoder for the ClientSQL
model, or something similar that won't require a pv-site-datamodel
change ideally... all in all the code I just pushed is still a work in progress, but hopefully you can see the idea.
This current code does fix the server blocking issue by putting DB accesses in the async def
routes into a dependency.
Let me know if you think this general idea works, or if switching to threads is the better option. My only concern with a thread based solution is it will involve additional complexity to do with creating the several threads and waiting on them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whats the status on this? Is the auth cache key still an issue?
The requests to ENODE to get the inverter data? How long does each on roughly take?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The auth cache key is no longer an issue since I serialize the current request's corresponding client, and use that as part of the key here:
pv-site-api/pv_site_api/cache.py
Lines 48 to 51 in 381f849
# translate authenticated client to serializable type | |
route_variables["auth"] = ( | |
route_variables["auth"] and client_to_pydantic(route_variables["auth"]).json() | |
) |
However, since we currently still get the first client in the database for every request, every request that is authenticated will share the same cache for the time being. This doesn't change behavior since the endpoints used the same client anyway.
The requests to Enode to get inverter data take on average 560ms in the sandbox API and, although I can't test the actual inverter endpoints with production since we don't have any users linked there at the moment, the endpoints take around 550ms before returning nothing.
This is on the longer side, but given that most users we'll work with will only have 1-2 inverters, we'd only be looking at a 2x slowdown at most, if were to make these requests in series.
I was just able to remove our Right now this is failing tests though because the library's class requires a non-empty string for the pv-site-api/pv_site_api/main.py Lines 118 to 119 in e185d93
Alternatively, the enode_auth instance there could be provided as a mocked |
To make the tests pass without those environment variables, I just opted to default them to unusable values. This doesn't change any behavior since the tests don't even use the |
Pull Request
This pull request adds all of the necessary endpoints we'll need for minimal Enode support in the
pv-sites-mobile
UI. For reference, it adds this functionality:Unrelated to Enode, but necessary for our project, is another endpoint:
We could move this latter endpoint to another PR if necessary.
Description
Adds support for linking inverters, storing linked inverters in our database, and retrieving inverter information from Enode.
Fixes: #71
Fixes: #72
Fixes: #73
Fixes: #74
Several new environment variables are now used:
ENODE_API_BASE_URL
, though not required (defaults to sandbox) found hereENODE_TOKEN_URL
, though not required (defaults to Enode token URL specific in docs) found hereENODE_CLIENT_ID
the Enode client IDENODE_CLIENT_SECRET
the Enode client secretHow Has This Been Tested?
This has been tested with the addition of new test files as well as run through manual testing.
Checklist: