-
Notifications
You must be signed in to change notification settings - Fork 7
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
Add support for PATCH to partially replace an entity #6
base: master
Are you sure you want to change the base?
Conversation
3ddfdca
to
4dc3ed0
Compare
I'll update this PR to add support for PATCH requests also, since I can refactor this a bit. |
I've added support for PATCH. I could refactor to create a I think this might make the code more complicated though, so I leave it up to you to indicate if you want to have some changes. |
Thanks for the work and your interest in the project. I do appreciate! I reviewed the commit for the What you implemented in the I'm okay with this implementation and I agree: at this point, we might as well refactor the classes with inheritance. For reference again, FastAPI did suggest a similar pattern, especially about the naming convention ( I can either accept the PR as is or wait for the refactor. |
Thanks for your review and comment!
Note that I removed the I think for this merge request, the PATCH implementation is fine and the refactoring can be done later if needed. I don't think the PUT request is needed. |
I'll update the PATCH implementations so it returns a 200 instead of a 201. |
So I ran your PR in my local environment and all seems good. Would you mind, however, add a bit more unit tests to get 100% code coverage of $ pip install coverage
$ coverage run -m unittest test.py
.....
----------------------------------------------------------------------
Ran 5 tests in 0.126s
OK
$ coverage report
Name Stmts Miss Cover
-------------------------------------
database.py 5 0 100%
pyfreeradius.py 333 16 95%
test.py 102 0 100%
-------------------------------------
TOTAL 440 16 96% You can see details and missing lines using the HTML report: $ coverage html
Wrote HTML report to htmlcov/index.html Also, could you sync your fork to get the last commit that removed the extra Thanks! |
I was trying to add the missing unit tests and got some failures. Here are the problems I observed:
So the PR is actually missing some semantic checks or logic. |
My bad, I forgot to add those. I think I addressed these changes in my last commit. |
Thanks for the changes. One last concern: the Example: {
"checks": [],
"replies": [],
"users": [{"username": "a", "priority": 1}, {"username":"b", "priority": 1}]
} So the Conceptually, having a group without any attributes (while permitted by the FreeRADIUS data model) is strange from the REST API point of view. It would mean that a user can belong to a non-existing group. The group will start to exist because a user has been bound to him, and will cease to exist once its last user has been deleted. |
Hmm, I do have the use-case for a group without any attributes or checks. This is to make it easier later on to just add attribute to that group without having to link all users to that group. I prefer to link the users to their group during creation instead of later on when a parameter is added, since this information isn't easily available at that point. |
So this is more of a conceptual change I prefer not to accept. I get your point, but this API is mainly made for automation purposes where users get created and updated (deleted and re-created) using a Source of Truth. It could be the same for groups (e.g., L3VPN groups shared among users of the same L3VPN). I don't see a practical need where an automatized tool will first create an empty group with users and then add the attributes later. At this point, either we add more logic to prevent empty groups or we implement attributes overwriting as I suggested in #5 (comment). You may disagree on the conceptual approach and I'd understand if you prefer to work on a separate fork with that enhancement. |
I'll try to describe my use-case, and leave it up to you what you want to do with it :) We will be using the API to push Radius users that is used for PPPoE sessions. We use a radius group to combine users that belong to the same Organisation. They can have multiple users if they have multiple connections on multiple locations. Usually, we specify on a group level if the user needs to be terminated in a specific VRF but we can also provide different DNS servers here for example. On occasion, these settings can change, or some other group level setting needs to be modified. (This is why I want to be able to PATCH them instead of deleting and recreating). Now, while writing this, I noticed that we always have at least one attribute in the group (to specify the VRF) so the restriction on having at least one attribute might be okay for us. |
Yes, the use case you described is a typical scenario for VRF-based users belonging to the same L3VPN and RADIUS groups are well suited for this. I'm okay with the PR but the extra logic in |
I can look at this next week :) |
@angely-dev I've added the check to the I also noticed that I had to check for the I wanted to add a test for this, but this would require a rewrite of the tests, since they now test the repository itself, instead of the api methods where the additional business logic resides. |
I reran the PR in my local environment and got errors:
That is because there is a mismatch between the
Note that I am working on the unit tests of the API itself as per Testing. |
This is intentional. I've removed the username from the Also, when patching, there is a difference between having a I'm not sure what I need the change for this? |
About the username/groupname/nasname, no need to have it as attribute of the About the |
Before changing the original behavior, I tried this: @router.patch('/groups/{groupname}', tags=['groups'], status_code=200, response_model=Group, responses={**e409_response})
def patch_group(groupname: str, group: GroupUpdate, response: Response):
if not group_repo.exists(groupname):
raise HTTPException(404, 'Given group does not exist')
for user in group.users or []:
if not user_repo.exists(user.username):
raise HTTPException(422, f"Given user '{user.username}' does not exist: create it first")
current_group = group_repo.find_one(groupname)
if (not current_group.replies and group.replies == []) and (not current_group.checks and group.checks == []):
raise HTTPException(422, 'Group must have at least one check or one reply attribute')
group_repo.update(groupname, group)
response.headers['Location'] = f'{API_URL}/groups/{groupname}' # used groupname instead of group.groupname
return group_repo.find_one(groupname) # simply re-fetch the group once updated So it's better but I am still encountering an error easy to reproduce: curl -X 'POST' \
'http://localhost:8000/groups' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"groupname": "my-group",
"replies": [
{
"attribute": "Filter-Id",
"op": ":=",
"value": "10m"
}
]
}' 201 Created: {"groupname":"my-group","checks":[],"replies":[{"attribute":"Filter-Id","op":":=","value":"10m"}],"users":[]} Now I remove the curl -X 'PATCH' \
'http://localhost:8000/groups/my-group' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"replies": []
}'
Trace: INFO: 127.0.0.1:57120 - "POST /groups HTTP/1.1" 201 Created
INFO: 127.0.0.1:42542 - "PATCH /groups/my-group HTTP/1.1" 500 Internal Server Error
ERROR: Exception in ASGI application
Traceback (most recent call last):
File "/tmp/freeradius-api-allow-updates/venv/lib/python3.10/site-packages/uvicorn/protocols/http/httptools_impl.py", line 399, in run_asgi
result = await app( # type: ignore[func-returns-value]
File "/tmp/freeradius-api-allow-updates/venv/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 70, in __call__
return await self.app(scope, receive, send)
File "/tmp/freeradius-api-allow-updates/venv/lib/python3.10/site-packages/fastapi/applications.py", line 1054, in __call__
await super().__call__(scope, receive, send)
File "/tmp/freeradius-api-allow-updates/venv/lib/python3.10/site-packages/starlette/applications.py", line 123, in __call__
await self.middleware_stack(scope, receive, send)
File "/tmp/freeradius-api-allow-updates/venv/lib/python3.10/site-packages/starlette/middleware/errors.py", line 186, in __call__
raise exc
File "/tmp/freeradius-api-allow-updates/venv/lib/python3.10/site-packages/starlette/middleware/errors.py", line 164, in __call__
await self.app(scope, receive, _send)
File "/tmp/freeradius-api-allow-updates/venv/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 65, in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
File "/tmp/freeradius-api-allow-updates/venv/lib/python3.10/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
raise exc
File "/tmp/freeradius-api-allow-updates/venv/lib/python3.10/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
await app(scope, receive, sender)
File "/tmp/freeradius-api-allow-updates/venv/lib/python3.10/site-packages/starlette/routing.py", line 756, in __call__
await self.middleware_stack(scope, receive, send)
File "/tmp/freeradius-api-allow-updates/venv/lib/python3.10/site-packages/starlette/routing.py", line 776, in app
await route.handle(scope, receive, send)
File "/tmp/freeradius-api-allow-updates/venv/lib/python3.10/site-packages/starlette/routing.py", line 297, in handle
await self.app(scope, receive, send)
File "/tmp/freeradius-api-allow-updates/venv/lib/python3.10/site-packages/starlette/routing.py", line 77, in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
File "/tmp/freeradius-api-allow-updates/venv/lib/python3.10/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
raise exc
File "/tmp/freeradius-api-allow-updates/venv/lib/python3.10/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
await app(scope, receive, sender)
File "/tmp/freeradius-api-allow-updates/venv/lib/python3.10/site-packages/starlette/routing.py", line 72, in app
response = await func(request)
File "/tmp/freeradius-api-allow-updates/venv/lib/python3.10/site-packages/fastapi/routing.py", line 296, in app
content = await serialize_response(
File "/tmp/freeradius-api-allow-updates/venv/lib/python3.10/site-packages/fastapi/routing.py", line 155, in serialize_response
raise ResponseValidationError(
fastapi.exceptions.ResponseValidationError: 1 validation errors:
{'type': 'model_attributes_type', 'loc': ('response',), 'msg': 'Input should be a valid dictionary or object to extract fields from', 'input': None} I still appreciate the help and the will to enhance this projet but I am surprised the code wasn't fully tested on your side. That's why I insist about the full code covarage of |
This PR adds support for PUT requests to fully replace an entity in one transaction. Note that you'll need to supply the full object. Non-supplied checks or linked users for example will be removed.This PR adds support for PATCH requests to partially replace an entity in one transaction. You'll need to pass the properties you want to replace. If you want to replace the full object, specify all properties.
See #5