-
-
Notifications
You must be signed in to change notification settings - Fork 50
fix(flows): replace GET /flows/exists with POST and keep deprecated GET alias #275
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
base: main
Are you sure you want to change the base?
Changes from all commits
3eebed2
fb9d1ce
a81a21a
72675aa
841c16e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,29 +7,38 @@ | |||||||||||||||||||||||||||||||||
| from core.conversions import _str_to_num | ||||||||||||||||||||||||||||||||||
| from core.errors import FlowNotFoundError | ||||||||||||||||||||||||||||||||||
| from routers.dependencies import expdb_connection | ||||||||||||||||||||||||||||||||||
| from schemas.flows import Flow, Parameter, Subflow | ||||||||||||||||||||||||||||||||||
| from schemas.flows import Flow, FlowExistsBody, Parameter, Subflow | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| router = APIRouter(prefix="/flows", tags=["flows"]) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @router.get("/exists/{name}/{external_version}") | ||||||||||||||||||||||||||||||||||
| @router.post("/exists") | ||||||||||||||||||||||||||||||||||
| async def flow_exists( | ||||||||||||||||||||||||||||||||||
| name: str, | ||||||||||||||||||||||||||||||||||
| external_version: str, | ||||||||||||||||||||||||||||||||||
| body: FlowExistsBody, | ||||||||||||||||||||||||||||||||||
| expdb: Annotated[AsyncConnection, Depends(expdb_connection)], | ||||||||||||||||||||||||||||||||||
| ) -> dict[Literal["flow_id"], int]: | ||||||||||||||||||||||||||||||||||
| """Check if a Flow with the name and version exists, if so, return the flow id.""" | ||||||||||||||||||||||||||||||||||
| flow = await database.flows.get_by_name( | ||||||||||||||||||||||||||||||||||
| name=name, | ||||||||||||||||||||||||||||||||||
| external_version=external_version, | ||||||||||||||||||||||||||||||||||
| name=body.name, | ||||||||||||||||||||||||||||||||||
| external_version=body.external_version, | ||||||||||||||||||||||||||||||||||
| expdb=expdb, | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| if flow is None: | ||||||||||||||||||||||||||||||||||
| msg = f"Flow with name {name} and external version {external_version} not found." | ||||||||||||||||||||||||||||||||||
| msg = f"Flow with name {body.name} and external version {body.external_version} not found." | ||||||||||||||||||||||||||||||||||
| raise FlowNotFoundError(msg) | ||||||||||||||||||||||||||||||||||
| return {"flow_id": flow.id} | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @router.get("/exists/{name}/{external_version}", deprecated=True) | ||||||||||||||||||||||||||||||||||
| async def flow_exists_get( | ||||||||||||||||||||||||||||||||||
| name: str, | ||||||||||||||||||||||||||||||||||
| external_version: str, | ||||||||||||||||||||||||||||||||||
| expdb: Annotated[AsyncConnection, Depends(expdb_connection)], | ||||||||||||||||||||||||||||||||||
| ) -> dict[Literal["flow_id"], int]: | ||||||||||||||||||||||||||||||||||
| """Use POST /flows/exists instead.""" | ||||||||||||||||||||||||||||||||||
| return await flow_exists(FlowExistsBody(name=name, external_version=external_version), expdb) | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+34
to
+41
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: If you manually instantiate/validate a Pydantic model inside a FastAPI route handler and it raises FastAPI only auto-converts request parsing/validation failures into So, if you want a 422, you must explicitly handle it, e.g. catch Sources: [1] [2] 🏁 Script executed: cd src/routers/openml && head -100 flows.py | cat -nRepository: openml/server-api Length of output: 3829 🏁 Script executed: rg "FlowExistsBody" --type py -A 5 -B 2Repository: openml/server-api Length of output: 4002 🏁 Script executed: rg "def flow_exists" --type py -A 15Repository: openml/server-api Length of output: 2056 Add Path constraints to the GET alias to match POST validation and ensure consistent 422 responses. The GET route accepts 🛠️ Suggested fix-from fastapi import APIRouter, Depends
+from fastapi import APIRouter, Depends, Path
@@
async def flow_exists_get(
- name: str,
- external_version: str,
+ name: Annotated[str, Path(min_length=1, max_length=1024)],
+ external_version: Annotated[str, Path(min_length=1, max_length=128)],
expdb: Annotated[AsyncConnection, Depends(expdb_connection)],
) -> dict[Literal["flow_id"], int]:📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @router.get("/{flow_id}") | ||||||||||||||||||||||||||||||||||
| async def get_flow( | ||||||||||||||||||||||||||||||||||
| flow_id: int, | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
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.
Make the untag operation atomic.
This authorizes against
matched_tag_rowin one query and then does a separate delete in another. Two concurrent untag requests can both pass the precheck; the loser deletes 0 rows but still returns200 OK. Please collapse this into a single authorized delete, or havedatabase.datasets.untag(...)returnrowcountand fail when nothing was actually removed.🤖 Prompt for AI Agents